summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorMike Edmunds <medmunds@gmail.com>2026-03-19 11:22:57 -0700
committerJacob Walls <jacobtylerwalls@gmail.com>2026-04-11 08:54:07 -0400
commita8ef1a697bbe7b74141cd74c26d15ed35dd7817f (patch)
tree66db50ebda8c67b653cbfca0cb3418e06edb51c3 /tests
parent36ccdcc511e8ca5dc20e881fb9a3a310a8f85871 (diff)
Refs #36953 -- Split apart catchall MailTests.
Replaced large MailTests class with smaller classes focused on specific django.core.mail APIs: - EmailMessageTests: covering EmailMessage and EmailMultiAlternatives classes (the bulk of the former MailTests cases). - SendMailTests, SendMassMailTests, MailAdminsAndManagersTests: covering the function-based mail APIs. - GetConnectionTests: covering get_connection(). - DeprecatedInternalsTests: covering deprecated internal methods used in deprecated functionality. - DummyBackendTests: covering the dummy EmailBackend. In the process, moved the two cases from MailTimeZoneTests into the new EmailMessageTests, as they related to EmailMessage Date headers.
Diffstat (limited to 'tests')
-rw-r--r--tests/mail/tests.py698
1 files changed, 365 insertions, 333 deletions
diff --git a/tests/mail/tests.py b/tests/mail/tests.py
index 68d41dc216..902215a3a2 100644
--- a/tests/mail/tests.py
+++ b/tests/mail/tests.py
@@ -235,9 +235,9 @@ class MailTestsMixin:
return "".join(structure)
-class MailTests(MailTestsMixin, SimpleTestCase):
+class EmailMessageTests(MailTestsMixin, SimpleTestCase):
"""
- Non-backend specific tests.
+ Tests for django.core.mail.EmailMessage and EmailMultiAlternative.
"""
def test_ascii(self):
@@ -250,44 +250,6 @@ class MailTests(MailTestsMixin, SimpleTestCase):
self.assertEqual(message["From"], "from@example.com")
self.assertEqual(message["To"], "to@example.com")
- # RemovedInDjango70Warning.
- @ignore_warnings(category=RemovedInDjango70Warning)
- @mock.patch("django.core.mail.message.MIMEText.set_payload")
- def test_nonascii_as_string_with_ascii_charset(self, mock_set_payload):
- """Line length check should encode the payload supporting
- `surrogateescape`.
-
- Following https://github.com/python/cpython/issues/76511, newer
- versions of Python (3.12.3 and 3.13+) ensure that a message's
- payload is encoded with the provided charset and `surrogateescape` is
- used as the error handling strategy.
-
- This test is heavily based on the test from the fix for the bug above.
- Line length checks in SafeMIMEText's set_payload should also use the
- same error handling strategy to avoid errors such as:
-
- UnicodeEncodeError: 'utf-8' codec can't encode <...>: surrogates not
- allowed
-
- """
- # This test is specific to Python's legacy MIMEText. This can be safely
- # removed when EmailMessage.message() uses Python's modern email API.
- # (Using surrogateescape for non-utf8 is covered in test_encoding().)
- from django.core.mail import SafeMIMEText
-
- def simplified_set_payload(instance, payload, charset):
- instance._payload = payload
-
- mock_set_payload.side_effect = simplified_set_payload
-
- text = (
- "Text heavily based in Python's text for non-ascii messages: Föö bär"
- ).encode("iso-8859-1")
- body = text.decode("ascii", errors="surrogateescape")
- message = SafeMIMEText(body, "plain", "ascii")
- mock_set_payload.assert_called_once()
- self.assertEqual(message.get_payload(decode=True), text)
-
def test_multiple_recipients(self):
email = EmailMessage(
"Subject",
@@ -529,6 +491,31 @@ class MailTests(MailTestsMixin, SimpleTestCase):
# Not the default ISO format from force_str(strings_only=False).
self.assertNotEqual(message["Date"], "2001-11-09 01:08:47+00:00")
+ @requires_tz_support
+ @override_settings(
+ EMAIL_USE_LOCALTIME=False, USE_TZ=True, TIME_ZONE="Africa/Algiers"
+ )
+ def test_date_header_utc(self):
+ """
+ EMAIL_USE_LOCALTIME=False creates a datetime in UTC.
+ """
+ email = EmailMessage()
+ # Per RFC 2822/5322 section 3.3, "The form '+0000' SHOULD be used
+ # to indicate a time zone at Universal Time."
+ self.assertEndsWith(email.message()["Date"], "+0000")
+
+ @requires_tz_support
+ @override_settings(
+ EMAIL_USE_LOCALTIME=True, USE_TZ=True, TIME_ZONE="Africa/Algiers"
+ )
+ def test_date_header_localtime(self):
+ """
+ EMAIL_USE_LOCALTIME=True creates a datetime in the local time zone.
+ """
+ email = EmailMessage()
+ # Africa/Algiers is UTC+1 year round.
+ self.assertEndsWith(email.message()["Date"], "+0100")
+
def test_from_header(self):
"""
Make sure we can manually set the From header (#9214)
@@ -1271,114 +1258,6 @@ class MailTests(MailTestsMixin, SimpleTestCase):
with self.assertRaisesMessage(ValueError, msg):
email_msg.attach("file.txt", mimetype="application/pdf")
- def test_dummy_backend(self):
- """
- Make sure that dummy backends returns correct number of sent messages
- """
- connection = dummy.EmailBackend()
- email = EmailMessage(to=["to@example.com"])
- self.assertEqual(connection.send_messages([email, email, email]), 3)
-
- def test_arbitrary_keyword(self):
- """
- Make sure that get_connection() accepts arbitrary keyword that might be
- used with custom backends.
- """
- c = mail.get_connection(fail_silently=True, foo="bar")
- self.assertTrue(c.fail_silently)
-
- def test_custom_backend(self):
- """Test custom backend defined in this suite."""
- conn = mail.get_connection("mail.custombackend.EmailBackend")
- self.assertTrue(hasattr(conn, "test_outbox"))
- email = EmailMessage(to=["to@example.com"])
- conn.send_messages([email])
- self.assertEqual(len(conn.test_outbox), 1)
-
- def test_backend_arg(self):
- """Test backend argument of mail.get_connection()"""
- cases = [
- ("django.core.mail.backends.smtp.EmailBackend", smtp.EmailBackend),
- ("django.core.mail.backends.locmem.EmailBackend", locmem.EmailBackend),
- ("django.core.mail.backends.dummy.EmailBackend", dummy.EmailBackend),
- ("django.core.mail.backends.console.EmailBackend", console.EmailBackend),
- ]
- for backend_path, backend_class in cases:
- with self.subTest(backend_path=backend_path):
- self.assertIsInstance(
- mail.get_connection(backend_path),
- backend_class,
- )
-
- # The filebased EmailBackend requires a file_path arg.
- with (
- self.subTest(
- backend_path="django.core.mail.backends.filebased.EmailBackend"
- ),
- tempfile.TemporaryDirectory() as tmp_dir,
- ):
- self.assertIsInstance(
- mail.get_connection(
- "django.core.mail.backends.filebased.EmailBackend",
- file_path=tmp_dir,
- ),
- filebased.EmailBackend,
- )
-
- def test_get_connection_raises_error_from_backend_init(self):
- msg = " not object"
- with self.assertRaisesMessage(TypeError, msg):
- mail.get_connection(
- "django.core.mail.backends.filebased.EmailBackend", file_path=object()
- )
-
- def test_connection_arg_send_mail(self):
- # Send using non-default connection.
- connection = mail.get_connection("mail.custombackend.EmailBackend")
- send_mail(
- "Subject",
- "Content",
- "from@example.com",
- ["to@example.com"],
- connection=connection,
- )
- self.assertEqual(mail.outbox, [])
- self.assertEqual(len(connection.test_outbox), 1)
- self.assertEqual(connection.test_outbox[0].subject, "Subject")
-
- def test_connection_arg_send_mass_mail(self):
- # Send using non-default connection.
- connection = mail.get_connection("mail.custombackend.EmailBackend")
- send_mass_mail(
- [
- ("Subject1", "Content1", "from1@example.com", ["to1@example.com"]),
- ("Subject2", "Content2", "from2@example.com", ["to2@example.com"]),
- ],
- connection=connection,
- )
- self.assertEqual(mail.outbox, [])
- self.assertEqual(len(connection.test_outbox), 2)
- self.assertEqual(connection.test_outbox[0].subject, "Subject1")
- self.assertEqual(connection.test_outbox[1].subject, "Subject2")
-
- @override_settings(ADMINS=["nobody@example.com"])
- def test_connection_arg_mail_admins(self):
- # Send using non-default connection.
- connection = mail.get_connection("mail.custombackend.EmailBackend")
- mail_admins("Admin message", "Content", connection=connection)
- self.assertEqual(mail.outbox, [])
- self.assertEqual(len(connection.test_outbox), 1)
- self.assertEqual(connection.test_outbox[0].subject, "[Django] Admin message")
-
- @override_settings(MANAGERS=["nobody@example.com"])
- def test_connection_arg_mail_managers(self):
- # Send using non-default connection.
- connection = mail.get_connection("mail.custombackend.EmailBackend")
- mail_managers("Manager message", "Content", connection=connection)
- self.assertEqual(mail.outbox, [])
- self.assertEqual(len(connection.test_outbox), 1)
- self.assertEqual(connection.test_outbox[0].subject, "[Django] Manager message")
-
def test_dont_mangle_from_in_body(self):
# Regression for #13433 - Make sure that EmailMessage doesn't mangle
# 'From ' in message body.
@@ -1412,148 +1291,6 @@ class MailTests(MailTestsMixin, SimpleTestCase):
s = msg.message().as_bytes()
self.assertIn(b"Content-Transfer-Encoding: quoted-printable", s)
- # RemovedInDjango70Warning.
- @ignore_warnings(category=RemovedInDjango70Warning)
- def test_sanitize_address(self):
- """Email addresses are properly sanitized."""
- # Tests the internal sanitize_address() function. Many of these cases
- # are duplicated in test_address_header_handling(), which verifies
- # headers in the generated message.
- from django.core.mail.message import sanitize_address
-
- for email_address, encoding, expected_result in (
- # ASCII addresses.
- ("to@example.com", "ascii", "to@example.com"),
- ("to@example.com", "utf-8", "to@example.com"),
- (("A name", "to@example.com"), "ascii", "A name <to@example.com>"),
- (
- ("A name", "to@example.com"),
- "utf-8",
- "A name <to@example.com>",
- ),
- ("localpartonly", "ascii", "localpartonly"),
- # ASCII addresses with display names.
- ("A name <to@example.com>", "ascii", "A name <to@example.com>"),
- ("A name <to@example.com>", "utf-8", "A name <to@example.com>"),
- ('"A name" <to@example.com>', "ascii", "A name <to@example.com>"),
- ('"A name" <to@example.com>', "utf-8", "A name <to@example.com>"),
- # Unicode addresses: IDNA encoded domain supported per RFC-5890.
- ("to@éxample.com", "utf-8", "to@xn--xample-9ua.com"),
- # The next three cases should be removed when fixing #35713.
- # (An 'encoded-word' localpart is prohibited by RFC-2047, and not
- # supported by any known mail service.)
- ("tó@example.com", "utf-8", "=?utf-8?b?dMOz?=@example.com"),
- (
- ("Tó Example", "tó@example.com"),
- "utf-8",
- "=?utf-8?q?T=C3=B3_Example?= <=?utf-8?b?dMOz?=@example.com>",
- ),
- (
- "Tó Example <tó@example.com>",
- "utf-8",
- # (Not RFC-2047 compliant.)
- "=?utf-8?q?T=C3=B3_Example?= <=?utf-8?b?dMOz?=@example.com>",
- ),
- # IDNA addresses with display names.
- (
- "To Example <to@éxample.com>",
- "ascii",
- "To Example <to@xn--xample-9ua.com>",
- ),
- (
- "To Example <to@éxample.com>",
- "utf-8",
- "To Example <to@xn--xample-9ua.com>",
- ),
- # Addresses with two @ signs.
- ('"to@other.com"@example.com', "utf-8", r'"to@other.com"@example.com'),
- (
- '"to@other.com" <to@example.com>',
- "utf-8",
- '"to@other.com" <to@example.com>',
- ),
- (
- ("To Example", "to@other.com@example.com"),
- "utf-8",
- 'To Example <"to@other.com"@example.com>',
- ),
- # Addresses with long unicode display names.
- (
- "Tó Example very long" * 4 + " <to@example.com>",
- "utf-8",
- "=?utf-8?q?T=C3=B3_Example_very_longT=C3=B3_Example_very_longT"
- "=C3=B3_Example_?=\n"
- " =?utf-8?q?very_longT=C3=B3_Example_very_long?= "
- "<to@example.com>",
- ),
- (
- ("Tó Example very long" * 4, "to@example.com"),
- "utf-8",
- "=?utf-8?q?T=C3=B3_Example_very_longT=C3=B3_Example_very_longT"
- "=C3=B3_Example_?=\n"
- " =?utf-8?q?very_longT=C3=B3_Example_very_long?= "
- "<to@example.com>",
- ),
- # Address with long display name and unicode domain.
- (
- ("To Example very long" * 4, "to@exampl€.com"),
- "utf-8",
- "To Example very longTo Example very longTo Example very longT"
- "o Example very\n"
- " long <to@xn--exampl-nc1c.com>",
- ),
- ):
- with self.subTest(email_address=email_address, encoding=encoding):
- self.assertEqual(
- sanitize_address(email_address, encoding), expected_result
- )
-
- # RemovedInDjango70Warning.
- @ignore_warnings(category=RemovedInDjango70Warning)
- def test_sanitize_address_invalid(self):
- # Tests the internal sanitize_address() function. Note that Django's
- # EmailMessage.message() will not catch these cases, as it only calls
- # sanitize_address() if an address also includes non-ASCII chars.
- # Django detects these cases in the SMTP EmailBackend during sending.
- # See SMTPBackendTests.test_avoids_sending_to_invalid_addresses()
- # below.
- from django.core.mail.message import sanitize_address
-
- 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",
- ("", ""),
- ):
- with self.subTest(email_address=email_address):
- with self.assertRaisesMessage(ValueError, "Invalid address"):
- sanitize_address(email_address, encoding="utf-8")
-
- # RemovedInDjango70Warning.
- @ignore_warnings(category=RemovedInDjango70Warning)
- def test_sanitize_address_header_injection(self):
- # Tests the internal sanitize_address() function. These cases are
- # duplicated in test_address_header_handling(), which verifies headers
- # in the generated message.
- from django.core.mail.message import sanitize_address
-
- msg = "Invalid address; address parts cannot contain newlines."
- tests = [
- "Name\nInjection <to@example.com>",
- ("Name\nInjection", "to@xample.com"),
- "Name <to\ninjection@example.com>",
- ("Name", "to\ninjection@example.com"),
- ]
- for email_address in tests:
- with self.subTest(email_address=email_address):
- with self.assertRaisesMessage(ValueError, msg):
- sanitize_address(email_address, encoding="utf-8")
-
def test_address_header_handling(self):
# This verifies the modern email API's address header handling.
cases = [
@@ -1995,7 +1732,42 @@ class MailTests(MailTestsMixin, SimpleTestCase):
message.as_string(policy=policy.compat32),
)
- def test_send_mail_fail_silently_conflict(self):
+ def test_send_fail_silently_conflict(self):
+ email = mail.EmailMessage(
+ "Subject",
+ "Body",
+ "from@example.com",
+ ["to@example.com"],
+ connection=mail.get_connection(),
+ )
+ msg = (
+ "fail_silently cannot be used with a connection. "
+ "Pass fail_silently to get_connection() instead."
+ )
+ with self.assertRaisesMessage(TypeError, msg):
+ email.send(fail_silently=True)
+
+
+class SendMailTests(SimpleTestCase):
+ """
+ Tests for django.core.mail.send_mail().
+ """
+
+ def test_connection_arg(self):
+ # Send using non-default connection.
+ connection = mail.get_connection("mail.custombackend.EmailBackend")
+ send_mail(
+ "Subject",
+ "Content",
+ "from@example.com",
+ ["to@example.com"],
+ connection=connection,
+ )
+ self.assertEqual(mail.outbox, [])
+ self.assertEqual(len(connection.test_outbox), 1)
+ self.assertEqual(connection.test_outbox[0].subject, "Subject")
+
+ def test_fail_silently_conflict(self):
msg = (
"fail_silently cannot be used with a connection. "
"Pass fail_silently to get_connection() instead."
@@ -2010,7 +1782,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
connection=mail.get_connection(),
)
- def test_send_mail_auth_conflict(self):
+ def test_auth_conflict(self):
msg = (
"auth_user and auth_password cannot be used with a connection. "
"Pass auth_user and auth_password to get_connection() instead."
@@ -2029,22 +1801,28 @@ class MailTests(MailTestsMixin, SimpleTestCase):
connection=mail.get_connection(),
)
- def test_email_message_send_fail_silently_conflict(self):
- email = mail.EmailMessage(
- "Subject",
- "Body",
- "from@example.com",
- ["to@example.com"],
- connection=mail.get_connection(),
- )
- msg = (
- "fail_silently cannot be used with a connection. "
- "Pass fail_silently to get_connection() instead."
+
+class SendMassMailTests(SimpleTestCase):
+ """
+ Tests for django.core.mail.send_mass_mail().
+ """
+
+ def test_connection_arg(self):
+ # Send using non-default connection.
+ connection = mail.get_connection("mail.custombackend.EmailBackend")
+ send_mass_mail(
+ [
+ ("Subject1", "Content1", "from1@example.com", ["to1@example.com"]),
+ ("Subject2", "Content2", "from2@example.com", ["to2@example.com"]),
+ ],
+ connection=connection,
)
- with self.assertRaisesMessage(TypeError, msg):
- email.send(fail_silently=True)
+ self.assertEqual(mail.outbox, [])
+ self.assertEqual(len(connection.test_outbox), 2)
+ self.assertEqual(connection.test_outbox[0].subject, "Subject1")
+ self.assertEqual(connection.test_outbox[1].subject, "Subject2")
- def test_send_mass_mail_fail_silently_conflict(self):
+ def test_send_fail_silently_conflict(self):
datatuple = (("Subject", "Message", "from@example.com", ["to@example.com"]),)
msg = (
"fail_silently cannot be used with a connection. "
@@ -2055,7 +1833,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
datatuple, fail_silently=True, connection=mail.get_connection()
)
- def test_send_mass_mail_auth_conflict(self):
+ def test_send_auth_conflict(self):
datatuple = (("Subject", "Message", "from@example.com", ["to@example.com"]),)
msg = (
"auth_user and auth_password cannot be used with a connection. "
@@ -2070,6 +1848,30 @@ class MailTests(MailTestsMixin, SimpleTestCase):
datatuple, **{param: "value"}, connection=mail.get_connection()
)
+
+class MailAdminsAndManagersTests(SimpleTestCase):
+ """
+ Tests for django.core.mail.mail_admins() and mail_managers().
+ """
+
+ @override_settings(ADMINS=["nobody@example.com"])
+ def test_connection_arg_mail_admins(self):
+ # Send using non-default connection.
+ connection = mail.get_connection("mail.custombackend.EmailBackend")
+ mail_admins("Admin message", "Content", connection=connection)
+ self.assertEqual(mail.outbox, [])
+ self.assertEqual(len(connection.test_outbox), 1)
+ self.assertEqual(connection.test_outbox[0].subject, "[Django] Admin message")
+
+ @override_settings(MANAGERS=["nobody@example.com"])
+ def test_connection_arg_mail_managers(self):
+ # Send using non-default connection.
+ connection = mail.get_connection("mail.custombackend.EmailBackend")
+ mail_managers("Manager message", "Content", connection=connection)
+ self.assertEqual(mail.outbox, [])
+ self.assertEqual(len(connection.test_outbox), 1)
+ self.assertEqual(connection.test_outbox[0].subject, "[Django] Manager message")
+
def test_mail_admins_fail_silently_conflict(self):
msg = (
"fail_silently cannot be used with a connection. "
@@ -2097,6 +1899,244 @@ class MailTests(MailTestsMixin, SimpleTestCase):
)
+class GetConnectionTests(SimpleTestCase):
+ """
+ Tests for django.core.mail.get_connection().
+ """
+
+ def test_backend_arg(self):
+ """Test backend argument of mail.get_connection()"""
+ cases = [
+ ("django.core.mail.backends.smtp.EmailBackend", smtp.EmailBackend),
+ ("django.core.mail.backends.locmem.EmailBackend", locmem.EmailBackend),
+ ("django.core.mail.backends.dummy.EmailBackend", dummy.EmailBackend),
+ ("django.core.mail.backends.console.EmailBackend", console.EmailBackend),
+ ]
+ for backend_path, backend_class in cases:
+ with self.subTest(backend_path=backend_path):
+ self.assertIsInstance(
+ mail.get_connection(backend_path),
+ backend_class,
+ )
+
+ # The filebased EmailBackend requires a file_path arg.
+ with (
+ self.subTest(
+ backend_path="django.core.mail.backends.filebased.EmailBackend"
+ ),
+ tempfile.TemporaryDirectory() as tmp_dir,
+ ):
+ self.assertIsInstance(
+ mail.get_connection(
+ "django.core.mail.backends.filebased.EmailBackend",
+ file_path=tmp_dir,
+ ),
+ filebased.EmailBackend,
+ )
+
+ def test_custom_backend(self):
+ """Test custom backend defined in this suite."""
+ conn = mail.get_connection("mail.custombackend.EmailBackend")
+ self.assertTrue(hasattr(conn, "test_outbox"))
+ email = EmailMessage(to=["to@example.com"])
+ conn.send_messages([email])
+ self.assertEqual(len(conn.test_outbox), 1)
+
+ def test_raises_error_from_backend_init(self):
+ msg = " not object"
+ with self.assertRaisesMessage(TypeError, msg):
+ mail.get_connection(
+ "django.core.mail.backends.filebased.EmailBackend", file_path=object()
+ )
+
+ def test_arbitrary_keyword(self):
+ """
+ Make sure that get_connection() accepts arbitrary keyword that might be
+ used with custom backends.
+ """
+ c = mail.get_connection(fail_silently=True, foo="bar")
+ self.assertTrue(c.fail_silently)
+
+
+# RemovedInDjango70Warning.
+class DeprecatedInternalsTests(SimpleTestCase):
+ @ignore_warnings(category=RemovedInDjango70Warning)
+ @mock.patch("django.core.mail.message.MIMEText.set_payload")
+ def test_nonascii_as_string_with_ascii_charset(self, mock_set_payload):
+ """Line length check should encode the payload supporting
+ `surrogateescape`.
+
+ Following https://github.com/python/cpython/issues/76511, newer
+ versions of Python (3.12.3 and 3.13+) ensure that a message's
+ payload is encoded with the provided charset and `surrogateescape` is
+ used as the error handling strategy.
+
+ This test is heavily based on the test from the fix for the bug above.
+ Line length checks in SafeMIMEText's set_payload should also use the
+ same error handling strategy to avoid errors such as:
+
+ UnicodeEncodeError: 'utf-8' codec can't encode <...>: surrogates not
+ allowed
+
+ """
+ # This test is specific to Python's legacy MIMEText. This can be safely
+ # removed when EmailMessage.message() uses Python's modern email API.
+ # (Using surrogateescape for non-utf8 is covered in test_encoding().)
+ from django.core.mail import SafeMIMEText
+
+ def simplified_set_payload(instance, payload, charset):
+ instance._payload = payload
+
+ mock_set_payload.side_effect = simplified_set_payload
+
+ text = (
+ "Text heavily based in Python's text for non-ascii messages: Föö bär"
+ ).encode("iso-8859-1")
+ body = text.decode("ascii", errors="surrogateescape")
+ message = SafeMIMEText(body, "plain", "ascii")
+ mock_set_payload.assert_called_once()
+ self.assertEqual(message.get_payload(decode=True), text)
+
+ @ignore_warnings(category=RemovedInDjango70Warning)
+ def test_sanitize_address(self):
+ """Email addresses are properly sanitized."""
+ # Tests the internal sanitize_address() function. Many of these cases
+ # are duplicated in test_address_header_handling(), which verifies
+ # headers in the generated message.
+ from django.core.mail.message import sanitize_address
+
+ for email_address, encoding, expected_result in (
+ # ASCII addresses.
+ ("to@example.com", "ascii", "to@example.com"),
+ ("to@example.com", "utf-8", "to@example.com"),
+ (("A name", "to@example.com"), "ascii", "A name <to@example.com>"),
+ (
+ ("A name", "to@example.com"),
+ "utf-8",
+ "A name <to@example.com>",
+ ),
+ ("localpartonly", "ascii", "localpartonly"),
+ # ASCII addresses with display names.
+ ("A name <to@example.com>", "ascii", "A name <to@example.com>"),
+ ("A name <to@example.com>", "utf-8", "A name <to@example.com>"),
+ ('"A name" <to@example.com>', "ascii", "A name <to@example.com>"),
+ ('"A name" <to@example.com>', "utf-8", "A name <to@example.com>"),
+ # Unicode addresses: IDNA encoded domain supported per RFC-5890.
+ ("to@éxample.com", "utf-8", "to@xn--xample-9ua.com"),
+ # The next three cases should be removed when fixing #35713.
+ # (An 'encoded-word' localpart is prohibited by RFC-2047, and not
+ # supported by any known mail service.)
+ ("tó@example.com", "utf-8", "=?utf-8?b?dMOz?=@example.com"),
+ (
+ ("Tó Example", "tó@example.com"),
+ "utf-8",
+ "=?utf-8?q?T=C3=B3_Example?= <=?utf-8?b?dMOz?=@example.com>",
+ ),
+ (
+ "Tó Example <tó@example.com>",
+ "utf-8",
+ # (Not RFC-2047 compliant.)
+ "=?utf-8?q?T=C3=B3_Example?= <=?utf-8?b?dMOz?=@example.com>",
+ ),
+ # IDNA addresses with display names.
+ (
+ "To Example <to@éxample.com>",
+ "ascii",
+ "To Example <to@xn--xample-9ua.com>",
+ ),
+ (
+ "To Example <to@éxample.com>",
+ "utf-8",
+ "To Example <to@xn--xample-9ua.com>",
+ ),
+ # Addresses with two @ signs.
+ ('"to@other.com"@example.com', "utf-8", r'"to@other.com"@example.com'),
+ (
+ '"to@other.com" <to@example.com>',
+ "utf-8",
+ '"to@other.com" <to@example.com>',
+ ),
+ (
+ ("To Example", "to@other.com@example.com"),
+ "utf-8",
+ 'To Example <"to@other.com"@example.com>',
+ ),
+ # Addresses with long unicode display names.
+ (
+ "Tó Example very long" * 4 + " <to@example.com>",
+ "utf-8",
+ "=?utf-8?q?T=C3=B3_Example_very_longT=C3=B3_Example_very_longT"
+ "=C3=B3_Example_?=\n"
+ " =?utf-8?q?very_longT=C3=B3_Example_very_long?= "
+ "<to@example.com>",
+ ),
+ (
+ ("Tó Example very long" * 4, "to@example.com"),
+ "utf-8",
+ "=?utf-8?q?T=C3=B3_Example_very_longT=C3=B3_Example_very_longT"
+ "=C3=B3_Example_?=\n"
+ " =?utf-8?q?very_longT=C3=B3_Example_very_long?= "
+ "<to@example.com>",
+ ),
+ # Address with long display name and unicode domain.
+ (
+ ("To Example very long" * 4, "to@exampl€.com"),
+ "utf-8",
+ "To Example very longTo Example very longTo Example very longT"
+ "o Example very\n"
+ " long <to@xn--exampl-nc1c.com>",
+ ),
+ ):
+ with self.subTest(email_address=email_address, encoding=encoding):
+ self.assertEqual(
+ sanitize_address(email_address, encoding), expected_result
+ )
+
+ @ignore_warnings(category=RemovedInDjango70Warning)
+ def test_sanitize_address_invalid(self):
+ # Tests the internal sanitize_address() function. Note that Django's
+ # EmailMessage.message() will not catch these cases, as it only calls
+ # sanitize_address() if an address also includes non-ASCII chars.
+ # Django detects these cases in the SMTP EmailBackend during sending.
+ # See SMTPBackendTests.test_avoids_sending_to_invalid_addresses()
+ # below.
+ from django.core.mail.message import sanitize_address
+
+ 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",
+ ("", ""),
+ ):
+ with self.subTest(email_address=email_address):
+ with self.assertRaisesMessage(ValueError, "Invalid address"):
+ sanitize_address(email_address, encoding="utf-8")
+
+ @ignore_warnings(category=RemovedInDjango70Warning)
+ def test_sanitize_address_header_injection(self):
+ # Tests the internal sanitize_address() function. These cases are
+ # duplicated in test_address_header_handling(), which verifies headers
+ # in the generated message.
+ from django.core.mail.message import sanitize_address
+
+ msg = "Invalid address; address parts cannot contain newlines."
+ tests = [
+ "Name\nInjection <to@example.com>",
+ ("Name\nInjection", "to@xample.com"),
+ "Name <to\ninjection@example.com>",
+ ("Name", "to\ninjection@example.com"),
+ ]
+ for email_address in tests:
+ with self.subTest(email_address=email_address):
+ with self.assertRaisesMessage(ValueError, msg):
+ sanitize_address(email_address, encoding="utf-8")
+
+
# RemovedInDjango70Warning.
class MailDeprecatedPositionalArgsTests(SimpleTestCase):
@@ -2214,32 +2254,6 @@ class MailDeprecatedPositionalArgsTests(SimpleTestCase):
)
-@requires_tz_support
-class MailTimeZoneTests(MailTestsMixin, SimpleTestCase):
- @override_settings(
- EMAIL_USE_LOCALTIME=False, USE_TZ=True, TIME_ZONE="Africa/Algiers"
- )
- def test_date_header_utc(self):
- """
- EMAIL_USE_LOCALTIME=False creates a datetime in UTC.
- """
- email = EmailMessage()
- # Per RFC 2822/5322 section 3.3, "The form '+0000' SHOULD be used
- # to indicate a time zone at Universal Time."
- self.assertEndsWith(email.message()["Date"], "+0000")
-
- @override_settings(
- EMAIL_USE_LOCALTIME=True, USE_TZ=True, TIME_ZONE="Africa/Algiers"
- )
- def test_date_header_localtime(self):
- """
- EMAIL_USE_LOCALTIME=True creates a datetime in the local time zone.
- """
- email = EmailMessage()
- # Africa/Algiers is UTC+1 year round.
- self.assertEndsWith(email.message()["Date"], "+0100")
-
-
# RemovedInDjango70Warning.
class PythonGlobalState(SimpleTestCase):
"""
@@ -2665,6 +2679,24 @@ class BaseEmailBackendTests(MailTestsMixin):
self.assertTrue(closed[0])
+class DummyBackendTests(BaseEmailBackendTests, SimpleTestCase):
+ email_backend = "django.core.mail.backends.dummy.EmailBackend"
+
+ def get_mailbox_content(self):
+ # Shared tests that examine the content of sent messages are not
+ # meaningful: the dummy backend immediately discards sent messages,
+ # so it's not possible to retrieve them.
+ self.skipTest("Dummy backend discards sent messages")
+
+ def flush_mailbox(self):
+ pass
+
+ def test_send_messages_returns_sent_count(self):
+ connection = dummy.EmailBackend()
+ email = EmailMessage(to=["to@example.com"])
+ self.assertEqual(connection.send_messages([email, email, email]), 3)
+
+
class LocmemBackendTests(BaseEmailBackendTests, SimpleTestCase):
email_backend = "django.core.mail.backends.locmem.EmailBackend"