Commit | Line | Data |
---|---|---|
920dae64 AT |
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() |