瀏覽代碼

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

Jake Howard 9 月之前
父節點
當前提交
d5bebc1c26
共有 5 個文件被更改,包括 128 次插入12 次删除
  1. 4 0
      django/core/mail/__init__.py
  2. 3 3
      django/core/mail/message.py
  3. 3 1
      docs/releases/5.2.txt
  4. 33 8
      docs/topics/email.txt
  5. 85 0
      tests/mail/tests.py

+ 4 - 0
django/core/mail/__init__.py

@@ -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",
 ]
 ]
 
 
 
 

+ 3 - 3
django/core/mail/message.py

@@ -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))

+ 3 - 1
docs/releases/5.2.txt

@@ -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:
 
 

+ 33 - 8
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.
   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
-  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
   .. 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
 * ``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
-        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(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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 

+ 85 - 0
tests/mail/tests.py

@@ -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].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][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_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"}