quopri.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. #! /usr/bin/env python
  2. """Conversions to/from quoted-printable transport encoding as per RFC 1521."""
  3. # (Dec 1991 version).
  4. __all__ = ["encode", "decode", "encodestring", "decodestring"]
  5. ESCAPE = '='
  6. MAXLINESIZE = 76
  7. HEX = '0123456789ABCDEF'
  8. EMPTYSTRING = ''
  9. try:
  10. from binascii import a2b_qp, b2a_qp
  11. except ImportError:
  12. a2b_qp = None
  13. b2a_qp = None
  14. def needsquoting(c, quotetabs, header):
  15. """Decide whether a particular character needs to be quoted.
  16. The 'quotetabs' flag indicates whether embedded tabs and spaces should be
  17. quoted. Note that line-ending tabs and spaces are always encoded, as per
  18. RFC 1521.
  19. """
  20. if c in ' \t':
  21. return quotetabs
  22. # if header, we have to escape _ because _ is used to escape space
  23. if c == '_':
  24. return header
  25. return c == ESCAPE or not (' ' <= c <= '~')
  26. def quote(c):
  27. """Quote a single character."""
  28. i = ord(c)
  29. return ESCAPE + HEX[i//16] + HEX[i%16]
  30. def encode(input, output, quotetabs, header = 0):
  31. """Read 'input', apply quoted-printable encoding, and write to 'output'.
  32. 'input' and 'output' are files with readline() and write() methods.
  33. The 'quotetabs' flag indicates whether embedded tabs and spaces should be
  34. quoted. Note that line-ending tabs and spaces are always encoded, as per
  35. RFC 1521.
  36. The 'header' flag indicates whether we are encoding spaces as _ as per
  37. RFC 1522.
  38. """
  39. if b2a_qp is not None:
  40. data = input.read()
  41. odata = b2a_qp(data, quotetabs = quotetabs, header = header)
  42. output.write(odata)
  43. return
  44. def write(s, output=output, lineEnd='\n'):
  45. # RFC 1521 requires that the line ending in a space or tab must have
  46. # that trailing character encoded.
  47. if s and s[-1:] in ' \t':
  48. output.write(s[:-1] + quote(s[-1]) + lineEnd)
  49. elif s == '.':
  50. output.write(quote(s) + lineEnd)
  51. else:
  52. output.write(s + lineEnd)
  53. prevline = None
  54. while 1:
  55. line = input.readline()
  56. if not line:
  57. break
  58. outline = []
  59. # Strip off any readline induced trailing newline
  60. stripped = ''
  61. if line[-1:] == '\n':
  62. line = line[:-1]
  63. stripped = '\n'
  64. # Calculate the un-length-limited encoded line
  65. for c in line:
  66. if needsquoting(c, quotetabs, header):
  67. c = quote(c)
  68. if header and c == ' ':
  69. outline.append('_')
  70. else:
  71. outline.append(c)
  72. # First, write out the previous line
  73. if prevline is not None:
  74. write(prevline)
  75. # Now see if we need any soft line breaks because of RFC-imposed
  76. # length limitations. Then do the thisline->prevline dance.
  77. thisline = EMPTYSTRING.join(outline)
  78. while len(thisline) > MAXLINESIZE:
  79. # Don't forget to include the soft line break `=' sign in the
  80. # length calculation!
  81. write(thisline[:MAXLINESIZE-1], lineEnd='=\n')
  82. thisline = thisline[MAXLINESIZE-1:]
  83. # Write out the current line
  84. prevline = thisline
  85. # Write out the last line, without a trailing newline
  86. if prevline is not None:
  87. write(prevline, lineEnd=stripped)
  88. def encodestring(s, quotetabs = 0, header = 0):
  89. if b2a_qp is not None:
  90. return b2a_qp(s, quotetabs = quotetabs, header = header)
  91. from cStringIO import StringIO
  92. infp = StringIO(s)
  93. outfp = StringIO()
  94. encode(infp, outfp, quotetabs, header)
  95. return outfp.getvalue()
  96. def decode(input, output, header = 0):
  97. """Read 'input', apply quoted-printable decoding, and write to 'output'.
  98. 'input' and 'output' are files with readline() and write() methods.
  99. If 'header' is true, decode underscore as space (per RFC 1522)."""
  100. if a2b_qp is not None:
  101. data = input.read()
  102. odata = a2b_qp(data, header = header)
  103. output.write(odata)
  104. return
  105. new = ''
  106. while 1:
  107. line = input.readline()
  108. if not line: break
  109. i, n = 0, len(line)
  110. if n > 0 and line[n-1] == '\n':
  111. partial = 0; n = n-1
  112. # Strip trailing whitespace
  113. while n > 0 and line[n-1] in " \t\r":
  114. n = n-1
  115. else:
  116. partial = 1
  117. while i < n:
  118. c = line[i]
  119. if c == '_' and header:
  120. new = new + ' '; i = i+1
  121. elif c != ESCAPE:
  122. new = new + c; i = i+1
  123. elif i+1 == n and not partial:
  124. partial = 1; break
  125. elif i+1 < n and line[i+1] == ESCAPE:
  126. new = new + ESCAPE; i = i+2
  127. elif i+2 < n and ishex(line[i+1]) and ishex(line[i+2]):
  128. new = new + chr(unhex(line[i+1:i+3])); i = i+3
  129. else: # Bad escape sequence -- leave it in
  130. new = new + c; i = i+1
  131. if not partial:
  132. output.write(new + '\n')
  133. new = ''
  134. if new:
  135. output.write(new)
  136. def decodestring(s, header = 0):
  137. if a2b_qp is not None:
  138. return a2b_qp(s, header = header)
  139. from cStringIO import StringIO
  140. infp = StringIO(s)
  141. outfp = StringIO()
  142. decode(infp, outfp, header = header)
  143. return outfp.getvalue()
  144. # Other helper functions
  145. def ishex(c):
  146. """Return true if the character 'c' is a hexadecimal digit."""
  147. return '0' <= c <= '9' or 'a' <= c <= 'f' or 'A' <= c <= 'F'
  148. def unhex(s):
  149. """Get the integer value of a hexadecimal number."""
  150. bits = 0
  151. for c in s:
  152. if '0' <= c <= '9':
  153. i = ord('0')
  154. elif 'a' <= c <= 'f':
  155. i = ord('a')-10
  156. elif 'A' <= c <= 'F':
  157. i = ord('A')-10
  158. else:
  159. break
  160. bits = bits*16 + (ord(c) - i)
  161. return bits
  162. def main():
  163. import sys
  164. import getopt
  165. try:
  166. opts, args = getopt.getopt(sys.argv[1:], 'td')
  167. except getopt.error, msg:
  168. sys.stdout = sys.stderr
  169. print msg
  170. print "usage: quopri [-t | -d] [file] ..."
  171. print "-t: quote tabs"
  172. print "-d: decode; default encode"
  173. sys.exit(2)
  174. deco = 0
  175. tabs = 0
  176. for o, a in opts:
  177. if o == '-t': tabs = 1
  178. if o == '-d': deco = 1
  179. if tabs and deco:
  180. sys.stdout = sys.stderr
  181. print "-t and -d are mutually exclusive"
  182. sys.exit(2)
  183. if not args: args = ['-']
  184. sts = 0
  185. for file in args:
  186. if file == '-':
  187. fp = sys.stdin
  188. else:
  189. try:
  190. fp = open(file)
  191. except IOError, msg:
  192. sys.stderr.write("%s: can't open (%s)\n" % (file, msg))
  193. sts = 1
  194. continue
  195. if deco:
  196. decode(fp, sys.stdout)
  197. else:
  198. encode(fp, sys.stdout, tabs)
  199. if fp is not sys.stdin:
  200. fp.close()
  201. if sts:
  202. sys.exit(sts)
  203. if __name__ == '__main__':
  204. main()