|
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() |