contentmanager.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import binascii
  2. import email.charset
  3. import email.message
  4. import email.errors
  5. from email import quoprimime
  6. class ContentManager:
  7. def __init__(self):
  8. self.get_handlers = {}
  9. self.set_handlers = {}
  10. def add_get_handler(self, key, handler):
  11. self.get_handlers[key] = handler
  12. def get_content(self, msg, *args, **kw):
  13. content_type = msg.get_content_type()
  14. if content_type in self.get_handlers:
  15. return self.get_handlers[content_type](msg, *args, **kw)
  16. maintype = msg.get_content_maintype()
  17. if maintype in self.get_handlers:
  18. return self.get_handlers[maintype](msg, *args, **kw)
  19. if '' in self.get_handlers:
  20. return self.get_handlers[''](msg, *args, **kw)
  21. raise KeyError(content_type)
  22. def add_set_handler(self, typekey, handler):
  23. self.set_handlers[typekey] = handler
  24. def set_content(self, msg, obj, *args, **kw):
  25. if msg.get_content_maintype() == 'multipart':
  26. # XXX: is this error a good idea or not? We can remove it later,
  27. # but we can't add it later, so do it for now.
  28. raise TypeError("set_content not valid on multipart")
  29. handler = self._find_set_handler(msg, obj)
  30. msg.clear_content()
  31. handler(msg, obj, *args, **kw)
  32. def _find_set_handler(self, msg, obj):
  33. full_path_for_error = None
  34. for typ in type(obj).__mro__:
  35. if typ in self.set_handlers:
  36. return self.set_handlers[typ]
  37. qname = typ.__qualname__
  38. modname = getattr(typ, '__module__', '')
  39. full_path = '.'.join((modname, qname)) if modname else qname
  40. if full_path_for_error is None:
  41. full_path_for_error = full_path
  42. if full_path in self.set_handlers:
  43. return self.set_handlers[full_path]
  44. if qname in self.set_handlers:
  45. return self.set_handlers[qname]
  46. name = typ.__name__
  47. if name in self.set_handlers:
  48. return self.set_handlers[name]
  49. if None in self.set_handlers:
  50. return self.set_handlers[None]
  51. raise KeyError(full_path_for_error)
  52. raw_data_manager = ContentManager()
  53. def get_text_content(msg, errors='replace'):
  54. content = msg.get_payload(decode=True)
  55. charset = msg.get_param('charset', 'ASCII')
  56. return content.decode(charset, errors=errors)
  57. raw_data_manager.add_get_handler('text', get_text_content)
  58. def get_non_text_content(msg):
  59. return msg.get_payload(decode=True)
  60. for maintype in 'audio image video application'.split():
  61. raw_data_manager.add_get_handler(maintype, get_non_text_content)
  62. del maintype
  63. def get_message_content(msg):
  64. return msg.get_payload(0)
  65. for subtype in 'rfc822 external-body'.split():
  66. raw_data_manager.add_get_handler('message/'+subtype, get_message_content)
  67. del subtype
  68. def get_and_fixup_unknown_message_content(msg):
  69. # If we don't understand a message subtype, we are supposed to treat it as
  70. # if it were application/octet-stream, per
  71. # tools.ietf.org/html/rfc2046#section-5.2.4. Feedparser doesn't do that,
  72. # so do our best to fix things up. Note that it is *not* appropriate to
  73. # model message/partial content as Message objects, so they are handled
  74. # here as well. (How to reassemble them is out of scope for this comment :)
  75. return bytes(msg.get_payload(0))
  76. raw_data_manager.add_get_handler('message',
  77. get_and_fixup_unknown_message_content)
  78. def _prepare_set(msg, maintype, subtype, headers):
  79. msg['Content-Type'] = '/'.join((maintype, subtype))
  80. if headers:
  81. if not hasattr(headers[0], 'name'):
  82. mp = msg.policy
  83. headers = [mp.header_factory(*mp.header_source_parse([header]))
  84. for header in headers]
  85. try:
  86. for header in headers:
  87. if header.defects:
  88. raise header.defects[0]
  89. msg[header.name] = header
  90. except email.errors.HeaderDefect as exc:
  91. raise ValueError("Invalid header: {}".format(
  92. header.fold(policy=msg.policy))) from exc
  93. def _finalize_set(msg, disposition, filename, cid, params):
  94. if disposition is None and filename is not None:
  95. disposition = 'attachment'
  96. if disposition is not None:
  97. msg['Content-Disposition'] = disposition
  98. if filename is not None:
  99. msg.set_param('filename',
  100. filename,
  101. header='Content-Disposition',
  102. replace=True)
  103. if cid is not None:
  104. msg['Content-ID'] = cid
  105. if params is not None:
  106. for key, value in params.items():
  107. msg.set_param(key, value)
  108. # XXX: This is a cleaned-up version of base64mime.body_encode (including a bug
  109. # fix in the calculation of unencoded_bytes_per_line). It would be nice to
  110. # drop both this and quoprimime.body_encode in favor of enhanced binascii
  111. # routines that accepted a max_line_length parameter.
  112. def _encode_base64(data, max_line_length):
  113. encoded_lines = []
  114. unencoded_bytes_per_line = max_line_length // 4 * 3
  115. for i in range(0, len(data), unencoded_bytes_per_line):
  116. thisline = data[i:i+unencoded_bytes_per_line]
  117. encoded_lines.append(binascii.b2a_base64(thisline).decode('ascii'))
  118. return ''.join(encoded_lines)
  119. def _encode_text(string, charset, cte, policy):
  120. lines = string.encode(charset).splitlines()
  121. linesep = policy.linesep.encode('ascii')
  122. def embedded_body(lines): return linesep.join(lines) + linesep
  123. def normal_body(lines): return b'\n'.join(lines) + b'\n'
  124. if cte is None:
  125. # Use heuristics to decide on the "best" encoding.
  126. if max((len(x) for x in lines), default=0) <= policy.max_line_length:
  127. try:
  128. return '7bit', normal_body(lines).decode('ascii')
  129. except UnicodeDecodeError:
  130. pass
  131. if policy.cte_type == '8bit':
  132. return '8bit', normal_body(lines).decode('ascii', 'surrogateescape')
  133. sniff = embedded_body(lines[:10])
  134. sniff_qp = quoprimime.body_encode(sniff.decode('latin-1'),
  135. policy.max_line_length)
  136. sniff_base64 = binascii.b2a_base64(sniff)
  137. # This is a little unfair to qp; it includes lineseps, base64 doesn't.
  138. if len(sniff_qp) > len(sniff_base64):
  139. cte = 'base64'
  140. else:
  141. cte = 'quoted-printable'
  142. if len(lines) <= 10:
  143. return cte, sniff_qp
  144. if cte == '7bit':
  145. data = normal_body(lines).decode('ascii')
  146. elif cte == '8bit':
  147. data = normal_body(lines).decode('ascii', 'surrogateescape')
  148. elif cte == 'quoted-printable':
  149. data = quoprimime.body_encode(normal_body(lines).decode('latin-1'),
  150. policy.max_line_length)
  151. elif cte == 'base64':
  152. data = _encode_base64(embedded_body(lines), policy.max_line_length)
  153. else:
  154. raise ValueError("Unknown content transfer encoding {}".format(cte))
  155. return cte, data
  156. def set_text_content(msg, string, subtype="plain", charset='utf-8', cte=None,
  157. disposition=None, filename=None, cid=None,
  158. params=None, headers=None):
  159. _prepare_set(msg, 'text', subtype, headers)
  160. cte, payload = _encode_text(string, charset, cte, msg.policy)
  161. msg.set_payload(payload)
  162. msg.set_param('charset',
  163. email.charset.ALIASES.get(charset, charset),
  164. replace=True)
  165. msg['Content-Transfer-Encoding'] = cte
  166. _finalize_set(msg, disposition, filename, cid, params)
  167. raw_data_manager.add_set_handler(str, set_text_content)
  168. def set_message_content(msg, message, subtype="rfc822", cte=None,
  169. disposition=None, filename=None, cid=None,
  170. params=None, headers=None):
  171. if subtype == 'partial':
  172. raise ValueError("message/partial is not supported for Message objects")
  173. if subtype == 'rfc822':
  174. if cte not in (None, '7bit', '8bit', 'binary'):
  175. # http://tools.ietf.org/html/rfc2046#section-5.2.1 mandate.
  176. raise ValueError(
  177. "message/rfc822 parts do not support cte={}".format(cte))
  178. # 8bit will get coerced on serialization if policy.cte_type='7bit'. We
  179. # may end up claiming 8bit when it isn't needed, but the only negative
  180. # result of that should be a gateway that needs to coerce to 7bit
  181. # having to look through the whole embedded message to discover whether
  182. # or not it actually has to do anything.
  183. cte = '8bit' if cte is None else cte
  184. elif subtype == 'external-body':
  185. if cte not in (None, '7bit'):
  186. # http://tools.ietf.org/html/rfc2046#section-5.2.3 mandate.
  187. raise ValueError(
  188. "message/external-body parts do not support cte={}".format(cte))
  189. cte = '7bit'
  190. elif cte is None:
  191. # http://tools.ietf.org/html/rfc2046#section-5.2.4 says all future
  192. # subtypes should be restricted to 7bit, so assume that.
  193. cte = '7bit'
  194. _prepare_set(msg, 'message', subtype, headers)
  195. msg.set_payload([message])
  196. msg['Content-Transfer-Encoding'] = cte
  197. _finalize_set(msg, disposition, filename, cid, params)
  198. raw_data_manager.add_set_handler(email.message.Message, set_message_content)
  199. def set_bytes_content(msg, data, maintype, subtype, cte='base64',
  200. disposition=None, filename=None, cid=None,
  201. params=None, headers=None):
  202. _prepare_set(msg, maintype, subtype, headers)
  203. if cte == 'base64':
  204. data = _encode_base64(data, max_line_length=msg.policy.max_line_length)
  205. elif cte == 'quoted-printable':
  206. # XXX: quoprimime.body_encode won't encode newline characters in data,
  207. # so we can't use it. This means max_line_length is ignored. Another
  208. # bug to fix later. (Note: encoders.quopri is broken on line ends.)
  209. data = binascii.b2a_qp(data, istext=False, header=False, quotetabs=True)
  210. data = data.decode('ascii')
  211. elif cte == '7bit':
  212. data = data.decode('ascii')
  213. elif cte in ('8bit', 'binary'):
  214. data = data.decode('ascii', 'surrogateescape')
  215. msg.set_payload(data)
  216. msg['Content-Transfer-Encoding'] = cte
  217. _finalize_set(msg, disposition, filename, cid, params)
  218. for typ in (bytes, bytearray, memoryview):
  219. raw_data_manager.add_set_handler(typ, set_bytes_content)
  220. del typ