Skip to content

Commit

Permalink
Refs #35537 -- Improved documentation and test coverage for email att…
Browse files Browse the repository at this point in the history
…achments and alternatives.
  • Loading branch information
RealOrangeOne authored and sarahboyce committed Aug 5, 2024
1 parent 5424151 commit d5bebc1
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 12 deletions.
4 changes: 4 additions & 0 deletions django/core/mail/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from django.core.mail.message import (
DEFAULT_ATTACHMENT_MIME_TYPE,
BadHeaderError,
EmailAlternative,
EmailAttachment,
EmailMessage,
EmailMultiAlternatives,
SafeMIMEMultipart,
Expand All @@ -37,6 +39,8 @@
"send_mass_mail",
"mail_admins",
"mail_managers",
"EmailAlternative",
"EmailAttachment",
]


Expand Down
6 changes: 3 additions & 3 deletions django/core/mail/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def __setitem__(self, name, val):
MIMEMultipart.__setitem__(self, name, val)


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


Expand Down Expand Up @@ -477,14 +477,14 @@ def __init__(
reply_to,
)
self.alternatives = [
Alternative(*alternative) for alternative in (alternatives or [])
EmailAlternative(*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(Alternative(content, mimetype))
self.alternatives.append(EmailAlternative(content, mimetype))

def _create_message(self, msg):
return self._create_attachments(self._create_alternatives(msg))
Expand Down
4 changes: 3 additions & 1 deletion docs/releases/5.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,9 @@ PostgreSQL 14 and higher.
Miscellaneous
-------------

* ...
* :attr:`EmailMultiAlternatives.alternatives
<django.core.mail.EmailMultiAlternatives.alternatives>` should only be added
to using :meth:`~django.core.mail.EmailMultiAlternatives.attach_alternative`.

.. _deprecated-features-5.2:

Expand Down
41 changes: 33 additions & 8 deletions docs/topics/email.txt
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,14 @@ 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 a named tuple
with attributes ``(filename, content, mimetype)``.
be instances of :class:`~email.mime.base.MIMEBase` or
:class:`~django.core.mail.EmailAttachment`, or a tuple with attributes
``(filename, content, mimetype)``.

.. versionchanged:: 5.2

In older versions, tuple items of ``attachments`` were regular tuples,
as opposed to named tuples.
Support for :class:`~django.core.mail.EmailAttachment` items of
``attachments`` were added.

* ``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 @@ -384,6 +385,18 @@ The class has the following methods:
For MIME types starting with :mimetype:`text/`, binary data is handled as in
``attach()``.

.. class:: EmailAttachment

.. versionadded:: 5.2

A named tuple to store attachments to an email.

The named tuple has the following indexes:

* ``filename``
* ``content``
* ``mimetype``

Sending alternative content types
---------------------------------

Expand All @@ -404,20 +417,21 @@ Django's email library, you can do this using the

.. attribute:: alternatives

A list of named tuples with attributes ``(content, mimetype)``. This is
particularly useful in tests::
A list of :class:`~django.core.mail.EmailAlternative` named tuples. 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:`attach_alternative`
method.
method, or passed to the constructor.

.. versionchanged:: 5.2

In older versions, ``alternatives`` was a list of regular tuples,
as opposed to named tuples.
as opposed to :class:`~django.core.mail.EmailAlternative` named
tuples.

.. method:: attach_alternative(content, mimetype)

Expand Down Expand Up @@ -456,6 +470,17 @@ Django's email library, you can do this using the
self.assertIs(msg.body_contains("I am content"), True)
self.assertIs(msg.body_contains("<p>I am content.</p>"), False)

.. class:: EmailAlternative

.. versionadded:: 5.2

A named tuple to store alternative versions of email content.

The named tuple has the following indexes:

* ``content``
* ``mimetype``

Updating the default content type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
85 changes: 85 additions & 0 deletions tests/mail/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from django.core import mail
from django.core.mail import (
DNS_NAME,
EmailAlternative,
EmailAttachment,
EmailMessage,
EmailMultiAlternatives,
mail_admins,
Expand Down Expand Up @@ -557,12 +559,50 @@ def test_alternatives(self):
mime_type = "text/html"
msg.attach_alternative(html_content, mime_type)

self.assertIsInstance(msg.alternatives[0], EmailAlternative)

self.assertEqual(msg.alternatives[0][0], html_content)
self.assertEqual(msg.alternatives[0].content, html_content)

self.assertEqual(msg.alternatives[0][1], mime_type)
self.assertEqual(msg.alternatives[0].mimetype, mime_type)

self.assertIn(html_content, msg.message().as_string())

def test_alternatives_constructor(self):
html_content = "<p>This is <strong>html</strong></p>"
mime_type = "text/html"

msg = EmailMultiAlternatives(
alternatives=[EmailAlternative(html_content, mime_type)]
)

self.assertIsInstance(msg.alternatives[0], EmailAlternative)

self.assertEqual(msg.alternatives[0][0], html_content)
self.assertEqual(msg.alternatives[0].content, html_content)

self.assertEqual(msg.alternatives[0][1], mime_type)
self.assertEqual(msg.alternatives[0].mimetype, mime_type)

self.assertIn(html_content, msg.message().as_string())

def test_alternatives_constructor_from_tuple(self):
html_content = "<p>This is <strong>html</strong></p>"
mime_type = "text/html"

msg = EmailMultiAlternatives(alternatives=[(html_content, mime_type)])

self.assertIsInstance(msg.alternatives[0], EmailAlternative)

self.assertEqual(msg.alternatives[0][0], html_content)
self.assertEqual(msg.alternatives[0].content, html_content)

self.assertEqual(msg.alternatives[0][1], mime_type)
self.assertEqual(msg.alternatives[0].mimetype, mime_type)

self.assertIn(html_content, msg.message().as_string())

def test_none_body(self):
msg = EmailMessage("subject", None, "[email protected]", ["[email protected]"])
self.assertEqual(msg.body, "")
Expand Down Expand Up @@ -654,6 +694,51 @@ def test_attachments(self):
self.assertEqual(msg.attachments[0][2], mime_type)
self.assertEqual(msg.attachments[0].mimetype, mime_type)

attachments = self.get_decoded_attachments(msg)
self.assertEqual(attachments[0], (file_name, file_content.encode(), mime_type))

def test_attachments_constructor(self):
file_name = "example.txt"
file_content = "Text file content"
mime_type = "text/plain"
msg = EmailMessage(
attachments=[EmailAttachment(file_name, file_content, mime_type)]
)

self.assertIsInstance(msg.attachments[0], EmailAttachment)

self.assertEqual(msg.attachments[0][0], file_name)
self.assertEqual(msg.attachments[0].filename, file_name)

self.assertEqual(msg.attachments[0][1], file_content)
self.assertEqual(msg.attachments[0].content, file_content)

self.assertEqual(msg.attachments[0][2], mime_type)
self.assertEqual(msg.attachments[0].mimetype, mime_type)

attachments = self.get_decoded_attachments(msg)
self.assertEqual(attachments[0], (file_name, file_content.encode(), mime_type))

def test_attachments_constructor_from_tuple(self):
file_name = "example.txt"
file_content = "Text file content"
mime_type = "text/plain"
msg = EmailMessage(attachments=[(file_name, file_content, mime_type)])

self.assertIsInstance(msg.attachments[0], EmailAttachment)

self.assertEqual(msg.attachments[0][0], file_name)
self.assertEqual(msg.attachments[0].filename, file_name)

self.assertEqual(msg.attachments[0][1], file_content)
self.assertEqual(msg.attachments[0].content, file_content)

self.assertEqual(msg.attachments[0][2], mime_type)
self.assertEqual(msg.attachments[0].mimetype, mime_type)

attachments = self.get_decoded_attachments(msg)
self.assertEqual(attachments[0], (file_name, file_content.encode(), mime_type))

def test_decoded_attachments(self):
"""Regression test for #9367"""
headers = {"Date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"}
Expand Down

0 comments on commit d5bebc1

Please sign in to comment.