_encoded_words.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. """ Routines for manipulating RFC2047 encoded words.
  2. This is currently a package-private API, but will be considered for promotion
  3. to a public API if there is demand.
  4. """
  5. # An ecoded word looks like this:
  6. #
  7. # =?charset[*lang]?cte?encoded_string?=
  8. #
  9. # for more information about charset see the charset module. Here it is one
  10. # of the preferred MIME charset names (hopefully; you never know when parsing).
  11. # cte (Content Transfer Encoding) is either 'q' or 'b' (ignoring case). In
  12. # theory other letters could be used for other encodings, but in practice this
  13. # (almost?) never happens. There could be a public API for adding entries
  14. # to the CTE tables, but YAGNI for now. 'q' is Quoted Printable, 'b' is
  15. # Base64. The meaning of encoded_string should be obvious. 'lang' is optional
  16. # as indicated by the brackets (they are not part of the syntax) but is almost
  17. # never encountered in practice.
  18. #
  19. # The general interface for a CTE decoder is that it takes the encoded_string
  20. # as its argument, and returns a tuple (cte_decoded_string, defects). The
  21. # cte_decoded_string is the original binary that was encoded using the
  22. # specified cte. 'defects' is a list of MessageDefect instances indicating any
  23. # problems encountered during conversion. 'charset' and 'lang' are the
  24. # corresponding strings extracted from the EW, case preserved.
  25. #
  26. # The general interface for a CTE encoder is that it takes a binary sequence
  27. # as input and returns the cte_encoded_string, which is an ascii-only string.
  28. #
  29. # Each decoder must also supply a length function that takes the binary
  30. # sequence as its argument and returns the length of the resulting encoded
  31. # string.
  32. #
  33. # The main API functions for the module are decode, which calls the decoder
  34. # referenced by the cte specifier, and encode, which adds the appropriate
  35. # RFC 2047 "chrome" to the encoded string, and can optionally automatically
  36. # select the shortest possible encoding. See their docstrings below for
  37. # details.
  38. import re
  39. import base64
  40. import binascii
  41. import functools
  42. from string import ascii_letters, digits
  43. from email import errors
  44. __all__ = ['decode_q',
  45. 'encode_q',
  46. 'decode_b',
  47. 'encode_b',
  48. 'len_q',
  49. 'len_b',
  50. 'decode',
  51. 'encode',
  52. ]
  53. #
  54. # Quoted Printable
  55. #
  56. # regex based decoder.
  57. _q_byte_subber = functools.partial(re.compile(br'=([a-fA-F0-9]{2})').sub,
  58. lambda m: bytes.fromhex(m.group(1).decode()))
  59. def decode_q(encoded):
  60. encoded = encoded.replace(b'_', b' ')
  61. return _q_byte_subber(encoded), []
  62. # dict mapping bytes to their encoded form
  63. class _QByteMap(dict):
  64. safe = b'-!*+/' + ascii_letters.encode('ascii') + digits.encode('ascii')
  65. def __missing__(self, key):
  66. if key in self.safe:
  67. self[key] = chr(key)
  68. else:
  69. self[key] = "={:02X}".format(key)
  70. return self[key]
  71. _q_byte_map = _QByteMap()
  72. # In headers spaces are mapped to '_'.
  73. _q_byte_map[ord(' ')] = '_'
  74. def encode_q(bstring):
  75. return ''.join(_q_byte_map[x] for x in bstring)
  76. def len_q(bstring):
  77. return sum(len(_q_byte_map[x]) for x in bstring)
  78. #
  79. # Base64
  80. #
  81. def decode_b(encoded):
  82. # First try encoding with validate=True, fixing the padding if needed.
  83. # This will succeed only if encoded includes no invalid characters.
  84. pad_err = len(encoded) % 4
  85. missing_padding = b'==='[:4-pad_err] if pad_err else b''
  86. try:
  87. return (
  88. base64.b64decode(encoded + missing_padding, validate=True),
  89. [errors.InvalidBase64PaddingDefect()] if pad_err else [],
  90. )
  91. except binascii.Error:
  92. # Since we had correct padding, this is likely an invalid char error.
  93. #
  94. # The non-alphabet characters are ignored as far as padding
  95. # goes, but we don't know how many there are. So try without adding
  96. # padding to see if it works.
  97. try:
  98. return (
  99. base64.b64decode(encoded, validate=False),
  100. [errors.InvalidBase64CharactersDefect()],
  101. )
  102. except binascii.Error:
  103. # Add as much padding as could possibly be necessary (extra padding
  104. # is ignored).
  105. try:
  106. return (
  107. base64.b64decode(encoded + b'==', validate=False),
  108. [errors.InvalidBase64CharactersDefect(),
  109. errors.InvalidBase64PaddingDefect()],
  110. )
  111. except binascii.Error:
  112. # This only happens when the encoded string's length is 1 more
  113. # than a multiple of 4, which is invalid.
  114. #
  115. # bpo-27397: Just return the encoded string since there's no
  116. # way to decode.
  117. return encoded, [errors.InvalidBase64LengthDefect()]
  118. def encode_b(bstring):
  119. return base64.b64encode(bstring).decode('ascii')
  120. def len_b(bstring):
  121. groups_of_3, leftover = divmod(len(bstring), 3)
  122. # 4 bytes out for each 3 bytes (or nonzero fraction thereof) in.
  123. return groups_of_3 * 4 + (4 if leftover else 0)
  124. _cte_decoders = {
  125. 'q': decode_q,
  126. 'b': decode_b,
  127. }
  128. def decode(ew):
  129. """Decode encoded word and return (string, charset, lang, defects) tuple.
  130. An RFC 2047/2243 encoded word has the form:
  131. =?charset*lang?cte?encoded_string?=
  132. where '*lang' may be omitted but the other parts may not be.
  133. This function expects exactly such a string (that is, it does not check the
  134. syntax and may raise errors if the string is not well formed), and returns
  135. the encoded_string decoded first from its Content Transfer Encoding and
  136. then from the resulting bytes into unicode using the specified charset. If
  137. the cte-decoded string does not successfully decode using the specified
  138. character set, a defect is added to the defects list and the unknown octets
  139. are replaced by the unicode 'unknown' character \\uFDFF.
  140. The specified charset and language are returned. The default for language,
  141. which is rarely if ever encountered, is the empty string.
  142. """
  143. _, charset, cte, cte_string, _ = ew.split('?')
  144. charset, _, lang = charset.partition('*')
  145. cte = cte.lower()
  146. # Recover the original bytes and do CTE decoding.
  147. bstring = cte_string.encode('ascii', 'surrogateescape')
  148. bstring, defects = _cte_decoders[cte](bstring)
  149. # Turn the CTE decoded bytes into unicode.
  150. try:
  151. string = bstring.decode(charset)
  152. except UnicodeDecodeError:
  153. defects.append(errors.UndecodableBytesDefect("Encoded word "
  154. f"contains bytes not decodable using {charset!r} charset"))
  155. string = bstring.decode(charset, 'surrogateescape')
  156. except (LookupError, UnicodeEncodeError):
  157. string = bstring.decode('ascii', 'surrogateescape')
  158. if charset.lower() != 'unknown-8bit':
  159. defects.append(errors.CharsetError(f"Unknown charset {charset!r} "
  160. f"in encoded word; decoded as unknown bytes"))
  161. return string, charset, lang, defects
  162. _cte_encoders = {
  163. 'q': encode_q,
  164. 'b': encode_b,
  165. }
  166. _cte_encode_length = {
  167. 'q': len_q,
  168. 'b': len_b,
  169. }
  170. def encode(string, charset='utf-8', encoding=None, lang=''):
  171. """Encode string using the CTE encoding that produces the shorter result.
  172. Produces an RFC 2047/2243 encoded word of the form:
  173. =?charset*lang?cte?encoded_string?=
  174. where '*lang' is omitted unless the 'lang' parameter is given a value.
  175. Optional argument charset (defaults to utf-8) specifies the charset to use
  176. to encode the string to binary before CTE encoding it. Optional argument
  177. 'encoding' is the cte specifier for the encoding that should be used ('q'
  178. or 'b'); if it is None (the default) the encoding which produces the
  179. shortest encoded sequence is used, except that 'q' is preferred if it is up
  180. to five characters longer. Optional argument 'lang' (default '') gives the
  181. RFC 2243 language string to specify in the encoded word.
  182. """
  183. if charset == 'unknown-8bit':
  184. bstring = string.encode('ascii', 'surrogateescape')
  185. else:
  186. bstring = string.encode(charset)
  187. if encoding is None:
  188. qlen = _cte_encode_length['q'](bstring)
  189. blen = _cte_encode_length['b'](bstring)
  190. # Bias toward q. 5 is arbitrary.
  191. encoding = 'q' if qlen - blen < 5 else 'b'
  192. encoded = _cte_encoders[encoding](bstring)
  193. if lang:
  194. lang = '*' + lang
  195. return "=?{}{}?{}?{}?=".format(charset, lang, encoding, encoded)