Initialized merge tracking via "svnmerge" with revisions "1-73579" from
[python/dscho.git] / Lib / quopri.py
blob6b3d13eca451d9008e735a75c19ebb648013b94e
1 #! /usr/bin/env python
3 """Conversions to/from quoted-printable transport encoding as per RFC 1521."""
5 # (Dec 1991 version).
7 __all__ = ["encode", "decode", "encodestring", "decodestring"]
9 ESCAPE = b'='
10 MAXLINESIZE = 76
11 HEX = b'0123456789ABCDEF'
12 EMPTYSTRING = b''
14 try:
15 from binascii import a2b_qp, b2a_qp
16 except ImportError:
17 a2b_qp = None
18 b2a_qp = None
21 def needsquoting(c, quotetabs, header):
22 """Decide whether a particular byte ordinal needs to be quoted.
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'~')
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]))
44 def encode(input, output, quotetabs, header = 0):
45 """Read 'input', apply quoted-printable encoding, and write to 'output'.
47 'input' and 'output' are files with readline() and write() methods.
48 The 'quotetabs' flag indicates whether embedded tabs and spaces should be
49 quoted. Note that line-ending tabs and spaces are always encoded, as per
50 RFC 1521.
51 The 'header' flag indicates whether we are encoding spaces as _ as per
52 RFC 1522.
53 """
55 if b2a_qp is not None:
56 data = input.read()
57 odata = b2a_qp(data, quotetabs = quotetabs, header = header)
58 output.write(odata)
59 return
61 def write(s, output=output, lineEnd=b'\n'):
62 # RFC 1521 requires that the line ending in a space or tab must have
63 # that trailing character encoded.
64 if s and s[-1:] in b' \t':
65 output.write(s[:-1] + quote(s[-1:]) + lineEnd)
66 elif s == b'.':
67 output.write(quote(s) + lineEnd)
68 else:
69 output.write(s + lineEnd)
71 prevline = None
72 while 1:
73 line = input.readline()
74 if not line:
75 break
76 outline = []
77 # Strip off any readline induced trailing newline
78 stripped = b''
79 if line[-1:] == b'\n':
80 line = line[:-1]
81 stripped = b'\n'
82 # Calculate the un-length-limited encoded line
83 for c in line:
84 c = bytes((c,))
85 if needsquoting(c, quotetabs, header):
86 c = quote(c)
87 if header and c == b' ':
88 outline.append(b'_')
89 else:
90 outline.append(c)
91 # First, write out the previous line
92 if prevline is not None:
93 write(prevline)
94 # Now see if we need any soft line breaks because of RFC-imposed
95 # length limitations. Then do the thisline->prevline dance.
96 thisline = EMPTYSTRING.join(outline)
97 while len(thisline) > MAXLINESIZE:
98 # Don't forget to include the soft line break `=' sign in the
99 # length calculation!
100 write(thisline[:MAXLINESIZE-1], lineEnd=b'=\n')
101 thisline = thisline[MAXLINESIZE-1:]
102 # Write out the current line
103 prevline = thisline
104 # Write out the last line, without a trailing newline
105 if prevline is not None:
106 write(prevline, lineEnd=stripped)
108 def encodestring(s, quotetabs = 0, header = 0):
109 if b2a_qp is not None:
110 return b2a_qp(s, quotetabs = quotetabs, header = header)
111 from io import BytesIO
112 infp = BytesIO(s)
113 outfp = BytesIO()
114 encode(infp, outfp, quotetabs, header)
115 return outfp.getvalue()
119 def decode(input, output, header = 0):
120 """Read 'input', apply quoted-printable decoding, and write to 'output'.
121 'input' and 'output' are files with readline() and write() methods.
122 If 'header' is true, decode underscore as space (per RFC 1522)."""
124 if a2b_qp is not None:
125 data = input.read()
126 odata = a2b_qp(data, header = header)
127 output.write(odata)
128 return
130 new = b''
131 while 1:
132 line = input.readline()
133 if not line: break
134 i, n = 0, len(line)
135 if n > 0 and line[n-1:n] == b'\n':
136 partial = 0; n = n-1
137 # Strip trailing whitespace
138 while n > 0 and line[n-1:n] in b" \t\r":
139 n = n-1
140 else:
141 partial = 1
142 while i < n:
143 c = line[i:i+1]
144 if c == b'_' and header:
145 new = new + b' '; i = i+1
146 elif c != ESCAPE:
147 new = new + c; i = i+1
148 elif i+1 == n and not partial:
149 partial = 1; break
150 elif i+1 < n and line[i+1] == ESCAPE:
151 new = new + ESCAPE; i = i+2
152 elif i+2 < n and ishex(line[i+1:i+2]) and ishex(line[i+2:i+3]):
153 new = new + bytes((unhex(line[i+1:i+3]),)); i = i+3
154 else: # Bad escape sequence -- leave it in
155 new = new + c; i = i+1
156 if not partial:
157 output.write(new + b'\n')
158 new = b''
159 if new:
160 output.write(new)
162 def decodestring(s, header = 0):
163 if a2b_qp is not None:
164 return a2b_qp(s, header = header)
165 from io import BytesIO
166 infp = BytesIO(s)
167 outfp = BytesIO()
168 decode(infp, outfp, header = header)
169 return outfp.getvalue()
173 # Other helper functions
174 def ishex(c):
175 """Return true if the byte ordinal 'c' is a hexadecimal digit in ASCII."""
176 assert isinstance(c, bytes)
177 return b'0' <= c <= b'9' or b'a' <= c <= b'f' or b'A' <= c <= b'F'
179 def unhex(s):
180 """Get the integer value of a hexadecimal number."""
181 bits = 0
182 for c in s:
183 c = bytes((c,))
184 if b'0' <= c <= b'9':
185 i = ord('0')
186 elif b'a' <= c <= b'f':
187 i = ord('a')-10
188 elif b'A' <= c <= b'F':
189 i = ord(b'A')-10
190 else:
191 assert False, "non-hex digit "+repr(c)
192 bits = bits*16 + (ord(c) - i)
193 return bits
197 def main():
198 import sys
199 import getopt
200 try:
201 opts, args = getopt.getopt(sys.argv[1:], 'td')
202 except getopt.error as msg:
203 sys.stdout = sys.stderr
204 print(msg)
205 print("usage: quopri [-t | -d] [file] ...")
206 print("-t: quote tabs")
207 print("-d: decode; default encode")
208 sys.exit(2)
209 deco = 0
210 tabs = 0
211 for o, a in opts:
212 if o == '-t': tabs = 1
213 if o == '-d': deco = 1
214 if tabs and deco:
215 sys.stdout = sys.stderr
216 print("-t and -d are mutually exclusive")
217 sys.exit(2)
218 if not args: args = ['-']
219 sts = 0
220 for file in args:
221 if file == '-':
222 fp = sys.stdin.buffer
223 else:
224 try:
225 fp = open(file, "rb")
226 except IOError as msg:
227 sys.stderr.write("%s: can't open (%s)\n" % (file, msg))
228 sts = 1
229 continue
230 try:
231 if deco:
232 decode(fp, sys.stdout.buffer)
233 else:
234 encode(fp, sys.stdout.buffer, tabs)
235 finally:
236 if file != '-':
237 fp.close()
238 if sts:
239 sys.exit(sts)
243 if __name__ == '__main__':
244 main()