mirror of
https://github.com/django/django.git
synced 2024-12-22 09:05:43 +00:00
Refs #35581 -- Added tests for email parameters, attachments, MIME structure, bcc header, encoding and sending.
This commit is contained in:
parent
5d7001b578
commit
ea34de3bd7
@ -7,11 +7,15 @@ import tempfile
|
|||||||
from email import charset, message_from_binary_file
|
from email import charset, message_from_binary_file
|
||||||
from email import message_from_bytes as _message_from_bytes
|
from email import message_from_bytes as _message_from_bytes
|
||||||
from email import policy
|
from email import policy
|
||||||
|
from email.message import EmailMessage as PyEmailMessage
|
||||||
|
from email.message import Message as PyMessage
|
||||||
|
from email.mime.image import MIMEImage
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from smtplib import SMTP, SMTPException
|
from smtplib import SMTP, SMTPException
|
||||||
from ssl import SSLError
|
from ssl import SSLError
|
||||||
|
from textwrap import dedent
|
||||||
from unittest import mock, skipUnless
|
from unittest import mock, skipUnless
|
||||||
|
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
@ -139,6 +143,26 @@ class MailTestsMixin:
|
|||||||
for attachment in self.get_raw_attachments(django_message)
|
for attachment in self.get_raw_attachments(django_message)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_message_structure(self, message, level=0):
|
||||||
|
"""
|
||||||
|
Return a multiline indented string representation
|
||||||
|
of the message's MIME content-type structure, e.g.:
|
||||||
|
|
||||||
|
multipart/mixed
|
||||||
|
multipart/alternative
|
||||||
|
text/plain
|
||||||
|
text/html
|
||||||
|
image/jpg
|
||||||
|
text/calendar
|
||||||
|
"""
|
||||||
|
# Adapted from email.iterators._structure().
|
||||||
|
indent = " " * (level * 4)
|
||||||
|
structure = [f"{indent}{message.get_content_type()}\n"]
|
||||||
|
if message.is_multipart():
|
||||||
|
for subpart in message.get_payload():
|
||||||
|
structure.append(self.get_message_structure(subpart, level + 1))
|
||||||
|
return "".join(structure)
|
||||||
|
|
||||||
|
|
||||||
class MailTests(MailTestsMixin, SimpleTestCase):
|
class MailTests(MailTestsMixin, SimpleTestCase):
|
||||||
"""
|
"""
|
||||||
@ -299,6 +323,20 @@ class MailTests(MailTestsMixin, SimpleTestCase):
|
|||||||
).message()
|
).message()
|
||||||
self.assertEqual(message.get_all("Cc"), ["foo@example.com"])
|
self.assertEqual(message.get_all("Cc"), ["foo@example.com"])
|
||||||
|
|
||||||
|
def test_bcc_not_in_headers(self):
|
||||||
|
"""
|
||||||
|
A bcc address should be in the recipients,
|
||||||
|
but not in the (visible) message headers.
|
||||||
|
"""
|
||||||
|
email = EmailMessage(
|
||||||
|
to=["to@example.com"],
|
||||||
|
bcc=["bcc@example.com"],
|
||||||
|
)
|
||||||
|
message = email.message()
|
||||||
|
self.assertNotIn("Bcc", message)
|
||||||
|
self.assertNotIn("bcc@example.com", message.as_string())
|
||||||
|
self.assertEqual(email.recipients(), ["to@example.com", "bcc@example.com"])
|
||||||
|
|
||||||
def test_reply_to(self):
|
def test_reply_to(self):
|
||||||
email = EmailMessage(
|
email = EmailMessage(
|
||||||
"Subject",
|
"Subject",
|
||||||
@ -875,6 +913,97 @@ class MailTests(MailTestsMixin, SimpleTestCase):
|
|||||||
self.assertEqual(content, b"\xff")
|
self.assertEqual(content, b"\xff")
|
||||||
self.assertEqual(mimetype, "application/octet-stream")
|
self.assertEqual(mimetype, "application/octet-stream")
|
||||||
|
|
||||||
|
def test_attach_mime_image(self):
|
||||||
|
"""
|
||||||
|
EmailMessage.attach() docs: "You can pass it
|
||||||
|
a single argument that is a MIMEBase instance."
|
||||||
|
"""
|
||||||
|
# This also verifies complex attachments with extra header fields.
|
||||||
|
email = EmailMessage()
|
||||||
|
image = MIMEImage(b"GIF89a...", "gif")
|
||||||
|
image["Content-Disposition"] = "inline"
|
||||||
|
image["Content-ID"] = "<content-id@example.org>"
|
||||||
|
email.attach(image)
|
||||||
|
|
||||||
|
attachments = self.get_raw_attachments(email)
|
||||||
|
self.assertEqual(len(attachments), 1)
|
||||||
|
image_att = attachments[0]
|
||||||
|
self.assertEqual(image_att.get_content_type(), "image/gif")
|
||||||
|
self.assertEqual(image_att.get_content_disposition(), "inline")
|
||||||
|
self.assertEqual(image_att["Content-ID"], "<content-id@example.org>")
|
||||||
|
self.assertEqual(image_att.get_content(), b"GIF89a...")
|
||||||
|
self.assertIsNone(image_att.get_filename())
|
||||||
|
|
||||||
|
def test_attach_mime_image_in_constructor(self):
|
||||||
|
image = MIMEImage(b"\x89PNG...", "png")
|
||||||
|
image["Content-Disposition"] = "attachment; filename=test.png"
|
||||||
|
email = EmailMessage(attachments=[image])
|
||||||
|
|
||||||
|
attachments = self.get_raw_attachments(email)
|
||||||
|
self.assertEqual(len(attachments), 1)
|
||||||
|
image_att = attachments[0]
|
||||||
|
self.assertEqual(image_att.get_content_type(), "image/png")
|
||||||
|
self.assertEqual(image_att.get_content(), b"\x89PNG...")
|
||||||
|
self.assertEqual(image_att.get_content_disposition(), "attachment")
|
||||||
|
self.assertEqual(image_att.get_filename(), "test.png")
|
||||||
|
|
||||||
|
def test_attach_rfc822_message(self):
|
||||||
|
"""
|
||||||
|
EmailMessage.attach() docs: "If you specify a mimetype of message/rfc822,
|
||||||
|
it will also accept django.core.mail.EmailMessage and email.message.Message."
|
||||||
|
"""
|
||||||
|
# django.core.mail.EmailMessage
|
||||||
|
django_email = EmailMessage("child subject", "child body")
|
||||||
|
# email.message.Message
|
||||||
|
py_message = PyMessage()
|
||||||
|
py_message["Subject"] = "child subject"
|
||||||
|
py_message.set_payload("child body")
|
||||||
|
# email.message.EmailMessage
|
||||||
|
py_email_message = PyEmailMessage()
|
||||||
|
py_email_message["Subject"] = "child subject"
|
||||||
|
py_email_message.set_content("child body")
|
||||||
|
|
||||||
|
cases = [
|
||||||
|
django_email,
|
||||||
|
py_message,
|
||||||
|
py_email_message,
|
||||||
|
# Should also allow message serialized as str or bytes.
|
||||||
|
py_message.as_string(),
|
||||||
|
py_message.as_bytes(),
|
||||||
|
]
|
||||||
|
|
||||||
|
for child_message in cases:
|
||||||
|
with self.subTest(child_type=child_message.__class__):
|
||||||
|
email = EmailMessage("parent message", "parent body")
|
||||||
|
email.attach(content=child_message, mimetype="message/rfc822")
|
||||||
|
self.assertEqual(len(email.attachments), 1)
|
||||||
|
self.assertIsInstance(email.attachments[0], EmailAttachment)
|
||||||
|
self.assertEqual(email.attachments[0].mimetype, "message/rfc822")
|
||||||
|
|
||||||
|
# Make sure it is serialized correctly: a message/rfc822 attachment
|
||||||
|
# whose "body" content (payload) is the "encapsulated" (child) message.
|
||||||
|
attachments = self.get_raw_attachments(email)
|
||||||
|
self.assertEqual(len(attachments), 1)
|
||||||
|
rfc822_attachment = attachments[0]
|
||||||
|
self.assertEqual(rfc822_attachment.get_content_type(), "message/rfc822")
|
||||||
|
|
||||||
|
attached_message = rfc822_attachment.get_content()
|
||||||
|
self.assertEqual(attached_message["Subject"], "child subject")
|
||||||
|
self.assertEqual(attached_message.get_content().rstrip(), "child body")
|
||||||
|
|
||||||
|
# Regression for #18967: Per RFC 2046 5.2.1, "No encoding other
|
||||||
|
# than '7bit', '8bit', or 'binary' is permitted for the body of
|
||||||
|
# a 'message/rfc822' entity." (Default CTE is "7bit".)
|
||||||
|
cte = rfc822_attachment.get("Content-Transfer-Encoding", "7bit")
|
||||||
|
self.assertIn(cte, ("7bit", "8bit", "binary"))
|
||||||
|
|
||||||
|
# Any properly declared CTE is allowed for the attached message itself
|
||||||
|
# (including quoted-printable or base64). For the plain ASCII content
|
||||||
|
# in this test, we'd expect 7bit.
|
||||||
|
child_cte = attached_message.get("Content-Transfer-Encoding", "7bit")
|
||||||
|
self.assertEqual(child_cte, "7bit")
|
||||||
|
self.assertEqual(attached_message.get_content_type(), "text/plain")
|
||||||
|
|
||||||
def test_attach_mimebase_prohibits_other_params(self):
|
def test_attach_mimebase_prohibits_other_params(self):
|
||||||
email_msg = EmailMessage()
|
email_msg = EmailMessage()
|
||||||
txt = MIMEText("content")
|
txt = MIMEText("content")
|
||||||
@ -1039,63 +1168,6 @@ class MailTests(MailTestsMixin, SimpleTestCase):
|
|||||||
s = msg.message().as_string()
|
s = msg.message().as_string()
|
||||||
self.assertIn("Content-Transfer-Encoding: 8bit", s)
|
self.assertIn("Content-Transfer-Encoding: 8bit", s)
|
||||||
|
|
||||||
def test_dont_base64_encode_message_rfc822(self):
|
|
||||||
# Ticket #18967
|
|
||||||
# Shouldn't use base64 encoding for a child EmailMessage attachment.
|
|
||||||
# Create a child message first
|
|
||||||
child_msg = EmailMessage(
|
|
||||||
"Child Subject",
|
|
||||||
"Some body of child message",
|
|
||||||
"bounce@example.com",
|
|
||||||
["to@example.com"],
|
|
||||||
headers={"From": "from@example.com"},
|
|
||||||
)
|
|
||||||
child_s = child_msg.message().as_string()
|
|
||||||
|
|
||||||
# Now create a parent
|
|
||||||
parent_msg = EmailMessage(
|
|
||||||
"Parent Subject",
|
|
||||||
"Some parent body",
|
|
||||||
"bounce@example.com",
|
|
||||||
["to@example.com"],
|
|
||||||
headers={"From": "from@example.com"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attach to parent as a string
|
|
||||||
parent_msg.attach(content=child_s, mimetype="message/rfc822")
|
|
||||||
parent_s = parent_msg.message().as_string()
|
|
||||||
|
|
||||||
# The child message header is not base64 encoded
|
|
||||||
self.assertIn("Child Subject", parent_s)
|
|
||||||
|
|
||||||
# Feature test: try attaching email.Message object directly to the mail.
|
|
||||||
parent_msg = EmailMessage(
|
|
||||||
"Parent Subject",
|
|
||||||
"Some parent body",
|
|
||||||
"bounce@example.com",
|
|
||||||
["to@example.com"],
|
|
||||||
headers={"From": "from@example.com"},
|
|
||||||
)
|
|
||||||
parent_msg.attach(content=child_msg.message(), mimetype="message/rfc822")
|
|
||||||
parent_s = parent_msg.message().as_string()
|
|
||||||
|
|
||||||
# The child message header is not base64 encoded
|
|
||||||
self.assertIn("Child Subject", parent_s)
|
|
||||||
|
|
||||||
# Feature test: try attaching Django's EmailMessage object directly to the mail.
|
|
||||||
parent_msg = EmailMessage(
|
|
||||||
"Parent Subject",
|
|
||||||
"Some parent body",
|
|
||||||
"bounce@example.com",
|
|
||||||
["to@example.com"],
|
|
||||||
headers={"From": "from@example.com"},
|
|
||||||
)
|
|
||||||
parent_msg.attach(content=child_msg, mimetype="message/rfc822")
|
|
||||||
parent_s = parent_msg.message().as_string()
|
|
||||||
|
|
||||||
# The child message header is not base64 encoded
|
|
||||||
self.assertIn("Child Subject", parent_s)
|
|
||||||
|
|
||||||
def test_custom_utf8_encoding(self):
|
def test_custom_utf8_encoding(self):
|
||||||
"""A UTF-8 charset with a custom body encoding is respected."""
|
"""A UTF-8 charset with a custom body encoding is respected."""
|
||||||
body = "Body with latin characters: àáä."
|
body = "Body with latin characters: àáä."
|
||||||
@ -1239,6 +1311,121 @@ class MailTests(MailTestsMixin, SimpleTestCase):
|
|||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
email_msg.attach_alternative("<p>content</p>", None)
|
email_msg.attach_alternative("<p>content</p>", None)
|
||||||
|
|
||||||
|
def test_mime_structure(self):
|
||||||
|
"""
|
||||||
|
Check generated messages have the expected MIME parts and nesting.
|
||||||
|
"""
|
||||||
|
html_body = EmailAlternative("<p>HTML</p>", "text/html")
|
||||||
|
image = EmailAttachment("image.gif", b"\x89PNG...", "image/png")
|
||||||
|
rfc822_attachment = EmailAttachment(
|
||||||
|
None, EmailMessage(body="text"), "message/rfc822"
|
||||||
|
)
|
||||||
|
cases = [
|
||||||
|
# name, email (EmailMessage or subclass), expected structure
|
||||||
|
(
|
||||||
|
"single body",
|
||||||
|
EmailMessage(body="text"),
|
||||||
|
"""
|
||||||
|
text/plain
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"single body with attachment",
|
||||||
|
EmailMessage(body="text", attachments=[image]),
|
||||||
|
"""
|
||||||
|
multipart/mixed
|
||||||
|
text/plain
|
||||||
|
image/png
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"alternative bodies",
|
||||||
|
EmailMultiAlternatives(body="text", alternatives=[html_body]),
|
||||||
|
"""
|
||||||
|
multipart/alternative
|
||||||
|
text/plain
|
||||||
|
text/html
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"alternative bodies with attachments",
|
||||||
|
EmailMultiAlternatives(
|
||||||
|
body="text", alternatives=[html_body], attachments=[image]
|
||||||
|
),
|
||||||
|
"""
|
||||||
|
multipart/mixed
|
||||||
|
multipart/alternative
|
||||||
|
text/plain
|
||||||
|
text/html
|
||||||
|
image/png
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"alternative bodies with rfc822 attachment",
|
||||||
|
EmailMultiAlternatives(
|
||||||
|
body="text",
|
||||||
|
alternatives=[html_body],
|
||||||
|
attachments=[rfc822_attachment],
|
||||||
|
),
|
||||||
|
"""
|
||||||
|
multipart/mixed
|
||||||
|
multipart/alternative
|
||||||
|
text/plain
|
||||||
|
text/html
|
||||||
|
message/rfc822
|
||||||
|
text/plain
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"attachment only",
|
||||||
|
EmailMessage(attachments=[image]),
|
||||||
|
# Avoid empty text/plain body.
|
||||||
|
"""
|
||||||
|
multipart/mixed
|
||||||
|
image/png
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"alternative only",
|
||||||
|
EmailMultiAlternatives(alternatives=[html_body]),
|
||||||
|
# Avoid empty text/plain body.
|
||||||
|
"""
|
||||||
|
multipart/alternative
|
||||||
|
text/html
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"alternative and attachment only",
|
||||||
|
EmailMultiAlternatives(alternatives=[html_body], attachments=[image]),
|
||||||
|
"""
|
||||||
|
multipart/mixed
|
||||||
|
multipart/alternative
|
||||||
|
text/html
|
||||||
|
image/png
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"empty EmailMessage",
|
||||||
|
EmailMessage(),
|
||||||
|
"""
|
||||||
|
text/plain
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"empty EmailMultiAlternatives",
|
||||||
|
EmailMultiAlternatives(),
|
||||||
|
"""
|
||||||
|
text/plain
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for name, email, expected in cases:
|
||||||
|
expected = dedent(expected).lstrip()
|
||||||
|
with self.subTest(name=name):
|
||||||
|
message = email.message()
|
||||||
|
structure = self.get_message_structure(message)
|
||||||
|
self.assertEqual(structure, expected)
|
||||||
|
|
||||||
def test_body_contains(self):
|
def test_body_contains(self):
|
||||||
email_msg = EmailMultiAlternatives()
|
email_msg = EmailMultiAlternatives()
|
||||||
email_msg.body = "I am content."
|
email_msg.body = "I am content."
|
||||||
@ -1257,6 +1444,114 @@ class MailTests(MailTestsMixin, SimpleTestCase):
|
|||||||
email_msg.attach_alternative(b"I am a song.", "audio/mpeg")
|
email_msg.attach_alternative(b"I am a song.", "audio/mpeg")
|
||||||
self.assertIs(email_msg.body_contains("I am content"), True)
|
self.assertIs(email_msg.body_contains("I am content"), True)
|
||||||
|
|
||||||
|
def test_all_params_optional(self):
|
||||||
|
"""
|
||||||
|
EmailMessage class docs: "All parameters are optional"
|
||||||
|
"""
|
||||||
|
email = EmailMessage()
|
||||||
|
self.assertIsInstance(email.message(), PyMessage) # force serialization.
|
||||||
|
|
||||||
|
email = EmailMultiAlternatives()
|
||||||
|
self.assertIsInstance(email.message(), PyMessage) # force serialization.
|
||||||
|
|
||||||
|
def test_positional_arguments_order(self):
|
||||||
|
"""
|
||||||
|
EmailMessage class docs: "… is initialized with the following parameters
|
||||||
|
(in the given order, if positional arguments are used)."
|
||||||
|
"""
|
||||||
|
connection = mail.get_connection()
|
||||||
|
email = EmailMessage(
|
||||||
|
# (If you need to insert/remove/reorder any params here,
|
||||||
|
# that indicates a breaking change to documented behavior.)
|
||||||
|
"subject",
|
||||||
|
"body",
|
||||||
|
"from@example.com",
|
||||||
|
["to@example.com"],
|
||||||
|
["bcc@example.com"],
|
||||||
|
connection,
|
||||||
|
[EmailAttachment("file.txt", "attachment", "text/plain")],
|
||||||
|
{"X-Header": "custom header"},
|
||||||
|
["cc@example.com"],
|
||||||
|
["reply-to@example.com"],
|
||||||
|
# (New options can be added below here, ideally as keyword-only args.)
|
||||||
|
)
|
||||||
|
|
||||||
|
message = email.message()
|
||||||
|
self.assertEqual(message.get_all("Subject"), ["subject"])
|
||||||
|
self.assertEqual(message.get_all("From"), ["from@example.com"])
|
||||||
|
self.assertEqual(message.get_all("To"), ["to@example.com"])
|
||||||
|
self.assertEqual(message.get_all("X-Header"), ["custom header"])
|
||||||
|
self.assertEqual(message.get_all("Cc"), ["cc@example.com"])
|
||||||
|
self.assertEqual(message.get_all("Reply-To"), ["reply-to@example.com"])
|
||||||
|
self.assertEqual(message.get_payload(0).get_payload(), "body")
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_decoded_attachments(email),
|
||||||
|
[("file.txt", "attachment", "text/plain")],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
email.recipients(), ["to@example.com", "cc@example.com", "bcc@example.com"]
|
||||||
|
)
|
||||||
|
self.assertIs(email.get_connection(), connection)
|
||||||
|
|
||||||
|
def test_all_params_can_be_set_before_send(self):
|
||||||
|
"""
|
||||||
|
EmailMessage class docs: "All parameters … can be set at any time
|
||||||
|
prior to calling the send() method."
|
||||||
|
"""
|
||||||
|
# This is meant to verify EmailMessage.__init__() doesn't apply any
|
||||||
|
# special processing that would be missing for properties set later.
|
||||||
|
original_connection = mail.get_connection(username="original")
|
||||||
|
new_connection = mail.get_connection(username="new")
|
||||||
|
email = EmailMessage(
|
||||||
|
"original subject",
|
||||||
|
"original body",
|
||||||
|
"original-from@example.com",
|
||||||
|
["original-to@example.com"],
|
||||||
|
["original-bcc@example.com"],
|
||||||
|
original_connection,
|
||||||
|
[EmailAttachment("original.txt", "original attachment", "text/plain")],
|
||||||
|
{"X-Header": "original header"},
|
||||||
|
["original-cc@example.com"],
|
||||||
|
["original-reply-to@example.com"],
|
||||||
|
)
|
||||||
|
email.subject = "new subject"
|
||||||
|
email.body = "new body"
|
||||||
|
email.from_email = "new-from@example.com"
|
||||||
|
email.to = ["new-to@example.com"]
|
||||||
|
email.bcc = ["new-bcc@example.com"]
|
||||||
|
email.connection = new_connection
|
||||||
|
email.attachments = [
|
||||||
|
("new1.txt", "new attachment 1", "text/plain"), # plain tuple.
|
||||||
|
EmailAttachment("new2.txt", "new attachment 2", "text/csv"),
|
||||||
|
MIMEImage(b"GIF89a...", "gif"),
|
||||||
|
]
|
||||||
|
email.extra_headers = {"X-Header": "new header"}
|
||||||
|
email.cc = ["new-cc@example.com"]
|
||||||
|
email.reply_to = ["new-reply-to@example.com"]
|
||||||
|
|
||||||
|
message = email.message()
|
||||||
|
self.assertEqual(message.get_all("Subject"), ["new subject"])
|
||||||
|
self.assertEqual(message.get_all("From"), ["new-from@example.com"])
|
||||||
|
self.assertEqual(message.get_all("To"), ["new-to@example.com"])
|
||||||
|
self.assertEqual(message.get_all("X-Header"), ["new header"])
|
||||||
|
self.assertEqual(message.get_all("Cc"), ["new-cc@example.com"])
|
||||||
|
self.assertEqual(message.get_all("Reply-To"), ["new-reply-to@example.com"])
|
||||||
|
self.assertEqual(message.get_payload(0).get_payload(), "new body")
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_decoded_attachments(email),
|
||||||
|
[
|
||||||
|
("new1.txt", "new attachment 1", "text/plain"),
|
||||||
|
("new2.txt", "new attachment 2", "text/csv"),
|
||||||
|
(None, b"GIF89a...", "image/gif"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
email.recipients(),
|
||||||
|
["new-to@example.com", "new-cc@example.com", "new-bcc@example.com"],
|
||||||
|
)
|
||||||
|
self.assertIs(email.get_connection(), new_connection)
|
||||||
|
self.assertNotIn("original", message.as_string())
|
||||||
|
|
||||||
|
|
||||||
@requires_tz_support
|
@requires_tz_support
|
||||||
class MailTimeZoneTests(MailTestsMixin, SimpleTestCase):
|
class MailTimeZoneTests(MailTestsMixin, SimpleTestCase):
|
||||||
@ -1806,6 +2101,7 @@ class ConsoleBackendTests(BaseEmailBackendTests, SimpleTestCase):
|
|||||||
class SMTPHandler:
|
class SMTPHandler:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.mailbox = []
|
self.mailbox = []
|
||||||
|
self.smtp_envelopes = []
|
||||||
|
|
||||||
async def handle_DATA(self, server, session, envelope):
|
async def handle_DATA(self, server, session, envelope):
|
||||||
data = envelope.content
|
data = envelope.content
|
||||||
@ -1820,10 +2116,17 @@ class SMTPHandler:
|
|||||||
if mail_from != header_from:
|
if mail_from != header_from:
|
||||||
return f"553 '{mail_from}' != '{header_from}'"
|
return f"553 '{mail_from}' != '{header_from}'"
|
||||||
self.mailbox.append(message)
|
self.mailbox.append(message)
|
||||||
|
self.smtp_envelopes.append(
|
||||||
|
{
|
||||||
|
"mail_from": envelope.mail_from,
|
||||||
|
"rcpt_tos": envelope.rcpt_tos,
|
||||||
|
}
|
||||||
|
)
|
||||||
return "250 OK"
|
return "250 OK"
|
||||||
|
|
||||||
def flush_mailbox(self):
|
def flush_mailbox(self):
|
||||||
self.mailbox[:] = []
|
self.mailbox[:] = []
|
||||||
|
self.smtp_envelopes[:] = []
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_AIOSMTPD, "No aiosmtpd library detected.")
|
@skipUnless(HAS_AIOSMTPD, "No aiosmtpd library detected.")
|
||||||
@ -1870,6 +2173,9 @@ class SMTPBackendTests(BaseEmailBackendTests, SMTPBackendTestsBase):
|
|||||||
def get_mailbox_content(self):
|
def get_mailbox_content(self):
|
||||||
return self.smtp_handler.mailbox
|
return self.smtp_handler.mailbox
|
||||||
|
|
||||||
|
def get_smtp_envelopes(self):
|
||||||
|
return self.smtp_handler.smtp_envelopes
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
EMAIL_HOST_USER="not empty username",
|
EMAIL_HOST_USER="not empty username",
|
||||||
EMAIL_HOST_PASSWORD="not empty password",
|
EMAIL_HOST_PASSWORD="not empty password",
|
||||||
@ -2094,6 +2400,85 @@ class SMTPBackendTests(BaseEmailBackendTests, SMTPBackendTestsBase):
|
|||||||
sent = backend.send_messages([email])
|
sent = backend.send_messages([email])
|
||||||
self.assertEqual(sent, 0)
|
self.assertEqual(sent, 0)
|
||||||
|
|
||||||
|
def test_avoids_sending_to_invalid_addresses(self):
|
||||||
|
"""
|
||||||
|
Verify invalid addresses can't sneak into SMTP commands through
|
||||||
|
EmailMessage.all_recipients() (which is distinct from message header fields).
|
||||||
|
"""
|
||||||
|
backend = smtp.EmailBackend()
|
||||||
|
backend.connection = mock.Mock()
|
||||||
|
for email_address in (
|
||||||
|
# Invalid address with two @ signs.
|
||||||
|
"to@other.com@example.com",
|
||||||
|
# Invalid address without the quotes.
|
||||||
|
"to@other.com <to@example.com>",
|
||||||
|
# Other invalid addresses.
|
||||||
|
"@",
|
||||||
|
"to@",
|
||||||
|
"@example.com",
|
||||||
|
# CR/NL in addr-spec. (SMTP strips display-name.)
|
||||||
|
'"evil@example.com\r\nto"@example.com',
|
||||||
|
"to\nevil@example.com",
|
||||||
|
):
|
||||||
|
with self.subTest(email_address=email_address):
|
||||||
|
# Use bcc (which is only processed by SMTP backend) to ensure
|
||||||
|
# error is coming from SMTP backend, not EmailMessage.message().
|
||||||
|
email = EmailMessage(bcc=[email_address])
|
||||||
|
with self.assertRaisesMessage(ValueError, "Invalid address"):
|
||||||
|
backend.send_messages([email])
|
||||||
|
|
||||||
|
def test_encodes_idna_in_smtp_commands(self):
|
||||||
|
"""
|
||||||
|
SMTP backend must encode non-ASCII domains for the SMTP envelope
|
||||||
|
(which can be distinct from the email headers).
|
||||||
|
"""
|
||||||
|
email = EmailMessage(
|
||||||
|
from_email="lists@discussão.example.org",
|
||||||
|
to=["To Example <to@漢字.example.com>"],
|
||||||
|
bcc=["monitor@discussão.example.org"],
|
||||||
|
headers={
|
||||||
|
"From": "Gestor de listas <lists@discussão.example.org>",
|
||||||
|
"To": "Discussão Django <django@discussão.example.org>",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
backend = smtp.EmailBackend()
|
||||||
|
backend.send_messages([email])
|
||||||
|
envelope = self.get_smtp_envelopes()[0]
|
||||||
|
self.assertEqual(envelope["mail_from"], "lists@xn--discusso-xza.example.org")
|
||||||
|
self.assertEqual(
|
||||||
|
envelope["rcpt_tos"],
|
||||||
|
["to@xn--p8s937b.example.com", "monitor@xn--discusso-xza.example.org"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_does_not_reencode_idna(self):
|
||||||
|
"""
|
||||||
|
SMTP backend should not downgrade IDNA 2008 to IDNA 2003.
|
||||||
|
|
||||||
|
Django does not currently handle IDNA 2008 encoding, but should retain
|
||||||
|
it for addresses that have been pre-encoded.
|
||||||
|
"""
|
||||||
|
# Test all four EmailMessage attrs accessed by the SMTP email backend.
|
||||||
|
# These are IDNA 2008 encoded domains that would be different
|
||||||
|
# in IDNA 2003, from https://www.unicode.org/reports/tr46/#Deviations.
|
||||||
|
email = EmailMessage(
|
||||||
|
from_email='"βόλος" <from@xn--fa-hia.example.com>',
|
||||||
|
to=['"faß" <to@xn--10cl1a0b660p.example.com>'],
|
||||||
|
cc=['"ශ්රී" <cc@xn--nxasmm1c.example.com>'],
|
||||||
|
bcc=['"نامهای." <bcc@xn--mgba3gch31f060k.example.com>'],
|
||||||
|
)
|
||||||
|
backend = smtp.EmailBackend()
|
||||||
|
backend.send_messages([email])
|
||||||
|
envelope = self.get_smtp_envelopes()[0]
|
||||||
|
self.assertEqual(envelope["mail_from"], "from@xn--fa-hia.example.com")
|
||||||
|
self.assertEqual(
|
||||||
|
envelope["rcpt_tos"],
|
||||||
|
[
|
||||||
|
"to@xn--10cl1a0b660p.example.com",
|
||||||
|
"cc@xn--nxasmm1c.example.com",
|
||||||
|
"bcc@xn--mgba3gch31f060k.example.com",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(HAS_AIOSMTPD, "No aiosmtpd library detected.")
|
@skipUnless(HAS_AIOSMTPD, "No aiosmtpd library detected.")
|
||||||
class SMTPBackendStoppedServerTests(SMTPBackendTestsBase):
|
class SMTPBackendStoppedServerTests(SMTPBackendTestsBase):
|
||||||
|
Loading…
Reference in New Issue
Block a user