blob: 7b448bbc561fd7ef23d97d703f79ac5182f1d1da [file] [log] [blame]
Guido van Rossum105bd981997-07-11 18:39:03 +00001#! /usr/bin/env python
2
Barry Warsaw9b630a52001-06-19 19:07:46 +00003"""Conversions to/from quoted-printable transport encoding as per RFC 1521."""
Guido van Rossume7b146f2000-02-04 15:28:42 +00004
Guido van Rossumf1945461995-06-14 23:43:44 +00005# (Dec 1991 version).
6
Barry Warsaw9b630a52001-06-19 19:07:46 +00007__all__ = ["encode", "decode", "encodestring", "decodestring"]
Skip Montanaroc62c81e2001-02-12 02:00:42 +00008
Guido van Rossumf1945461995-06-14 23:43:44 +00009ESCAPE = '='
10MAXLINESIZE = 76
11HEX = '0123456789ABCDEF'
Barry Warsaw9b630a52001-06-19 19:07:46 +000012EMPTYSTRING = ''
Guido van Rossumf1945461995-06-14 23:43:44 +000013
Barry Warsaw9b630a52001-06-19 19:07:46 +000014
15
Guido van Rossumf1945461995-06-14 23:43:44 +000016def needsquoting(c, quotetabs):
Jeremy Hylton77249442000-10-05 17:24:33 +000017 """Decide whether a particular character needs to be quoted.
Guido van Rossume7b146f2000-02-04 15:28:42 +000018
Barry Warsaw9b630a52001-06-19 19:07:46 +000019 The 'quotetabs' flag indicates whether embedded tabs and spaces should be
20 quoted. Note that line-ending tabs and spaces are always encoded, as per
21 RFC 1521.
22 """
23 if c in ' \t':
24 return quotetabs
25 return c == ESCAPE or not (' ' <= c <= '~')
Guido van Rossumf1945461995-06-14 23:43:44 +000026
27def quote(c):
Jeremy Hylton77249442000-10-05 17:24:33 +000028 """Quote a single character."""
29 i = ord(c)
30 return ESCAPE + HEX[i/16] + HEX[i%16]
Guido van Rossumf1945461995-06-14 23:43:44 +000031
Barry Warsaw9b630a52001-06-19 19:07:46 +000032
33
Guido van Rossumf1945461995-06-14 23:43:44 +000034def encode(input, output, quotetabs):
Jeremy Hylton77249442000-10-05 17:24:33 +000035 """Read 'input', apply quoted-printable encoding, and write to 'output'.
Guido van Rossume7b146f2000-02-04 15:28:42 +000036
Jeremy Hylton77249442000-10-05 17:24:33 +000037 'input' and 'output' are files with readline() and write() methods.
Barry Warsaw9b630a52001-06-19 19:07:46 +000038 The 'quotetabs' flag indicates whether embedded tabs and spaces should be
39 quoted. Note that line-ending tabs and spaces are always encoded, as per
40 RFC 1521.
41 """
42 def write(s, output=output, lineEnd='\n'):
43 # RFC 1521 requires that the line ending in a space or tab must have
44 # that trailing character encoded.
45 if s and s[-1:] in ' \t':
46 output.write(s[:-1] + quote(s[-1]) + lineEnd)
47 else:
48 output.write(s + lineEnd)
49
50 prevline = None
Jeremy Hylton77249442000-10-05 17:24:33 +000051 while 1:
52 line = input.readline()
53 if not line:
54 break
Barry Warsaw9b630a52001-06-19 19:07:46 +000055 outline = []
56 # Strip off any readline induced trailing newline
57 stripped = ''
58 if line[-1:] == '\n':
Jeremy Hylton77249442000-10-05 17:24:33 +000059 line = line[:-1]
Barry Warsaw9b630a52001-06-19 19:07:46 +000060 stripped = '\n'
Barry Warsawdac67ac2001-06-19 22:48:10 +000061 # Calculate the un-length-limited encoded line
Jeremy Hylton77249442000-10-05 17:24:33 +000062 for c in line:
63 if needsquoting(c, quotetabs):
64 c = quote(c)
Barry Warsaw9b630a52001-06-19 19:07:46 +000065 outline.append(c)
Barry Warsawdac67ac2001-06-19 22:48:10 +000066 # First, write out the previous line
Barry Warsaw9b630a52001-06-19 19:07:46 +000067 if prevline is not None:
68 write(prevline)
Barry Warsawdac67ac2001-06-19 22:48:10 +000069 # Now see if we need any soft line breaks because of RFC-imposed
70 # length limitations. Then do the thisline->prevline dance.
71 thisline = EMPTYSTRING.join(outline)
72 while len(thisline) > MAXLINESIZE:
73 # Don't forget to include the soft line break `=' sign in the
74 # length calculation!
75 write(thisline[:MAXLINESIZE-1], lineEnd='=\n')
76 thisline = thisline[MAXLINESIZE-1:]
77 # Write out the current line
78 prevline = thisline
Barry Warsaw9b630a52001-06-19 19:07:46 +000079 # Write out the last line, without a trailing newline
80 if prevline is not None:
81 write(prevline, lineEnd=stripped)
Guido van Rossumf1945461995-06-14 23:43:44 +000082
Barry Warsaw9b630a52001-06-19 19:07:46 +000083def encodestring(s, quotetabs=0):
84 from cStringIO import StringIO
85 infp = StringIO(s)
86 outfp = StringIO()
87 encode(infp, outfp, quotetabs)
88 return outfp.getvalue()
89
90
91
Guido van Rossumf1945461995-06-14 23:43:44 +000092def decode(input, output):
Jeremy Hylton77249442000-10-05 17:24:33 +000093 """Read 'input', apply quoted-printable decoding, and write to 'output'.
Guido van Rossume7b146f2000-02-04 15:28:42 +000094
Jeremy Hylton77249442000-10-05 17:24:33 +000095 'input' and 'output' are files with readline() and write() methods."""
96 new = ''
97 while 1:
98 line = input.readline()
99 if not line: break
100 i, n = 0, len(line)
101 if n > 0 and line[n-1] == '\n':
102 partial = 0; n = n-1
103 # Strip trailing whitespace
Guido van Rossum14b90a42001-03-22 22:30:21 +0000104 while n > 0 and line[n-1] in " \t\r":
Jeremy Hylton77249442000-10-05 17:24:33 +0000105 n = n-1
106 else:
107 partial = 1
108 while i < n:
109 c = line[i]
Fred Drake8152d322000-12-12 23:20:45 +0000110 if c != ESCAPE:
Jeremy Hylton77249442000-10-05 17:24:33 +0000111 new = new + c; i = i+1
112 elif i+1 == n and not partial:
113 partial = 1; break
114 elif i+1 < n and line[i+1] == ESCAPE:
115 new = new + ESCAPE; i = i+2
116 elif i+2 < n and ishex(line[i+1]) and ishex(line[i+2]):
117 new = new + chr(unhex(line[i+1:i+3])); i = i+3
118 else: # Bad escape sequence -- leave it in
119 new = new + c; i = i+1
120 if not partial:
121 output.write(new + '\n')
122 new = ''
123 if new:
124 output.write(new)
Guido van Rossumf1945461995-06-14 23:43:44 +0000125
Barry Warsaw9b630a52001-06-19 19:07:46 +0000126def decodestring(s):
127 from cStringIO import StringIO
128 infp = StringIO(s)
129 outfp = StringIO()
130 decode(infp, outfp)
131 return outfp.getvalue()
132
133
134
135# Other helper functions
Guido van Rossumf1945461995-06-14 23:43:44 +0000136def ishex(c):
Jeremy Hylton77249442000-10-05 17:24:33 +0000137 """Return true if the character 'c' is a hexadecimal digit."""
138 return '0' <= c <= '9' or 'a' <= c <= 'f' or 'A' <= c <= 'F'
Guido van Rossumf1945461995-06-14 23:43:44 +0000139
140def unhex(s):
Jeremy Hylton77249442000-10-05 17:24:33 +0000141 """Get the integer value of a hexadecimal number."""
142 bits = 0
143 for c in s:
144 if '0' <= c <= '9':
145 i = ord('0')
146 elif 'a' <= c <= 'f':
147 i = ord('a')-10
148 elif 'A' <= c <= 'F':
149 i = ord('A')-10
150 else:
151 break
152 bits = bits*16 + (ord(c) - i)
153 return bits
Guido van Rossumf1945461995-06-14 23:43:44 +0000154
Barry Warsaw9b630a52001-06-19 19:07:46 +0000155
156
157def main():
Jeremy Hylton77249442000-10-05 17:24:33 +0000158 import sys
159 import getopt
160 try:
161 opts, args = getopt.getopt(sys.argv[1:], 'td')
162 except getopt.error, msg:
163 sys.stdout = sys.stderr
164 print msg
165 print "usage: quopri [-t | -d] [file] ..."
166 print "-t: quote tabs"
167 print "-d: decode; default encode"
168 sys.exit(2)
169 deco = 0
170 tabs = 0
171 for o, a in opts:
172 if o == '-t': tabs = 1
173 if o == '-d': deco = 1
174 if tabs and deco:
175 sys.stdout = sys.stderr
176 print "-t and -d are mutually exclusive"
177 sys.exit(2)
178 if not args: args = ['-']
179 sts = 0
180 for file in args:
181 if file == '-':
182 fp = sys.stdin
183 else:
184 try:
185 fp = open(file)
186 except IOError, msg:
187 sys.stderr.write("%s: can't open (%s)\n" % (file, msg))
188 sts = 1
189 continue
190 if deco:
191 decode(fp, sys.stdout)
192 else:
193 encode(fp, sys.stdout, tabs)
194 if fp is not sys.stdin:
195 fp.close()
196 if sts:
197 sys.exit(sts)
Guido van Rossumf1945461995-06-14 23:43:44 +0000198
Barry Warsaw9b630a52001-06-19 19:07:46 +0000199
200
Guido van Rossumf1945461995-06-14 23:43:44 +0000201if __name__ == '__main__':
Barry Warsaw9b630a52001-06-19 19:07:46 +0000202 main()