From aba0e541caaa086f183197eaaca0ac20a730bbe4 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 9 Jun 2024 09:09:07 +0100 Subject: [PATCH] Fixed #35537 -- Changed EmailMessage.attachments and EmailMultiAlternatives.alternatives to use namedtuples. This makes it more descriptive to pull out the named fields. --- django/core/mail/message.py | 19 +++++++++++---- docs/releases/5.2.txt | 10 +++++++- docs/topics/email.txt | 35 +++++++++++++++++++++++----- tests/logging_tests/tests.py | 2 +- tests/mail/tests.py | 32 +++++++++++++++++++++++-- tests/view_tests/tests/test_debug.py | 4 ++-- 6 files changed, 86 insertions(+), 16 deletions(-) diff --git a/django/core/mail/message.py b/django/core/mail/message.py index 205c680561..7eee5da8b8 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -1,4 +1,5 @@ import mimetypes +from collections import namedtuple from email import charset as Charset from email import encoders as Encoders from email import generator, message_from_string @@ -190,6 +191,10 @@ class SafeMIMEMultipart(MIMEMixin, MIMEMultipart): MIMEMultipart.__setitem__(self, name, val) +Alternative = namedtuple("Alternative", ["content", "mimetype"]) +EmailAttachment = namedtuple("Attachment", ["filename", "content", "mimetype"]) + + class EmailMessage: """A container for email information.""" @@ -338,7 +343,7 @@ class EmailMessage: # actually binary, read() raises a UnicodeDecodeError. mimetype = DEFAULT_ATTACHMENT_MIME_TYPE - self.attachments.append((filename, content, mimetype)) + self.attachments.append(EmailAttachment(filename, content, mimetype)) def attach_file(self, path, mimetype=None): """ @@ -471,13 +476,15 @@ class EmailMultiAlternatives(EmailMessage): cc, reply_to, ) - self.alternatives = alternatives or [] + self.alternatives = [ + Alternative(*alternative) for alternative in (alternatives or []) + ] def attach_alternative(self, content, mimetype): """Attach an alternative content representation.""" if content is None or mimetype is None: raise ValueError("Both content and mimetype must be provided.") - self.alternatives.append((content, mimetype)) + self.alternatives.append(Alternative(content, mimetype)) def _create_message(self, msg): return self._create_attachments(self._create_alternatives(msg)) @@ -492,5 +499,9 @@ class EmailMultiAlternatives(EmailMessage): if self.body: msg.attach(body_msg) for alternative in self.alternatives: - msg.attach(self._create_mime_attachment(*alternative)) + msg.attach( + self._create_mime_attachment( + alternative.content, alternative.mimetype + ) + ) return msg diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index e0f190076a..61101ce1fd 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -133,7 +133,15 @@ Decorators Email ~~~~~ -* ... +* Tuple items of :class:`EmailMessage.attachments + ` and + :class:`EmailMultiAlternatives.attachments + ` are now named tuples, as opposed + to regular tuples. + +* :attr:`EmailMultiAlternatives.alternatives + ` is now a list of + named tuples, as opposed to regular tuples. Error Reporting ~~~~~~~~~~~~~~~ diff --git a/docs/topics/email.txt b/docs/topics/email.txt index 9b7b404ec1..1a283bdbb4 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -282,8 +282,13 @@ All parameters are optional and can be set at any time prior to calling the new connection is created when ``send()`` is called. * ``attachments``: A list of attachments to put on the message. These can - be either :class:`~email.mime.base.MIMEBase` instances, or ``(filename, - content, mimetype)`` triples. + be either :class:`~email.mime.base.MIMEBase` instances, or a named tuple + with attributes ``(filename, content, mimetype)``. + + .. versionchanged:: 5.2 + + In older versions, tuple items of ``attachments`` were regular tuples, + as opposed to named tuples. * ``headers``: A dictionary of extra headers to put on the message. The keys are the header name, values are the header values. It's up to the @@ -392,10 +397,10 @@ Django's email library, you can do this using the .. class:: EmailMultiAlternatives - A subclass of :class:`~django.core.mail.EmailMessage` that has an - additional ``attach_alternative()`` method for including extra versions of - the message body in the email. All the other methods (including the class - initialization) are inherited directly from + A subclass of :class:`~django.core.mail.EmailMessage` that allows + additional versions of the message body in the email via the + ``attach_alternative()`` method. This directly inherits all methods + (including the class initialization) from :class:`~django.core.mail.EmailMessage`. .. method:: attach_alternative(content, mimetype) @@ -415,6 +420,24 @@ Django's email library, you can do this using the msg.attach_alternative(html_content, "text/html") msg.send() + .. attribute:: alternatives + + A list of named tuples with attributes ``(content, mimetype)``. This is + particularly useful in tests:: + + self.assertEqual(len(msg.alternatives), 1) + self.assertEqual(msg.alternatives[0].content, html_content) + self.assertEqual(msg.alternatives[0].mimetype, "text/html") + + Alternatives should only be added using the + :meth:`~django.core.mail.EmailMultiAlternatives.attach_alternative` + method. + + .. versionchanged:: 5.2 + + In older versions, ``alternatives`` was a list of regular tuples, as opposed + to named tuples. + Updating the default content type ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py index 610bdc1124..e58109fb78 100644 --- a/tests/logging_tests/tests.py +++ b/tests/logging_tests/tests.py @@ -467,7 +467,7 @@ class AdminEmailHandlerTest(SimpleTestCase): msg = mail.outbox[0] self.assertEqual(msg.subject, "[Django] ERROR: message") self.assertEqual(len(msg.alternatives), 1) - body_html = str(msg.alternatives[0][0]) + body_html = str(msg.alternatives[0].content) self.assertIn('
', body_html) self.assertNotIn("