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 9ESCAPE = '=' 10MAXLINESIZE = 76 11HEX = '0123456789ABCDEF' 12EMPTYSTRING = '' 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 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 35def quote(c): 36 """Quote a single character.""" 37 i = ord(c) 38 return ESCAPE + HEX[i//16] + HEX[i%16] 39 40 41 42def 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 105def 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 116def 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 159def 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 171def 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 175def 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 192def 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 236if __name__ == '__main__': 237 main() 238