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