1
0
mirror of https://github.com/django/django.git synced 2025-06-03 18:49:12 +00:00

Refs #35537 -- Improved documentation and test coverage for email attachments and alternatives.

This commit is contained in:
Jake Howard 2024-06-27 12:01:19 +01:00 committed by Sarah Boyce
parent 5424151f96
commit d5bebc1c26
5 changed files with 128 additions and 12 deletions

View File

@ -11,6 +11,8 @@ from django.conf import settings
from django.core.mail.message import ( from django.core.mail.message import (
DEFAULT_ATTACHMENT_MIME_TYPE, DEFAULT_ATTACHMENT_MIME_TYPE,
BadHeaderError, BadHeaderError,
EmailAlternative,
EmailAttachment,
EmailMessage, EmailMessage,
EmailMultiAlternatives, EmailMultiAlternatives,
SafeMIMEMultipart, SafeMIMEMultipart,
@ -37,6 +39,8 @@ __all__ = [
"send_mass_mail", "send_mass_mail",
"mail_admins", "mail_admins",
"mail_managers", "mail_managers",
"EmailAlternative",
"EmailAttachment",
] ]

View File

@ -191,7 +191,7 @@ class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
MIMEMultipart.__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"]) EmailAttachment = namedtuple("Attachment", ["filename", "content", "mimetype"])
@ -477,14 +477,14 @@ class EmailMultiAlternatives(EmailMessage):
reply_to, reply_to,
) )
self.alternatives = [ self.alternatives = [
Alternative(*alternative) for alternative in (alternatives or []) EmailAlternative(*alternative) for alternative in (alternatives or [])
] ]
def attach_alternative(self, content, mimetype): def attach_alternative(self, content, mimetype):
"""Attach an alternative content representation.""" """Attach an alternative content representation."""
if content is None or mimetype is None: if content is None or mimetype is None:
raise ValueError("Both content and mimetype must be provided.") 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): def _create_message(self, msg):
return self._create_attachments(self._create_alternatives(msg)) return self._create_attachments(self._create_alternatives(msg))

View File

@ -284,7 +284,9 @@ PostgreSQL 14 and higher.
Miscellaneous 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: .. _deprecated-features-5.2:

View File

@ -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. new connection is created when ``send()`` is called.
* ``attachments``: A list of attachments to put on the message. These can * ``attachments``: A list of attachments to put on the message. These can
be either :class:`~email.mime.base.MIMEBase` instances, or a named tuple be instances of :class:`~email.mime.base.MIMEBase` or
with attributes ``(filename, content, mimetype)``. :class:`~django.core.mail.EmailAttachment`, or a tuple with attributes
``(filename, content, mimetype)``.
.. versionchanged:: 5.2 .. versionchanged:: 5.2
In older versions, tuple items of ``attachments`` were regular tuples, Support for :class:`~django.core.mail.EmailAttachment` items of
as opposed to named tuples. ``attachments`` were added.
* ``headers``: A dictionary of extra headers to put on the message. The * ``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 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 For MIME types starting with :mimetype:`text/`, binary data is handled as in
``attach()``. ``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 Sending alternative content types
--------------------------------- ---------------------------------
@ -404,20 +417,21 @@ Django's email library, you can do this using the
.. attribute:: alternatives .. attribute:: alternatives
A list of named tuples with attributes ``(content, mimetype)``. This is A list of :class:`~django.core.mail.EmailAlternative` named tuples. This
particularly useful in tests:: is particularly useful in tests::
self.assertEqual(len(msg.alternatives), 1) self.assertEqual(len(msg.alternatives), 1)
self.assertEqual(msg.alternatives[0].content, html_content) self.assertEqual(msg.alternatives[0].content, html_content)
self.assertEqual(msg.alternatives[0].mimetype, "text/html") self.assertEqual(msg.alternatives[0].mimetype, "text/html")
Alternatives should only be added using the :meth:`attach_alternative` Alternatives should only be added using the :meth:`attach_alternative`
method. method, or passed to the constructor.
.. versionchanged:: 5.2 .. versionchanged:: 5.2
In older versions, ``alternatives`` was a list of regular tuples, 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) .. 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("I am content"), True)
self.assertIs(msg.body_contains("<p>I am content.</p>"), False) 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 Updating the default content type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -17,6 +17,8 @@ from unittest import mock, skipUnless
from django.core import mail from django.core import mail
from django.core.mail import ( from django.core.mail import (
DNS_NAME, DNS_NAME,
EmailAlternative,
EmailAttachment,
EmailMessage, EmailMessage,
EmailMultiAlternatives, EmailMultiAlternatives,
mail_admins, mail_admins,
@ -557,12 +559,50 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
mime_type = "text/html" mime_type = "text/html"
msg.attach_alternative(html_content, mime_type) 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][0], html_content)
self.assertEqual(msg.alternatives[0].content, html_content) self.assertEqual(msg.alternatives[0].content, html_content)
self.assertEqual(msg.alternatives[0][1], mime_type) self.assertEqual(msg.alternatives[0][1], mime_type)
self.assertEqual(msg.alternatives[0].mimetype, 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): def test_none_body(self):
msg = EmailMessage("subject", None, "from@example.com", ["to@example.com"]) msg = EmailMessage("subject", None, "from@example.com", ["to@example.com"])
self.assertEqual(msg.body, "") 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][2], mime_type)
self.assertEqual(msg.attachments[0].mimetype, 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): def test_decoded_attachments(self):
"""Regression test for #9367""" """Regression test for #9367"""
headers = {"Date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"} headers = {"Date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"}