diff --git a/Doc/library/email.contentmanager.rst b/Doc/library/email.contentmanager.rst index 3ec67cd..376ddf3 100644 --- a/Doc/library/email.contentmanager.rst +++ b/Doc/library/email.contentmanager.rst @@ -119,64 +119,84 @@ when creating MIME parts from content when using the :class:`ObjectManager`. :mod:`~email.policy`. - .. method:: set_content(*args, headers=None, content_manager=None, **kw) + .. method:: set_content(*args, content_manager=None, **kw) Calls the ``set_content`` method of the *content_manager*, passing itself as the message object, and passing along any other arguments or keywords - as additional arguments. *headers* is a list of header objects to be - added to the message, which is done before the *content_manager* is - called. If *content_manager* is not specified, it defaults to the - ``content_manager`` specified by the current :mod:`~email.policy`. + as additional arguments. If *content_manager* is not specified, it + defaults to the ``content_manager`` specified by the current + :mod:`~email.policy`. - .. method:: make_related() + .. method:: make_related(boundary=None) Convert a non-``multipart`` message into a ``multipart/related`` message, - moving the existing content into the (new) first part of the ``mulitpart``. + moving any existing content into the (new) first part of the + ``mulitpart``. Headers starting from :mailheader:`Content-Type` to the + end of the header list are moved to the new sub-part, any earlier headers + are left in the base part. Optional *boundary* is the boundary string + for the newly created multipart. When ``None`` (the default), the + boundary is calculated when needed (for example, when the message is + serialized). - .. method:: make_alternative() + .. method:: make_alternative(boundary=None) Convert a non-``multipart`` or a ``multipart-related`` into a ``multipart/alternative``, moving the existing content into the (new) - first part of the ``multipart``. + first part of the ``multipart``. Headers starting from + :mailheader:`Content-Type` to the end of the header list are moved to the + new sub-part, any earlier headers are left in the base part. Optional + *boundary* is the boundary string for the newly created multipart. When + ``None`` (the default), the boundary is calculated when needed (for + example, when the message is serialized). - .. method:: make_mixed() + .. method:: make_mixed(boundary=None) Convert a non-``multipart``, a ``multipart-related``, or a ``multipart-alternative`` into a ``multipart/alternative``, moving the - existing content into the (new) first part of the ``multipart``. + existing content into the (new) first part of the ``multipart``. Headers + starting from :mailheader:`Content-Type` to the end of the header list + are moved to the new sub-part, any earlier headers are left in the base + part. Optional *boundary* is the boundary string for the newly created + multipart. When ``None`` (the default), the boundary is calculated when + needed (for example, when the message is serialized). - .. method:: add_related(*args, **kw) + .. method:: add_related(*args, content_manager=None, **kw) If the message is a ``multipart/related``, create a new message object, pass all of the arguments to its :meth:`set_content` method, and :meth:`~email.message.Message.attach` it to the ``multipart``. If the message is a non-``multipart``, call :meth:`make_related` and then proceeds as above. If the message is any other type of ``multipart``, - raise a :exc:`TypeError`. + raise a :exc:`TypeError`. If *content_manager* is not specified, it + defaults to the ``content_manager`` specified by the current + :mod:`~email.policy`. - .. method:: add_alternative(*args, **kw) + .. method:: add_alternative(*args, content_manager=None, **kw) If the message is a ``multipart/alternative``, create a new message object, pass all of the arguments to its :meth:`set_content` method, and :meth:`~email.message.Message.attach` it to the ``multipart``. If the message is a non-``multipart`` or ``multipart-related``, call :meth:`make_alternative` and then proceeds as above. If the message is - any other type of ``multipart``, raise a :exc:`TypeError`. + any other type of ``multipart``, raise a :exc:`TypeError`. If + *content_manager* is not specified, it defaults to the + ``content_manager`` specified by the current :mod:`~email.policy`. - .. method:: add_attachment(*args, **kw) + .. method:: add_attachment(*args, content_manager=None, **kw) If the message is a ``multipart/mixed``, create a new message object, pass all of the arguments to its :meth:`set_content` method, and :meth:`~email.message.Message.attach` it to the ``multipart``. If the message is a non-``multipart``, ``multipart-related``, or ``multipart/alternative``, call :meth:`make_mixed` and then proceeds as - above. + above. If *content_manager* is not specified, it defaults to the + ``content_manager`` specified by the current :mod:`~email.policy`. .. class ContentManager() @@ -185,38 +205,48 @@ when creating MIME parts from content when using the :class:`ObjectManager`. map MIME content types to other representations, as well as the ``get_content`` and ``set_content`` dispatch methods. + .. method get_content(msg, *args, **kw) - Extract the payload from *msg* and return an object that encodes information - about the extracted data. + Look up a handler function based on the ``mimetype`` of *msg*, call it, + passing through all arguments, and return the result of the call. The + expectation is that the handler will extract the payload from *msg* and + return an object that encodes information about the extracted data. + If the full ``mimetype`` of the message is found in the registry, + call the associated handler. If not, but a handler is registered + for just the ``maintype``, call that handler. If there is no handler + for the ``maintype``, but there is a handler registered for the + empty string, call that handler. If there are no handlers for any + of these keys, raise a :exc:`KeyError` for the full ``mimetype``. .. method set_content(msg, obj, *args, **kw) - Transform and store *obj* into *msg*, possibly making other changes to *msg* - as well, such as adding various MIME headers to encode information needed - to interpret the stored data. + Look up a handler function based on the type of *obj* and call it, + passing through all arguments. The expectation is that the handler will + transform and store *obj* into *msg*, possibly making other changes to + *msg* as well, such as adding various MIME headers to encode information + needed to interpret the stored data. First use the object's type + (``type(obj)``) as a key to look for a handler in the registry, and call + it if found. If not, use the type's fully qualified name + (``type(obj).__module__ + '.' + type(obj).__qualname__``). If no handler + is found for that key, use the type's qualname + (``type(obj).__qualname__``). If no handler is found, use the type's + name (``type(obj).__name__``). If no handler is found, repeat these four + checks for each remaining type in the type's ``__mro__``. Finally, if no + other handler is found, but a hander exists under the key ``None``, call + that handler. Otherwise raise a :exc:``KeyError`` for the fully + qualified name of the type. + + + .. method add_get_handler(key, handler) - .. method add_get_handler(mimetype, handler) + Record the function *handler* as the handler for *key*. For the possible + values of *key*, see :meth:`get_content`. - Record *handler* as the function to call when :meth:`get_content` is - called on a message whose ``mimetype`` is *mimetype*. *mimetype* is a - string of the form ``maintype[/subtype]``. That is, ``subtype`` is - optional; if only a ``maintype`` is given, then if there is no more - specific match for the ``mimetype`` of a message, the handler - corresponding to its ``maintype`` is called. .. method add_set_handler(typekey, handler) Record *handler* as the function to call when an object of a type - matching *typekey* is passed to :meth:`set_content`. *typekey* may be - one of three things: an actual ``type`` object (eg: ``str``), the - ``__qualname__`` of a type (eg: ``email.message.Message``), or the - ``__name__`` of a type (eg: ``Message``). The preceding is the order in - which ``set_content`` does lookup attempts when passed an object. That - is, given an object ``obj``, ``set_content`` will first try to look the - object up by ``obj.__class__``, then by ``obj.__class__.__qualname__``, - then by ``obj.__class__.__name__``. If no match is found, the lookup - sequence is repeated for each class in ``obj.__mro__`` until a match is - found. To register a default handler, therefore, register it under the - *typekey* ``object`` or ``"object"``. + matching *typekey* is passed to :meth:`set_content`. For the possible + values of *typekey*, see :meth:`set_content`. diff --git a/Doc/library/email.policy.rst b/Doc/library/email.policy.rst index 5856879..100d855 100644 --- a/Doc/library/email.policy.rst +++ b/Doc/library/email.policy.rst @@ -408,6 +408,18 @@ added matters. To illustrate:: fields are treated as unstructured. This list will be completed before the extension is marked stable.) + .. attribute:: content_manager + + An object with at least two methods: get_content and set_content. When + the :meth:`~email.message.Message.get_content` or + :meth:`~email.message.Message.set_content` method of a + :class:`~email.message.Message` object is called, it calls the + corresponding method of this object, passing it the message object as its + first argument, and any arguments or keywords that were passed to it as + additional arguments. By default ``content_manager`` is set to an + instance of the :class:`~email.contentmanager.ObjectManager`` class. + + The class provides the following concrete implementations of the abstract methods of :class:`Policy`: diff --git a/Lib/email/contentmanager.py b/Lib/email/contentmanager.py index 4e05e63..6ab06c3 100644 --- a/Lib/email/contentmanager.py +++ b/Lib/email/contentmanager.py @@ -1,2 +1,47 @@ -class ObjectManager: +class ContentManager: + + def __init__(self): + self.get_handlers = {} + self.set_handlers = {} + + def add_get_handler(self, key, handler): + self.get_handlers[key] = handler + + def get_content(self, msg, *args, **kw): + content_type = msg.get_content_type() + if content_type in self.get_handlers: + return self.get_handlers[content_type](msg, *args, **kw) + maintype = msg.get_content_maintype() + if maintype in self.get_handlers: + return self.get_handlers[maintype](msg, *args, **kw) + if '' in self.get_handlers: + return self.get_handlers[''](msg, *args, **kw) + raise KeyError(content_type) + + def add_set_handler(self, typekey, handler): + self.set_handlers[typekey] = handler + + def set_content(self, msg, obj, *args, **kw): + full_path_for_error = None + for typ in type(obj).__mro__: + if typ in self.set_handlers: + return self.set_handlers[typ](msg, obj, *args, **kw) + qname = typ.__qualname__ + modname = getattr(typ, '__module__', '') + full_path = '.'.join((modname, qname)) if modname else qname + if full_path_for_error is None: + full_path_for_error = full_path + if full_path in self.set_handlers: + return self.set_handlers[full_path](msg, obj, *args, **kw) + if qname in self.set_handlers: + return self.set_handlers[qname](msg, obj, *args, **kw) + name = typ.__name__ + if name in self.set_handlers: + return self.set_handlers[name](msg, obj, *args, **kw) + if None in self.set_handlers: + return self.set_handlers[None](msg, obj, *args, **kw) + raise KeyError(full_path_for_error) + + +class ObjectManager(ContentManager): pass diff --git a/Lib/email/message.py b/Lib/email/message.py index 52d8e9f..cf42e27 100644 --- a/Lib/email/message.py +++ b/Lib/email/message.py @@ -913,13 +913,19 @@ class MIMEMessage(Message): def get_body(self, preferencelist=('related', 'html', 'plain')): found = [None] * len(preferencelist) + skip_next = False for part in self.walk(): maintype, subtype = part.get_content_type().split('/') - if (subtype not in preferencelist or + if (skip_next or subtype not in preferencelist or not (maintype == 'text' and subtype in ('html', 'plain')) and not (maintype == 'multipart' and subtype == 'related') or part.get('content-disposition') not in (None, 'inline')): + skip_next = False + if maintype == 'message': + # Walk returns the message in the message part as if the + # message part were a multipart, even though it isn't. + skip_next = True continue priority = preferencelist.index(subtype) if priority == 0: @@ -947,5 +953,67 @@ class MIMEMessage(Message): yield part def iter_parts(self): - if self.is_multipart(): + if self.get_content_maintype() == 'multipart': yield from self.get_payload() + + def get_content(self, *args, content_manager=None, **kw): + if content_manager is None: + content_manager = self.policy.content_manager + return content_manager.get_content(self, *args, **kw) + + def set_content(self, *args, headers=[], content_manager=None, **kw): + if content_manager is None: + content_manager = self.policy.content_manager + for header in headers: + self.add_header(header.name, header) + content_manager.set_content(self, *args, **kw) + + def _make_multipart(self, subtype, disallowed_subtypes, boundary): + if self.get_content_maintype() == 'multipart': + existing_subtype = self.get_content_subtype() + disallowed_subtypes = disallowed_subtypes + (subtype,) + if existing_subtype in disallowed_subtypes: + raise ValueError("Cannot convert {} to {}".format( + existing_subtype, subtype)) + part = type(self)(policy=self.policy) + headers = iter(self.items()) + for i, (name, value) in enumerate(headers): + if name.lower() == 'content-type': + part[name] = value + break + else: + i = -1 + for name, value in headers: + part[name] = value + self._headers[i:] = [] + part._payload = self._payload + self._payload = [part] + self['Content-Type'] = 'multipart/' + subtype + if boundary is not None: + self.set_param('boundary', boundary) + + def make_related(self, boundary=None): + self._make_multipart('related', ('alternative', 'mixed'), boundary) + + def make_alternative(self, boundary=None): + self._make_multipart('alternative', ('mixed',), boundary) + + def make_mixed(self, boundary=None): + self._make_multipart('mixed', (), boundary) + + def _add_multipart(self, subtype, *args, **kw): + if (self.get_content_maintype() != 'multipart' or + self.get_content_subtype() != subtype): + getattr(self, 'make_' + subtype)() + part = type(self)(policy=self.policy) + part.set_content(*args, **kw) + self.attach(part) + + def add_related(self, *args, **kw): + self._add_multipart('related', *args, **kw) + + def add_alternative(self, *args, **kw): + self._add_multipart('alternative', *args, **kw) + + def add_attachment(self, *args, **kw): + self._add_multipart('mixed', *args, **kw) diff --git a/Lib/email/policy.py b/Lib/email/policy.py index 38e88af..0e53feb 100644 --- a/Lib/email/policy.py +++ b/Lib/email/policy.py @@ -5,6 +5,7 @@ code that adds all the email6 features. from email._policybase import Policy, Compat32, compat32, _extend_docstrings from email.utils import _has_surrogates from email.headerregistry import HeaderRegistry as HeaderRegistry +from email.contentmanager import ObjectManager __all__ = [ 'Compat32', @@ -58,10 +59,20 @@ class EmailPolicy(Policy): special treatment, while all other fields are treated as unstructured. This list will be completed before the extension is marked stable.) + + content_manager -- an object with at least two methods: get_content + and set_content. When the get_content or + set_content method of a Message object is called, + it calls the corresponding method of this object, + passing it the message object as its first argument, + and any arguments or keywords that were passed to + it as additional arguments. + """ refold_source = 'long' header_factory = HeaderRegistry() + content_manager = ObjectManager() def __init__(self, **kw): # Ensure that each new instance gets a unique header factory diff --git a/Lib/test/test_email/__init__.py b/Lib/test/test_email/__init__.py index 284de05..df1ae9b 100644 --- a/Lib/test/test_email/__init__.py +++ b/Lib/test/test_email/__init__.py @@ -127,6 +127,7 @@ def parameterize(cls): """ paramdicts = {} + testers = [] for name, attr in cls.__dict__.items(): if name.endswith('_params'): if not hasattr(attr, 'keys'): @@ -138,9 +139,11 @@ def parameterize(cls): d[n] = x attr = d paramdicts[name[:-7] + '_as_'] = attr + if '_as_' in name: + testers.append(name.split('_as_')[0] + '_as_') testfuncs = {} for name, attr in cls.__dict__.items(): - for paramsname, paramsdict in paramdicts.items(): + for paramsname, paramsdict in list(paramdicts.items()): if name.startswith(paramsname): testnameroot = 'test_' + name[len(paramsname):] for paramname, params in paramsdict.items(): @@ -149,6 +152,14 @@ def parameterize(cls): testname = testnameroot + '_' + paramname test.__name__ = testname testfuncs[testname] = test + del paramdicts[paramsname] + testers.remove(paramsname) + if paramdicts: + raise ValueError("No tester found for {}".format( + ', '.join(paramdicts.keys()))) + if testers: + raise ValueError("No params found for {}".format( + ', '.join(testers))) for key, value in testfuncs.items(): setattr(cls, key, value) return cls diff --git a/Lib/test/test_email/test_contentmanager.py b/Lib/test/test_email/test_contentmanager.py new file mode 100644 index 0000000..444be18 --- /dev/null +++ b/Lib/test/test_email/test_contentmanager.py @@ -0,0 +1,81 @@ +import unittest +from test.test_email import TestEmailBase, parameterize +from email.contentmanager import ContentManager +from email import policy +from email.message import MIMEMessage + + +@parameterize +class TestContentManager(TestEmailBase): + + policy = policy.default + message = MIMEMessage + + get_key_params = { + 'full_type': ('text/plain',), + 'maintype_only': ('text',), + 'null_key': ('',), + } + + def get_key_as_get_content_key(self, key): + def foo_getter(msg, foo=None): + bar = msg['X-Bar-Header'] + return foo, bar + cm = ContentManager() + cm.add_get_handler(key, foo_getter) + m = self.message() + m['Content-Type'] = 'text/plain' + m['X-Bar-Header'] = 'foo' + self.assertEqual(cm.get_content(m, foo='bar'), ('bar', 'foo')) + + def test_get_content_raises_if_unknown_mimetype_and_no_default(self): + cm = ContentManager() + m = self.message() + m['Content-Type'] = 'text/plain' + with self.assertRaisesRegex(KeyError, 'text/plain'): + cm.get_content(m) + + class BaseMessage(str): + pass + baseobject_full_path = __name__ + '.' + 'TestContentManager.BaseMessage' + class Message(BaseMessage): + pass + testobject_full_path = __name__ + '.' + 'TestContentManager.Message' + + set_key_params = { + 'type': (Message,), + 'full_path': (testobject_full_path,), + 'qualname': ('TestContentManager.Message',), + 'name': ('Message',), + 'base_type': (BaseMessage,), + 'base_full_path': (baseobject_full_path,), + 'base_qualname': ('TestContentManager.BaseMessage',), + 'base_name': ('BaseMessage',), + 'str_type': (str,), + 'str_full_path': ('builtins.str',), + 'str_name': ('str',), # str name and qualname are the same + 'null_key': (None,), + } + + def set_key_as_set_content_key(self, key): + def foo_setter(msg, obj, foo=None): + msg['X-Foo-Header'] = foo + msg.set_payload(obj) + cm = ContentManager() + cm.add_set_handler(key, foo_setter) + m = self.message() + msg_obj = self.Message() + cm.set_content(m, msg_obj, foo='bar') + self.assertEqual(m['X-Foo-Header'], 'bar') + self.assertEqual(m.get_payload(), msg_obj) + + def test_set_content_raises_if_unknown_type_and_no_default(self): + cm = ContentManager() + m = self.message() + msg_obj = self.Message() + with self.assertRaisesRegex(KeyError, self.testobject_full_path): + cm.set_content(m, msg_obj) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index 354f01d..2727695 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -259,6 +259,46 @@ class TestMIMEMessage(TestEmailBase): --===-- """)), + 'message_rfc822': ( + (None, None, None), + (), + (), + textwrap.dedent("""\ + To: foo@example.com + MIME-Version: 1.0 + Content-Type: message/rfc822 + + To: bar@example.com + From: robot@examp.com + + this is a message body. + """)), + + 'mixed_text_message_rfc822': ( + (None, None, 1), + (2,), + (1, 2), + textwrap.dedent("""\ + To: foo@example.com + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="===" + + --=== + Content-Type: text/plain + + Your message has bounced, ser. + + --=== + Content-Type: message/rfc822 + + To: bar@example.com + From: robot@examp.com + + this is a message body. + + --===-- + """)), + } def message_as_body_source(self, body_parts, attachments, parts, msg): @@ -303,6 +343,215 @@ class TestMIMEMessage(TestEmailBase): parts = [allparts[n] for n in parts] self.assertEqual(list(m.iter_parts()), parts) + class _TestContentManager: + def get_content(self, msg, *args, **kw): + return msg, args, kw + def set_content(self, msg, *args, **kw): + self.msg = msg + self.args = args + self.kw = kw + + def test_get_content_with_cm(self): + m = self._str_msg('') + cm = self._TestContentManager() + self.assertEqual(m.get_content(content_manager=cm), (m, (), {})) + msg, args, kw = m.get_content('foo', content_manager=cm, bar=1, k=2) + self.assertEqual(msg, m) + self.assertEqual(args, ('foo',)) + self.assertEqual(kw, dict(bar=1, k=2)) + + def test_get_content_default_cm_comes_from_policy(self): + p = policy.default.clone(content_manager=self._TestContentManager()) + m = self._str_msg('', policy=p) + self.assertEqual(m.get_content(), (m, (), {})) + msg, args, kw = m.get_content('foo', bar=1, k=2) + self.assertEqual(msg, m) + self.assertEqual(args, ('foo',)) + self.assertEqual(kw, dict(bar=1, k=2)) + + def test_set_content_with_cm(self): + m = self._str_msg('') + cm = self._TestContentManager() + m.set_content(content_manager=cm) + self.assertEqual(cm.msg, m) + self.assertEqual(cm.args, ()) + self.assertEqual(cm.kw, {}) + m.set_content('foo', content_manager=cm, bar=1, k=2) + self.assertEqual(cm.msg, m) + self.assertEqual(cm.args, ('foo',)) + self.assertEqual(cm.kw, dict(bar=1, k=2)) + + def test_set_content_default_cm_comes_from_policy(self): + cm = self._TestContentManager() + p = policy.default.clone(content_manager=cm) + m = self._str_msg('', policy=p) + m.set_content() + self.assertEqual(cm.msg, m) + self.assertEqual(cm.args, ()) + self.assertEqual(cm.kw, {}) + m.set_content('foo', bar=1, k=2) + self.assertEqual(cm.msg, m) + self.assertEqual(cm.args, ('foo',)) + self.assertEqual(cm.kw, dict(bar=1, k=2)) + + # Method should raise ValueError error when called on multipart/subtype. + subtype_params = ( + ('related', 'plain', 'succeeds'), + ('related', 'related', ''), + ('related', 'alternative', 'raises'), + ('related', 'mixed', 'raises'), + ('alternative', 'plain', 'succeeds'), + ('alternative', 'related', 'succeeds'), + ('alternative', 'alternative', ''), + ('alternative', 'mixed', 'raises'), + ('mixed', 'plain', 'succeeds'), + ('mixed', 'related', 'succeeds'), + ('mixed', 'alternative', 'succeeds'), + ('mixed', 'mixed', ''), + ) + + def subtype_as_make(self, method, subtype, outcome): + m = self.message() + + if outcome in ('', 'raises'): + m['Content-Type'] = 'multipart/' + subtype + with self.assertRaises(ValueError) as cm: + getattr(m, 'make_' + method)() + exc_text = str(cm.exception) + self.assertIn(subtype, exc_text) + self.assertIn(method, exc_text) + return + + msg_headers = ( + ('To', 'foo@bar.com'), + ('From', 'bar@foo.com'), + ('X-Random-Header', 'Corwin'), + ) + if subtype == 'text': + maintype = 'text' + payload = '' + else: + maintype = 'multipart' + payload = [] + maintype = 'text' if subtype=='plain' else 'multipart' + part_headers = ( + ('Content-Type', '/'.join([maintype, subtype])), + ('X-Trump', 'Random'), + ) + for name, value in msg_headers + part_headers: + m[name] = value + m.set_payload(payload) + getattr(m, 'make_' + method)() + self.assertEqual(m.get_content_maintype(), 'multipart') + self.assertEqual(len(m.get_payload()), 1) + self.assertEqual(m.get_content_subtype(), method) + for name, value in msg_headers: + self.assertEqual(m[name], value) + self.assertEqual(len(m), len(msg_headers)+1) # +1 for new Content-Type + part = next(m.iter_parts()) + self.assertEqual(len(part), len(part_headers)) + for name, value in part_headers: + self.assertEqual(part[name], value) + self.assertEqual(part.get_payload(), payload) + + def make_subtype_as_with_boundary(self, method, subtype, outcome): + # Doing all variation is a bit of overkill... + m = self.message() + if outcome in ('', 'raises'): + m['Content-Type'] = 'multipart/' + subtype + with self.assertRaises(ValueError) as cm: + getattr(m, 'make_' + method)() + return + m['Content-Type'] = ('text/plain' if subtype == 'plain' + else 'multipart/' + subtype) + getattr(m, 'make_' + method)(boundary="abc") + self.assertTrue(m.is_multipart()) + self.assertEqual(len(m.get_payload()), 1) + self.assertEqual(m.get_boundary(), 'abc') + + def test_policy_on_part_made_by_make_comes_from_message(self): + for method in ('make_related', 'make_alternative', 'make_mixed'): + m = self.message(policy=self.policy.clone(content_manager='foo')) + getattr(m, method)() + self.assertEqual(m.get_payload(0).policy.content_manager, 'foo') + + class _TestSetContentManager: + def set_content(self, msg, content, *args, **kw): + msg['Content-Type'] = 'text/plain' + msg.set_payload(content) + + def subtype_as_add(self, method, subtype, outcome): + cm = self._TestSetContentManager() + m = self.message() + add_method = 'add_attachment' if method=='mixed' else 'add_' + method + + if outcome == 'raises': + m['Content-Type'] = 'multipart/' + subtype + with self.assertRaises(ValueError) as ar: + getattr(m, add_method)() + exc_text = str(ar.exception) + self.assertIn(subtype, exc_text) + self.assertIn(method, exc_text) + return + + msg_headers = ( + ('To', 'foo@bar.com'), + ('From', 'bar@foo.com'), + ('X-Random-Header', 'Corwin'), + ) + if subtype == 'text': + maintype = 'text' + payload = '' + else: + maintype = 'multipart' + payload = [] + maintype = 'text' if subtype=='plain' else 'multipart' + part_headers = ( + ('Content-Type', '/'.join([maintype, subtype])), + ('X-Trump', 'Random'), + ) + for name, value in msg_headers + part_headers: + m[name] = value + m.set_payload(payload) + getattr(m, add_method)('test', content_manager=cm) + self.assertEqual(m.get_content_maintype(), 'multipart') + self.assertEqual(m.get_content_subtype(), method) + + if method == subtype: + self.assertEqual(len(m.get_payload()), 1) + for name, value in msg_headers + part_headers: + self.assertEqual(m[name], value) + part = next(m.iter_parts()) + self.assertEqual(part.get_content_type(), 'text/plain') + self.assertEqual(part.get_payload(), 'test') + return + + self.assertEqual(len(m.get_payload()), 2) + for name, value in msg_headers: + self.assertEqual(m[name], value) + self.assertEqual(len(m), len(msg_headers)+1) # +1 for new Content-Type + parts = m.iter_parts() + part = next(parts) + self.assertEqual(len(part), len(part_headers)) + for name, value in part_headers: + self.assertEqual(part[name], value) + self.assertEqual(part.get_payload(), payload) + new_part = next(parts) + self.assertEqual(new_part.get_content_type(), 'text/plain') + self.assertEqual(new_part.get_payload(), 'test') + + class _TestSetRaisingContentManager: + def set_content(self, msg, content, *args, **kw): + raise Exception('test') + + def test_default_content_manager_for_add_comes_from_policy(self): + cm = self._TestSetRaisingContentManager() + m = self.message(policy=self.policy.clone(content_manager=cm)) + for method in ('add_related', 'add_alternative', 'add_attachment'): + with self.assertRaises(Exception) as ar: + getattr(m, method)('') + self.assertEqual(str(ar.exception), 'test') + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py index 983bd49..06ad5f2 100644 --- a/Lib/test/test_email/test_policy.py +++ b/Lib/test/test_email/test_policy.py @@ -30,6 +30,7 @@ class PolicyAPITests(unittest.TestCase): 'raise_on_defect': False, 'header_factory': email.policy.EmailPolicy.header_factory, 'refold_source': 'long', + 'content_manager': email.policy.EmailPolicy.content_manager, }) # For each policy under test, we give here what we expect the defaults to