| #! /usr/bin/env python |
| |
| """Conversions to/from quoted-printable transport encoding as per RFC 1521.""" |
| |
| # (Dec 1991 version). |
| |
| __all__ = ["encode", "decode", "encodestring", "decodestring"] |
| |
| ESCAPE = '=' |
| MAXLINESIZE = 76 |
| HEX = '0123456789ABCDEF' |
| EMPTYSTRING = '' |
| |
| try: |
| from binascii import a2b_qp, b2a_qp |
| except ImportError: |
| a2b_qp = None |
| b2a_qp = None |
| |
| |
| def needsquoting(c, quotetabs, header): |
| """Decide whether a particular character needs to be quoted. |
| |
| The 'quotetabs' flag indicates whether embedded tabs and spaces should be |
| quoted. Note that line-ending tabs and spaces are always encoded, as per |
| RFC 1521. |
| """ |
| if c in ' \t': |
| return quotetabs |
| # if header, we have to escape _ because _ is used to escape space |
| if c == '_': |
| return header |
| return c == ESCAPE or not (' ' <= c <= '~') |
| |
| def quote(c): |
| """Quote a single character.""" |
| i = ord(c) |
| return ESCAPE + HEX[i//16] + HEX[i%16] |
| |
| |
| |
| def encode(input, output, quotetabs, header = 0): |
| """Read 'input', apply quoted-printable encoding, and write to 'output'. |
| |
| 'input' and 'output' are files with readline() and write() methods. |
| The 'quotetabs' flag indicates whether embedded tabs and spaces should be |
| quoted. Note that line-ending tabs and spaces are always encoded, as per |
| RFC 1521. |
| The 'header' flag indicates whether we are encoding spaces as _ as per |
| RFC 1522. |
| """ |
| |
| if b2a_qp is not None: |
| data = input.read() |
| odata = b2a_qp(data, quotetabs = quotetabs, header = header) |
| output.write(odata) |
| return |
| |
| def write(s, output=output, lineEnd='\n'): |
| # RFC 1521 requires that the line ending in a space or tab must have |
| # that trailing character encoded. |
| if s and s[-1:] in ' \t': |
| output.write(s[:-1] + quote(s[-1]) + lineEnd) |
| elif s == '.': |
| output.write(quote(s) + lineEnd) |
| else: |
| output.write(s + lineEnd) |
| |
| prevline = None |
| while 1: |
| line = input.readline() |
| if not line: |
| break |
| outline = [] |
| # Strip off any readline induced trailing newline |
| stripped = '' |
| if line[-1:] == '\n': |
| line = line[:-1] |
| stripped = '\n' |
| # Calculate the un-length-limited encoded line |
| for c in line: |
| if needsquoting(c, quotetabs, header): |
| c = quote(c) |
| if header and c == ' ': |
| outline.append('_') |
| else: |
| outline.append(c) |
| # First, write out the previous line |
| if prevline is not None: |
| write(prevline) |
| # Now see if we need any soft line breaks because of RFC-imposed |
| # length limitations. Then do the thisline->prevline dance. |
| thisline = EMPTYSTRING.join(outline) |
| while len(thisline) > MAXLINESIZE: |
| # Don't forget to include the soft line break `=' sign in the |
| # length calculation! |
| write(thisline[:MAXLINESIZE-1], lineEnd='=\n') |
| thisline = thisline[MAXLINESIZE-1:] |
| # Write out the current line |
| prevline = thisline |
| # Write out the last line, without a trailing newline |
| if prevline is not None: |
| write(prevline, lineEnd=stripped) |
| |
| def encodestring(s, quotetabs = 0, header = 0): |
| if b2a_qp is not None: |
| return b2a_qp(s, quotetabs = quotetabs, header = header) |
| from cStringIO import StringIO |
| infp = StringIO(s) |
| outfp = StringIO() |
| encode(infp, outfp, quotetabs, header) |
| return outfp.getvalue() |
| |
| |
| |
| def decode(input, output, header = 0): |
| """Read 'input', apply quoted-printable decoding, and write to 'output'. |
| 'input' and 'output' are files with readline() and write() methods. |
| If 'header' is true, decode underscore as space (per RFC 1522).""" |
| |
| if a2b_qp is not None: |
| data = input.read() |
| odata = a2b_qp(data, header = header) |
| output.write(odata) |
| return |
| |
| new = '' |
| while 1: |
| line = input.readline() |
| if not line: break |
| i, n = 0, len(line) |
| if n > 0 and line[n-1] == '\n': |
| partial = 0; n = n-1 |
| # Strip trailing whitespace |
| while n > 0 and line[n-1] in " \t\r": |
| n = n-1 |
| else: |
| partial = 1 |
| while i < n: |
| c = line[i] |
| if c == '_' and header: |
| new = new + ' '; i = i+1 |
| elif c != ESCAPE: |
| new = new + c; i = i+1 |
| elif i+1 == n and not partial: |
| partial = 1; break |
| elif i+1 < n and line[i+1] == ESCAPE: |
| new = new + ESCAPE; i = i+2 |
| elif i+2 < n and ishex(line[i+1]) and ishex(line[i+2]): |
| new = new + chr(unhex(line[i+1:i+3])); i = i+3 |
| else: # Bad escape sequence -- leave it in |
| new = new + c; i = i+1 |
| if not partial: |
| output.write(new + '\n') |
| new = '' |
| if new: |
| output.write(new) |
| |
| def decodestring(s, header = 0): |
| if a2b_qp is not None: |
| return a2b_qp(s, header = header) |
| from cStringIO import StringIO |
| infp = StringIO(s) |
| outfp = StringIO() |
| decode(infp, outfp, header = header) |
| return outfp.getvalue() |
| |
| |
| |
| # Other helper functions |
| def ishex(c): |
| """Return true if the character 'c' is a hexadecimal digit.""" |
| return '0' <= c <= '9' or 'a' <= c <= 'f' or 'A' <= c <= 'F' |
| |
| def unhex(s): |
| """Get the integer value of a hexadecimal number.""" |
| bits = 0 |
| for c in s: |
| if '0' <= c <= '9': |
| i = ord('0') |
| elif 'a' <= c <= 'f': |
| i = ord('a')-10 |
| elif 'A' <= c <= 'F': |
| i = ord('A')-10 |
| else: |
| break |
| bits = bits*16 + (ord(c) - i) |
| return bits |
| |
| |
| |
| def main(): |
| import sys |
| import getopt |
| try: |
| opts, args = getopt.getopt(sys.argv[1:], 'td') |
| except getopt.error, msg: |
| sys.stdout = sys.stderr |
| print msg |
| print "usage: quopri [-t | -d] [file] ..." |
| print "-t: quote tabs" |
| print "-d: decode; default encode" |
| sys.exit(2) |
| deco = 0 |
| tabs = 0 |
| for o, a in opts: |
| if o == '-t': tabs = 1 |
| if o == '-d': deco = 1 |
| if tabs and deco: |
| sys.stdout = sys.stderr |
| print "-t and -d are mutually exclusive" |
| sys.exit(2) |
| if not args: args = ['-'] |
| sts = 0 |
| for file in args: |
| if file == '-': |
| fp = sys.stdin |
| else: |
| try: |
| fp = open(file) |
| except IOError, msg: |
| sys.stderr.write("%s: can't open (%s)\n" % (file, msg)) |
| sts = 1 |
| continue |
| if deco: |
| decode(fp, sys.stdout) |
| else: |
| encode(fp, sys.stdout, tabs) |
| if fp is not sys.stdin: |
| fp.close() |
| if sts: |
| sys.exit(sts) |
| |
| |
| |
| if __name__ == '__main__': |
| main() |