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