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 9ESCAPE = b'=' 10MAXLINESIZE = 76 11HEX = b'0123456789ABCDEF' 12EMPTYSTRING = b'' 13 14try: 15 from binascii import a2b_qp, b2a_qp 16except ImportError: 17 a2b_qp = None 18 b2a_qp = None 19 20 21def 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 36def 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 44def 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 106def 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 117def 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 160def 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 172def 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 177def 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 195def 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 241if __name__ == '__main__': 242 main() 243