summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Howard <git@theorangeone.net>2024-06-27 12:01:19 +0100
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-08-05 09:21:44 +0200
commitd5bebc1c26d4c0ec9eaa057aefc5b38649c0ba3b (patch)
treeb9ce1c33f1936f41cbebd189448ea617b48e408a
parent5424151f96252e1289e9a6f7eb842cd1dc87850a (diff)
Refs #35537 -- Improved documentation and test coverage for email attachments and alternatives.
-rw-r--r--django/core/mail/__init__.py4
-rw-r--r--django/core/mail/message.py6
-rw-r--r--docs/releases/5.2.txt4
-rw-r--r--docs/topics/email.txt41
-rw-r--r--tests/mail/tests.py85
5 files changed, 128 insertions, 12 deletions
diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py
index 676326697b..b179736b15 100644
--- a/django/core/mail/__init__.py
+++ b/django/core/mail/__init__.py
@@ -11,6 +11,8 @@ from django.conf import settings
from django.core.mail.message import (
DEFAULT_ATTACHMENT_MIME_TYPE,
BadHeaderError,
+ EmailAlternative,
+ EmailAttachment,
EmailMessage,
EmailMultiAlternatives,
SafeMIMEMultipart,
@@ -37,6 +39,8 @@ __all__ = [
"send_mass_mail",
"mail_admins",
"mail_managers",
+ "EmailAlternative",
+ "EmailAttachment",
]
diff --git a/django/core/mail/message.py b/django/core/mail/message.py
index eb467de429..e6d0ec2dc8 100644
--- a/django/core/mail/message.py
+++ b/django/core/mail/message.py
@@ -191,7 +191,7 @@ class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
MIMEMultipart.__setitem__(self, name, val)
-Alternative = namedtuple("Alternative", ["content", "mimetype"])
+EmailAlternative = namedtuple("Alternative", ["content", "mimetype"])
EmailAttachment = namedtuple("Attachment", ["filename", "content", "mimetype"])
@@ -477,14 +477,14 @@ class EmailMultiAlternatives(EmailMessage):
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))
diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt
index a64bc3bd00..92bb501d61 100644
--- a/docs/releases/5.2.txt
+++ b/docs/releases/5.2.txt
@@ -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:
diff --git a/docs/topics/email.txt b/docs/topics/email.txt
index e5d4f277f5..b991de4d78 100644
--- a/docs/topics/email.txt
+++ b/docs/topics/email.txt
@@ -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
@@ -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
---------------------------------
@@ -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)
@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/mail/tests.py b/tests/mail/tests.py
index a0d28eb0ce..6280bfa5c8 100644
--- a/tests/mail/tests.py
+++ b/tests/mail/tests.py
@@ -17,6 +17,8 @@ from unittest import mock, skipUnless
from django.core import mail
from django.core.mail import (
DNS_NAME,
+ EmailAlternative,
+ EmailAttachment,
EmailMessage,
EmailMultiAlternatives,
mail_admins,
@@ -557,12 +559,50 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
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, "from@example.com", ["to@example.com"])
self.assertEqual(msg.body, "")
@@ -654,6 +694,51 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
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"}