Home | History | Annotate | Download | only in python2.7
      1 #! /usr/bin/env python
      2 
      3 """Conversions to/from quoted-printable transport encoding as per RFC 1521."""
      4 
      5 # (Dec 1991 version).
      6 
      7 __all__ = ["encode", "decode", "encodestring", "decodestring"]
      8 
      9 ESCAPE = '='
     10 MAXLINESIZE = 76
     11 HEX = '0123456789ABCDEF'
     12 EMPTYSTRING = ''
     13 
     14 try:
     15     from binascii import a2b_qp, b2a_qp
     16 except ImportError:
     17     a2b_qp = None
     18     b2a_qp = None
     19 
     20 
     21 def needsquoting(c, quotetabs, header):
     22     """Decide whether a particular character needs to be quoted.
     23 
     24     The 'quotetabs' flag indicates whether embedded tabs and spaces should be
     25     quoted.  Note that line-ending tabs and spaces are always encoded, as per
     26     RFC 1521.
     27     """
     28     if c in ' \t':
     29         return quotetabs
     30     # if header, we have to escape _ because _ is used to escape space
     31     if c == '_':
     32         return header
     33     return c == ESCAPE or not (' ' <= c <= '~')
     34 
     35 def quote(c):
     36     """Quote a single character."""
     37     i = ord(c)
     38     return ESCAPE + HEX[i//16] + HEX[i%16]
     39 
     40 
     41 
     42 def encode(input, output, quotetabs, header = 0):
     43     """Read 'input', apply quoted-printable encoding, and write to 'output'.
     44 
     45     'input' and 'output' are files with readline() and write() methods.
     46     The 'quotetabs' flag indicates whether embedded tabs and spaces should be
     47     quoted.  Note that line-ending tabs and spaces are always encoded, as per
     48     RFC 1521.
     49     The 'header' flag indicates whether we are encoding spaces as _ as per
     50     RFC 1522.
     51     """
     52 
     53     if b2a_qp is not None:
     54         data = input.read()
     55         odata = b2a_qp(data, quotetabs = quotetabs, header = header)
     56         output.write(odata)
     57         return
     58 
     59     def write(s, output=output, lineEnd='\n'):
     60         # RFC 1521 requires that the line ending in a space or tab must have
     61         # that trailing character encoded.
     62         if s and s[-1:] in ' \t':
     63             output.write(s[:-1] + quote(s[-1]) + lineEnd)
     64         elif s == '.':
     65             output.write(quote(s) + lineEnd)
     66         else:
     67             output.write(s + lineEnd)
     68 
     69     prevline = None
     70     while 1:
     71         line = input.readline()
     72         if not line:
     73             break
     74         outline = []
     75         # Strip off any readline induced trailing newline
     76         stripped = ''
     77         if line[-1:] == '\n':
     78             line = line[:-1]
     79             stripped = '\n'
     80         # Calculate the un-length-limited encoded line
     81         for c in line:
     82             if needsquoting(c, quotetabs, header):
     83                 c = quote(c)
     84             if header and c == ' ':
     85                 outline.append('_')
     86             else:
     87                 outline.append(c)
     88         # First, write out the previous line
     89         if prevline is not None:
     90             write(prevline)
     91         # Now see if we need any soft line breaks because of RFC-imposed
     92         # length limitations.  Then do the thisline->prevline dance.
     93         thisline = EMPTYSTRING.join(outline)
     94         while len(thisline) > MAXLINESIZE:
     95             # Don't forget to include the soft line break `=' sign in the
     96             # length calculation!
     97             write(thisline[:MAXLINESIZE-1], lineEnd='=\n')
     98             thisline = thisline[MAXLINESIZE-1:]
     99         # Write out the current line
    100         prevline = thisline
    101     # Write out the last line, without a trailing newline
    102     if prevline is not None:
    103         write(prevline, lineEnd=stripped)
    104 
    105 def encodestring(s, quotetabs = 0, header = 0):
    106     if b2a_qp is not None:
    107         return b2a_qp(s, quotetabs = quotetabs, header = header)
    108     from cStringIO import StringIO
    109     infp = StringIO(s)
    110     outfp = StringIO()
    111     encode(infp, outfp, quotetabs, header)
    112     return outfp.getvalue()
    113 
    114 
    115 
    116 def decode(input, output, header = 0):
    117     """Read 'input', apply quoted-printable decoding, and write to 'output'.
    118     'input' and 'output' are files with readline() and write() methods.
    119     If 'header' is true, decode underscore as space (per RFC 1522)."""
    120 
    121     if a2b_qp is not None:
    122         data = input.read()
    123         odata = a2b_qp(data, header = header)
    124         output.write(odata)
    125         return
    126 
    127     new = ''
    128     while 1:
    129         line = input.readline()
    130         if not line: break
    131         i, n = 0, len(line)
    132         if n > 0 and line[n-1] == '\n':
    133             partial = 0; n = n-1
    134             # Strip trailing whitespace
    135             while n > 0 and line[n-1] in " \t\r":
    136                 n = n-1
    137         else:
    138             partial = 1
    139         while i < n:
    140             c = line[i]
    141             if c == '_' and header:
    142                 new = new + ' '; i = i+1
    143             elif c != ESCAPE:
    144                 new = new + c; i = i+1
    145             elif i+1 == n and not partial:
    146                 partial = 1; break
    147             elif i+1 < n and line[i+1] == ESCAPE:
    148                 new = new + ESCAPE; i = i+2
    149             elif i+2 < n and ishex(line[i+1]) and ishex(line[i+2]):
    150                 new = new + chr(unhex(line[i+1:i+3])); i = i+3
    151             else: # Bad escape sequence -- leave it in
    152                 new = new + c; i = i+1
    153         if not partial:
    154             output.write(new + '\n')
    155             new = ''
    156     if new:
    157         output.write(new)
    158 
    159 def decodestring(s, header = 0):
    160     if a2b_qp is not None:
    161         return a2b_qp(s, header = header)
    162     from cStringIO import StringIO
    163     infp = StringIO(s)
    164     outfp = StringIO()
    165     decode(infp, outfp, header = header)
    166     return outfp.getvalue()
    167 
    168 
    169 
    170 # Other helper functions
    171 def ishex(c):
    172     """Return true if the character 'c' is a hexadecimal digit."""
    173     return '0' <= c <= '9' or 'a' <= c <= 'f' or 'A' <= c <= 'F'
    174 
    175 def unhex(s):
    176     """Get the integer value of a hexadecimal number."""
    177     bits = 0
    178     for c in s:
    179         if '0' <= c <= '9':
    180             i = ord('0')
    181         elif 'a' <= c <= 'f':
    182             i = ord('a')-10
    183         elif 'A' <= c <= 'F':
    184             i = ord('A')-10
    185         else:
    186             break
    187         bits = bits*16 + (ord(c) - i)
    188     return bits
    189 
    190 
    191 
    192 def main():
    193     import sys
    194     import getopt
    195     try:
    196         opts, args = getopt.getopt(sys.argv[1:], 'td')
    197     except getopt.error, msg:
    198         sys.stdout = sys.stderr
    199         print msg
    200         print "usage: quopri [-t | -d] [file] ..."
    201         print "-t: quote tabs"
    202         print "-d: decode; default encode"
    203         sys.exit(2)
    204     deco = 0
    205     tabs = 0
    206     for o, a in opts:
    207         if o == '-t': tabs = 1
    208         if o == '-d': deco = 1
    209     if tabs and deco:
    210         sys.stdout = sys.stderr
    211         print "-t and -d are mutually exclusive"
    212         sys.exit(2)
    213     if not args: args = ['-']
    214     sts = 0
    215     for file in args:
    216         if file == '-':
    217             fp = sys.stdin
    218         else:
    219             try:
    220                 fp = open(file)
    221             except IOError, msg:
    222                 sys.stderr.write("%s: can't open (%s)\n" % (file, msg))
    223                 sts = 1
    224                 continue
    225         if deco:
    226             decode(fp, sys.stdout)
    227         else:
    228             encode(fp, sys.stdout, tabs)
    229         if fp is not sys.stdin:
    230             fp.close()
    231     if sts:
    232         sys.exit(sts)
    233 
    234 
    235 
    236 if __name__ == '__main__':
    237     main()
    238