Skip to content

Commit

Permalink
Use a namedtuple for email attachments and alternatives.
Browse files Browse the repository at this point in the history
This makes it more descriptive to pull out the named fields
  • Loading branch information
RealOrangeOne committed Jun 19, 2024
1 parent a0c44d4 commit d1005ff
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 14 deletions.
19 changes: 15 additions & 4 deletions django/core/mail/message.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -190,6 +191,10 @@ def __setitem__(self, name, val):
MIMEMultipart.__setitem__(self, name, val)


Alternative = namedtuple("Alternative", ["content", "mime_type"])
EmailAttachment = namedtuple("Attachment", ["filename", "content", "mime_type"])


class EmailMessage:
"""A container for email information."""

Expand Down Expand Up @@ -338,7 +343,7 @@ def attach(self, filename=None, content=None, mimetype=None):
# 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):
"""
Expand Down Expand Up @@ -471,13 +476,15 @@ def __init__(
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))
Expand All @@ -492,5 +499,9 @@ def _create_alternatives(self, msg):
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.mime_type
)
)
return msg
8 changes: 7 additions & 1 deletion docs/releases/5.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,13 @@ Decorators
Email
~~~~~

* ...
* Tuple items of ``attachments`` of :class:`~django.core.mail.EmailMessage`
and :class:`~django.core.mail.EmailMultiAlternatives` are now named tuples,
as opposed to regular tuples.

* :attr:`EmailMultiAlternatives.alternatives
<django.core.mail.EmailMultiAlternatives.alternatives>` is now a list of
named tuples, as opposed to regular tuples.

Error Reporting
~~~~~~~~~~~~~~~
Expand Down
32 changes: 26 additions & 6 deletions docs/topics/email.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -415,6 +420,21 @@ 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)

Alternatives should only be added using the ``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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion tests/logging_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ def test_emit_no_form_tag(self):
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('<div id="traceback">', body_html)
self.assertNotIn("<form", body_html)

Expand Down
4 changes: 2 additions & 2 deletions tests/view_tests/tests/test_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -1463,7 +1463,7 @@ def verify_unsafe_email(self, view, check_for_POST_params=True):
self.assertNotIn("worcestershire", body_plain)

# Frames vars are shown in html email reports.
body_html = str(email.alternatives[0][0])
body_html = str(email.alternatives[0].content)
self.assertIn("cooked_eggs", body_html)
self.assertIn("scrambled", body_html)
self.assertIn("sauce", body_html)
Expand Down Expand Up @@ -1499,7 +1499,7 @@ def verify_safe_email(self, view, check_for_POST_params=True):
self.assertNotIn("worcestershire", body_plain)

# Frames vars are shown in html email reports.
body_html = str(email.alternatives[0][0])
body_html = str(email.alternatives[0].content)
self.assertIn("cooked_eggs", body_html)
self.assertIn("scrambled", body_html)
self.assertIn("sauce", body_html)
Expand Down

0 comments on commit d1005ff

Please sign in to comment.