]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/quopri.py
dist/mkfile: run binds in subshell
[plan9front.git] / sys / lib / python / quopri.py
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()