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