#! /usr/bin/env python | |
"""Conversions to/from quoted-printable transport encoding as per RFC 1521.""" | |
# (Dec 1991 version). | |
__all__ = ["encode", "decode", "encodestring", "decodestring"] | |
ESCAPE = '=' | |
MAXLINESIZE = 76 | |
HEX = '0123456789ABCDEF' | |
EMPTYSTRING = '' | |
try: | |
from binascii import a2b_qp, b2a_qp | |
except ImportError: | |
a2b_qp = None | |
b2a_qp = None | |
def needsquoting(c, quotetabs, header): | |
"""Decide whether a particular character needs to be quoted. | |
The 'quotetabs' flag indicates whether embedded tabs and spaces should be | |
quoted. Note that line-ending tabs and spaces are always encoded, as per | |
RFC 1521. | |
""" | |
if c in ' \t': | |
return quotetabs | |
# if header, we have to escape _ because _ is used to escape space | |
if c == '_': | |
return header | |
return c == ESCAPE or not (' ' <= c <= '~') | |
def quote(c): | |
"""Quote a single character.""" | |
i = ord(c) | |
return ESCAPE + HEX[i//16] + HEX[i%16] | |
def encode(input, output, quotetabs, header = 0): | |
"""Read 'input', apply quoted-printable encoding, and write to 'output'. | |
'input' and 'output' are files with readline() and write() methods. | |
The 'quotetabs' flag indicates whether embedded tabs and spaces should be | |
quoted. Note that line-ending tabs and spaces are always encoded, as per | |
RFC 1521. | |
The 'header' flag indicates whether we are encoding spaces as _ as per | |
RFC 1522. | |
""" | |
if b2a_qp is not None: | |
data = input.read() | |
odata = b2a_qp(data, quotetabs = quotetabs, header = header) | |
output.write(odata) | |
return | |
def write(s, output=output, lineEnd='\n'): | |
# RFC 1521 requires that the line ending in a space or tab must have | |
# that trailing character encoded. | |
if s and s[-1:] in ' \t': | |
output.write(s[:-1] + quote(s[-1]) + lineEnd) | |
elif s == '.': | |
output.write(quote(s) + lineEnd) | |
else: | |
output.write(s + lineEnd) | |
prevline = None | |
while 1: | |
line = input.readline() | |
if not line: | |
break | |
outline = [] | |
# Strip off any readline induced trailing newline | |
stripped = '' | |
if line[-1:] == '\n': | |
line = line[:-1] | |
stripped = '\n' | |
# Calculate the un-length-limited encoded line | |
for c in line: | |
if needsquoting(c, quotetabs, header): | |
c = quote(c) | |
if header and c == ' ': | |
outline.append('_') | |
else: | |
outline.append(c) | |
# First, write out the previous line | |
if prevline is not None: | |
write(prevline) | |
# Now see if we need any soft line breaks because of RFC-imposed | |
# length limitations. Then do the thisline->prevline dance. | |
thisline = EMPTYSTRING.join(outline) | |
while len(thisline) > MAXLINESIZE: | |
# Don't forget to include the soft line break `=' sign in the | |
# length calculation! | |
write(thisline[:MAXLINESIZE-1], lineEnd='=\n') | |
thisline = thisline[MAXLINESIZE-1:] | |
# Write out the current line | |
prevline = thisline | |
# Write out the last line, without a trailing newline | |
if prevline is not None: | |
write(prevline, lineEnd=stripped) | |
def encodestring(s, quotetabs = 0, header = 0): | |
if b2a_qp is not None: | |
return b2a_qp(s, quotetabs = quotetabs, header = header) | |
from cStringIO import StringIO | |
infp = StringIO(s) | |
outfp = StringIO() | |
encode(infp, outfp, quotetabs, header) | |
return outfp.getvalue() | |
def decode(input, output, header = 0): | |
"""Read 'input', apply quoted-printable decoding, and write to 'output'. | |
'input' and 'output' are files with readline() and write() methods. | |
If 'header' is true, decode underscore as space (per RFC 1522).""" | |
if a2b_qp is not None: | |
data = input.read() | |
odata = a2b_qp(data, header = header) | |
output.write(odata) | |
return | |
new = '' | |
while 1: | |
line = input.readline() | |
if not line: break | |
i, n = 0, len(line) | |
if n > 0 and line[n-1] == '\n': | |
partial = 0; n = n-1 | |
# Strip trailing whitespace | |
while n > 0 and line[n-1] in " \t\r": | |
n = n-1 | |
else: | |
partial = 1 | |
while i < n: | |
c = line[i] | |
if c == '_' and header: | |
new = new + ' '; i = i+1 | |
elif c != ESCAPE: | |
new = new + c; i = i+1 | |
elif i+1 == n and not partial: | |
partial = 1; break | |
elif i+1 < n and line[i+1] == ESCAPE: | |
new = new + ESCAPE; i = i+2 | |
elif i+2 < n and ishex(line[i+1]) and ishex(line[i+2]): | |
new = new + chr(unhex(line[i+1:i+3])); i = i+3 | |
else: # Bad escape sequence -- leave it in | |
new = new + c; i = i+1 | |
if not partial: | |
output.write(new + '\n') | |
new = '' | |
if new: | |
output.write(new) | |
def decodestring(s, header = 0): | |
if a2b_qp is not None: | |
return a2b_qp(s, header = header) | |
from cStringIO import StringIO | |
infp = StringIO(s) | |
outfp = StringIO() | |
decode(infp, outfp, header = header) | |
return outfp.getvalue() | |
# Other helper functions | |
def ishex(c): | |
"""Return true if the character 'c' is a hexadecimal digit.""" | |
return '0' <= c <= '9' or 'a' <= c <= 'f' or 'A' <= c <= 'F' | |
def unhex(s): | |
"""Get the integer value of a hexadecimal number.""" | |
bits = 0 | |
for c in s: | |
if '0' <= c <= '9': | |
i = ord('0') | |
elif 'a' <= c <= 'f': | |
i = ord('a')-10 | |
elif 'A' <= c <= 'F': | |
i = ord('A')-10 | |
else: | |
break | |
bits = bits*16 + (ord(c) - i) | |
return bits | |
def main(): | |
import sys | |
import getopt | |
try: | |
opts, args = getopt.getopt(sys.argv[1:], 'td') | |
except getopt.error, msg: | |
sys.stdout = sys.stderr | |
print msg | |
print "usage: quopri [-t | -d] [file] ..." | |
print "-t: quote tabs" | |
print "-d: decode; default encode" | |
sys.exit(2) | |
deco = 0 | |
tabs = 0 | |
for o, a in opts: | |
if o == '-t': tabs = 1 | |
if o == '-d': deco = 1 | |
if tabs and deco: | |
sys.stdout = sys.stderr | |
print "-t and -d are mutually exclusive" | |
sys.exit(2) | |
if not args: args = ['-'] | |
sts = 0 | |
for file in args: | |
if file == '-': | |
fp = sys.stdin | |
else: | |
try: | |
fp = open(file) | |
except IOError, msg: | |
sys.stderr.write("%s: can't open (%s)\n" % (file, msg)) | |
sts = 1 | |
continue | |
if deco: | |
decode(fp, sys.stdout) | |
else: | |
encode(fp, sys.stdout, tabs) | |
if fp is not sys.stdin: | |
fp.close() | |
if sts: | |
sys.exit(sts) | |
if __name__ == '__main__': | |
main() |