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
9ESCAPE = '='
10MAXLINESIZE = 76
11HEX = '0123456789ABCDEF'
12EMPTYSTRING = ''
13
14try:
15    from binascii import a2b_qp, b2a_qp
16except ImportError:
17    a2b_qp = None
18    b2a_qp = None
19
20
21def 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
35def quote(c):
36    """Quote a single character."""
37    i = ord(c)
38    return ESCAPE + HEX[i//16] + HEX[i%16]
39
40
41
42def 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
105def 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
116def 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
159def 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
171def 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
175def 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
192def 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
236if __name__ == '__main__':
237    main()
238