diff --git a/django/core/mail/backends/filebased.py b/django/core/mail/backends/filebased.py index 3b2b0371508f..07ca959a594b 100644 --- a/django/core/mail/backends/filebased.py +++ b/django/core/mail/backends/filebased.py @@ -15,6 +15,10 @@ def __init__(self, *args, file_path=None, **kwargs): self.file_path = file_path else: self.file_path = getattr(settings, "EMAIL_FILE_PATH", None) + if self.file_path is None: + raise ImproperlyConfigured( + "The EMAIL_FILE_PATH setting must be set to use the file EmailBackend." + ) self.file_path = os.path.abspath(self.file_path) try: os.makedirs(self.file_path, exist_ok=True) diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py index 983fe8c39e80..167722c6171a 100644 --- a/tests/auth_tests/test_forms.py +++ b/tests/auth_tests/test_forms.py @@ -4,6 +4,8 @@ import urllib.parse from unittest import mock +from mail.custombackend import FailingEmailBackend + from django import forms from django.contrib.auth.forms import ( AdminPasswordChangeForm, @@ -1382,6 +1384,7 @@ def test_save_html_email_template_name(self): @override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend") def test_save_send_email_exceptions_are_catched_and_logged(self): + self.addCleanup(FailingEmailBackend.reset) user, username, email = self.create_dummy_user() form = PasswordResetForm({"email": email}) self.assertTrue(form.is_valid()) diff --git a/tests/logging_tests/logconfig.py b/tests/logging_tests/logconfig.py index 35999989fbfc..4d9a73f1b4d3 100644 --- a/tests/logging_tests/logconfig.py +++ b/tests/logging_tests/logconfig.py @@ -1,7 +1,6 @@ import logging from django.conf import settings -from django.core.mail.backends.base import BaseEmailBackend from django.views.debug import ExceptionReporter @@ -11,11 +10,6 @@ def __init__(self): self.config = settings.LOGGING -class MyEmailBackend(BaseEmailBackend): - def send_messages(self, email_messages): - pass - - class CustomExceptionReporter(ExceptionReporter): def get_traceback_text(self): return "custom traceback text" diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py index 9690147e8180..d4cdbe7dd97a 100644 --- a/tests/logging_tests/tests.py +++ b/tests/logging_tests/tests.py @@ -4,6 +4,7 @@ from unittest import TestCase, mock from admin_scripts.tests import AdminScriptTestCase +from mail.custombackend import FailingEmailBackend, OptionsCapturingBackend from django.conf import settings from django.core import mail @@ -26,7 +27,6 @@ from django.views.debug import ExceptionReporter from . import views -from .logconfig import MyEmailBackend class LoggingFiltersTest(SimpleTestCase): @@ -286,9 +286,22 @@ def get_admin_email_handler(self, logger): h for h in logger.handlers if h.__class__.__name__ == "AdminEmailHandler" ][0] - def test_fail_silently(self): - admin_email_handler = self.get_admin_email_handler(self.logger) - self.assertTrue(admin_email_handler.connection().fail_silently) + def make_log_record(self, url_path=None, *args, **kwargs): + record = self.logger.makeRecord( + "name", logging.ERROR, "function", "lno", "message", None, None + ) + if url_path is not None: + record.request = self.request_factory.get(url_path, *args, **kwargs) + return record + + @override_settings( + ADMINS=["admin@example.com"], + EMAIL_BACKEND="mail.custombackend.FailingEmailBackend", + ) + def test_sends_using_fail_silently(self): + self.addCleanup(FailingEmailBackend.reset) + self.logger.error("All work and no play makes Jack a dull boy") + self.assertIs(FailingEmailBackend.init_kwargs[0]["fail_silently"], True) @override_settings( ADMINS=["admin@example.com"], @@ -383,36 +396,16 @@ def test_subject_accepts_newlines(self): self.assertNotIn("\r", mail.outbox[0].subject) self.assertEqual(mail.outbox[0].subject, expected_subject) - @override_settings( - ADMINS=["admin@example.com"], - DEBUG=False, - ) + @override_settings(ADMINS=["admin@example.com"]) def test_uses_custom_email_backend(self): - """ - Refs #19325 - """ - message = "All work and no play makes Jack a dull boy" - admin_email_handler = self.get_admin_email_handler(self.logger) - mail_admins_called = {"called": False} - - def my_mail_admins(*args, **kwargs): - connection = kwargs["connection"] - self.assertIsInstance(connection, MyEmailBackend) - mail_admins_called["called"] = True - - # Monkeypatches - orig_mail_admins = mail.mail_admins - orig_email_backend = admin_email_handler.email_backend - mail.mail_admins = my_mail_admins - admin_email_handler.email_backend = "logging_tests.logconfig.MyEmailBackend" - - try: - self.logger.error(message) - self.assertTrue(mail_admins_called["called"]) - finally: - # Revert Monkeypatches - mail.mail_admins = orig_mail_admins - admin_email_handler.email_backend = orig_email_backend + self.addCleanup(OptionsCapturingBackend.reset) + handler = AdminEmailHandler( + email_backend="mail.custombackend.OptionsCapturingBackend" + ) + handler.emit(self.make_log_record("/")) + self.assertEqual(len(mail.outbox), 0) + self.assertIs(OptionsCapturingBackend.init_kwargs[0]["fail_silently"], True) + self.assertEqual(len(OptionsCapturingBackend.sent_messages), 1) @override_settings( ADMINS=["admin@example.com"], @@ -423,12 +416,8 @@ def test_emit_non_ascii(self): request. """ handler = self.get_admin_email_handler(self.logger) - record = self.logger.makeRecord( - "name", logging.ERROR, "function", "lno", "message", None, None - ) url_path = "/º" - record.request = self.request_factory.get(url_path) - handler.emit(record) + handler.emit(self.make_log_record(url_path)) self.assertEqual(len(mail.outbox), 1) msg = mail.outbox[0] self.assertEqual(msg.to, ["admin@example.com"]) @@ -442,16 +431,10 @@ def test_emit_non_ascii(self): def test_customize_send_mail_method(self): class ManagerEmailHandler(AdminEmailHandler): def send_mail(self, subject, message, *args, **kwargs): - mail.mail_managers( - subject, message, *args, connection=self.connection(), **kwargs - ) + mail.mail_managers(subject, message, *args, **kwargs) handler = ManagerEmailHandler() - record = self.logger.makeRecord( - "name", logging.ERROR, "function", "lno", "message", None, None - ) - self.assertEqual(len(mail.outbox), 0) - handler.emit(record) + handler.emit(self.make_log_record()) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to, ["manager@example.com"]) @@ -480,14 +463,10 @@ def test_default_exception_reporter_class(self): @override_settings(ADMINS=["admin@example.com"]) def test_custom_exception_reporter_is_used(self): - record = self.logger.makeRecord( - "name", logging.ERROR, "function", "lno", "message", None, None - ) - record.request = self.request_factory.get("/") handler = AdminEmailHandler( reporter_class="logging_tests.logconfig.CustomExceptionReporter" ) - handler.emit(record) + handler.emit(self.make_log_record("/")) self.assertEqual(len(mail.outbox), 1) msg = mail.outbox[0] self.assertEqual(msg.body, "message\n\ncustom traceback text") @@ -496,16 +475,7 @@ def test_custom_exception_reporter_is_used(self): def test_emit_no_form_tag(self): """HTML email doesn't contain forms.""" handler = AdminEmailHandler(include_html=True) - record = self.logger.makeRecord( - "name", - logging.ERROR, - "function", - "lno", - "message", - None, - None, - ) - handler.emit(record) + handler.emit(self.make_log_record()) self.assertEqual(len(mail.outbox), 1) msg = mail.outbox[0] self.assertEqual(msg.subject, "[Django] ERROR: message") @@ -517,15 +487,7 @@ def test_emit_no_form_tag(self): @override_settings(ADMINS=[]) def test_emit_no_admins(self): handler = AdminEmailHandler() - record = self.logger.makeRecord( - "name", - logging.ERROR, - "function", - "lno", - "message", - None, - None, - ) + record = self.make_log_record() with mock.patch.object( handler, "format_subject", diff --git a/tests/mail/custombackend.py b/tests/mail/custombackend.py index c63f1c07b5bd..6d4f946a2b23 100644 --- a/tests/mail/custombackend.py +++ b/tests/mail/custombackend.py @@ -14,7 +14,53 @@ def send_messages(self, email_messages): return len(email_messages) -class FailingEmailBackend(BaseEmailBackend): +class OptionsCapturingBackend(BaseEmailBackend): + """Capture init kwargs and sent messages for use in test assertions. + + Test cases using this backend _must_ ensure reset() is called:: + + def test_something(self): + self.addCleanup(OptionsCapturingBackend.reset) + ... + + Failing to call reset() will cause unexpected behavior in other tests that + use the OptionsCapturingBackend. + """ + + init_kwargs = [] + sent_messages = [] + + @classmethod + def reset(cls): + cls.init_kwargs = [] + cls.sent_messages = [] + + def __init__(self, **kwargs): + self.init_kwargs.append(kwargs.copy()) + super().__init__(**kwargs) + + def send_messages(self, email_messages): + self.sent_messages.extend(email_messages) + return len(email_messages) + + +class FailingEmailBackend(OptionsCapturingBackend): + """Raise on send_messages(), or do nothing if fail_silently is set. + + Test cases using this backend _must_ ensure reset() is called:: + + def test_something(self): + self.addCleanup(FailingEmailBackend.reset) + ... + + Failing to call reset() will cause unexpected behavior in other tests that + use the FailingEmailBackend. + """ + + init_kwargs = [] + sent_messages = [] def send_messages(self, email_messages): + if self.fail_silently: + return 0 raise ValueError("FailingEmailBackend is doomed to fail.") diff --git a/tests/mail/test_backends.py b/tests/mail/test_backends.py index 1ac627bf6ab0..ba1f652b6d66 100644 --- a/tests/mail/test_backends.py +++ b/tests/mail/test_backends.py @@ -1,19 +1,22 @@ import os import shutil import socket +import ssl import sys import tempfile -from email import message_from_binary_file, policy from io import StringIO from pathlib import Path -from smtplib import SMTP, SMTPException +from smtplib import SMTPException from ssl import SSLError -from unittest import mock, skipUnless +from unittest import mock, skipIf, skipUnless from django.core import mail -from django.core.mail import EmailMessage, send_mail -from django.core.mail.backends import dummy, locmem, smtp +from django.core.exceptions import ImproperlyConfigured +from django.core.mail import EmailMessage +from django.core.mail.backends import dummy, filebased, locmem, smtp +from django.core.mail.backends.base import BaseEmailBackend from django.test import SimpleTestCase, override_settings +from django.utils.module_loading import import_string from .tests import MailTestsMixin, message_from_bytes @@ -25,10 +28,21 @@ HAS_AIOSMTPD = False -class BaseEmailBackendTests(MailTestsMixin): - """ - Shared test cases repeated for each EmailBackend. - """ +class BaseEmailBackendTests(SimpleTestCase): + def test_fail_silently_arg_accepted(self): + for value in [True, False]: + with self.subTest(fail_silently=value): + backend = BaseEmailBackend(fail_silently=value) + self.assertIs(backend.fail_silently, value) + + def test_unknown_kwargs_ignored(self): + backend = BaseEmailBackend(unknown_kwarg="foo") + self.assertIsInstance(backend, BaseEmailBackend) + self.assertFalse(hasattr(backend, "unknown_kwarg")) + + +class SharedEmailBackendTests(MailTestsMixin): + """Common test cases run against each EmailBackend.""" email_backend = None @@ -39,13 +53,14 @@ def setUpClass(cls): def get_mailbox_content(self): raise NotImplementedError( - "subclasses of BaseEmailBackendTests must provide a get_mailbox_content() " - "method" + "Subclasses of SharedEmailBackendTests must provide a " + "get_mailbox_content() method." ) def flush_mailbox(self): raise NotImplementedError( - "subclasses of BaseEmailBackendTests may require a flush_mailbox() method" + "Subclasses of SharedEmailBackendTests may require a " + "flush_mailbox() method." ) def get_the_message(self): @@ -98,38 +113,36 @@ def test_send_many(self): self.assertEqual(messages[1]["To"], "to-2@example.com") self.flush_mailbox() - def test_close_connection(self): - """ - Connection can be closed (even when not explicitly opened) - """ - conn = mail.get_connection(username="", password="") - conn.close() + def test_connection_can_be_closed_even_if_not_opened(self): + backend = mail.get_connection() + backend.close() - def test_use_as_contextmanager(self): - """ - The connection can be used as a contextmanager. - """ - opened = [False] - closed = [False] - conn = mail.get_connection(username="", password="") + def test_connection_can_be_used_as_contextmanager(self): + backend = mail.get_connection() + backend.open = mock.Mock() + backend.close = mock.Mock() - def open(): - opened[0] = True + with backend as backend_cm: + backend.open.assert_called_once() + self.assertIs(backend_cm, backend) + backend.close.assert_not_called() - conn.open = open + backend.close.assert_called_once() - def close(): - closed[0] = True + def test_fail_silently_arg_accepted(self): + backend_class = import_string(self.email_backend) + for value in [True, False]: + with self.subTest(fail_silently=value): + backend = backend_class(fail_silently=value) + self.assertIs(backend.fail_silently, value) - conn.close = close - with conn as same_conn: - self.assertTrue(opened[0]) - self.assertIs(same_conn, conn) - self.assertFalse(closed[0]) - self.assertTrue(closed[0]) + def test_unknown_kwargs_ignored(self): + backend_class = import_string(self.email_backend) + backend = backend_class(unknown_kwarg="foo") + self.assertFalse(hasattr(backend, "unknown_kwarg")) -class DummyBackendTests(BaseEmailBackendTests, SimpleTestCase): +class DummyBackendTests(SharedEmailBackendTests, SimpleTestCase): email_backend = "django.core.mail.backends.dummy.EmailBackend" def get_mailbox_content(self): @@ -142,12 +155,12 @@ def flush_mailbox(self): pass def test_send_messages_returns_sent_count(self): - connection = dummy.EmailBackend() + backend = dummy.EmailBackend() email = EmailMessage(to=["to@example.com"]) - self.assertEqual(connection.send_messages([email, email, email]), 3) + self.assertEqual(backend.send_messages([email, email, email]), 3) -class LocmemBackendTests(BaseEmailBackendTests, SimpleTestCase): +class LocmemBackendTests(SharedEmailBackendTests, SimpleTestCase): email_backend = "django.core.mail.backends.locmem.EmailBackend" def get_mailbox_content(self): @@ -162,109 +175,191 @@ def tearDown(self): def test_locmem_shared_messages(self): """ - Make sure that the locmen backend populates the outbox. + Make sure that the locmem backend populates the outbox. """ - connection = locmem.EmailBackend() - connection2 = locmem.EmailBackend() + backend1 = locmem.EmailBackend() + backend2 = locmem.EmailBackend() email = EmailMessage(to=["to@example.com"]) - connection.send_messages([email]) - connection2.send_messages([email]) + backend1.send_messages([email]) + backend2.send_messages([email]) self.assertEqual(len(mail.outbox), 2) def test_validate_multiline_headers(self): # Headers are validated when using the locmem backend (#18861). # (See also EmailMessageTests.test_header_injection().) + email = EmailMessage(subject="Subject\nMultiline", to=["to@example.com"]) + backend = locmem.EmailBackend() with self.assertRaises(ValueError): - send_mail( - "Subject\nMultiline", "Content", "from@example.com", ["to@example.com"] - ) + backend.send_messages([email]) def test_outbox_not_mutated_after_send(self): email = EmailMessage( subject="correct subject", to=["to@example.com"], ) - email.send() + backend = locmem.EmailBackend() + backend.send_messages([email]) email.subject = "other subject" email.to.append("other@example.com") self.assertEqual(mail.outbox[0].subject, "correct subject") self.assertEqual(mail.outbox[0].to, ["to@example.com"]) -class FileBackendTests(BaseEmailBackendTests, SimpleTestCase): +class FileBackendTests(SharedEmailBackendTests, SimpleTestCase): email_backend = "django.core.mail.backends.filebased.EmailBackend" def setUp(self): super().setUp() self.tmp_dir = self.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp_dir) _settings_override = override_settings(EMAIL_FILE_PATH=self.tmp_dir) _settings_override.enable() self.addCleanup(_settings_override.disable) def mkdtemp(self): - return tempfile.mkdtemp() + tmp_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, tmp_dir) + return tmp_dir + + def get_filenames(self): + return os.listdir(self.tmp_dir) + + def get_messages_from_filename(self, filename): + with open(os.path.join(self.tmp_dir, filename), "rb") as fp: + messages = fp.read().split(b"\n" + (b"-" * 79) + b"\n") + return [message_from_bytes(m) for m in messages if m] def flush_mailbox(self): - for filename in os.listdir(self.tmp_dir): + for filename in self.get_filenames(): os.unlink(os.path.join(self.tmp_dir, filename)) def get_mailbox_content(self): messages = [] - for filename in os.listdir(self.tmp_dir): - with open(os.path.join(self.tmp_dir, filename), "rb") as fp: - session = fp.read().split(b"\n" + (b"-" * 79) + b"\n") - messages.extend(message_from_bytes(m) for m in session if m) + for filename in self.get_filenames(): + messages.extend(self.get_messages_from_filename(filename)) return messages - def test_file_sessions(self): - """Make sure opening a connection creates a new file""" - msg = EmailMessage( - "Subject", - "Content", - "bounce@example.com", - ["to@example.com"], - headers={"From": "from@example.com"}, + def test_email_file_path_use_settings(self): + file_path_settings = self.mkdtemp() + with self.settings(EMAIL_FILE_PATH=file_path_settings): + backend = filebased.EmailBackend() + self.assertEqual(backend.file_path, str(file_path_settings)) + + def test_email_file_path_override_settings(self): + file_path_settings = self.mkdtemp() + file_path_override = self.mkdtemp() + self.assertNotEqual(file_path_settings, file_path_override) + + with self.settings(EMAIL_FILE_PATH=file_path_settings): + backend = filebased.EmailBackend(file_path=file_path_override) + self.assertEqual(backend.file_path, str(file_path_override)) + + def test_error_if_email_file_path_setting_not_defined(self): + msg = "The EMAIL_FILE_PATH setting must be set to use the file EmailBackend." + with ( + self.settings(EMAIL_FILE_PATH=None), + self.assertRaisesMessage(ImproperlyConfigured, msg), + ): + filebased.EmailBackend() + + def test_error_if_file_path_is_not_directory(self): + tmp_file = Path(self.tmp_dir) / "ordinary-file" + tmp_file.touch() + if isinstance(self.tmp_dir, str): + # Running the non-"PathLib" version of FileBackendTests. + tmp_file = str(tmp_file) + msg = ( + f"Path for saving email messages exists, but is not a directory: {tmp_file}" ) - connection = mail.get_connection() - connection.send_messages([msg]) + with self.assertRaisesMessage(ImproperlyConfigured, msg): + filebased.EmailBackend(file_path=tmp_file) + + @skipIf( + sys.platform == "win32", + "No cross-platform means to force an OSError from os.makedirs().", + ) + def test_error_if_file_path_cannot_be_created(self): + msg = "Could not create directory for saving email messages: /dev/null/foo" + with self.assertRaisesMessage(ImproperlyConfigured, msg): + filebased.EmailBackend(file_path="/dev/null/foo") + + @skipIf( + sys.platform == "win32", + "chmod does not reliably make directories read-only on Windows.", + ) + def test_error_if_file_path_is_not_writeable(self): + os.chmod(self.tmp_dir, 0o444) + self.addCleanup(os.chmod, self.tmp_dir, 0o777) + msg = f"Could not write to directory: {self.tmp_dir}" + with self.assertRaisesMessage(ImproperlyConfigured, msg): + filebased.EmailBackend(file_path=self.tmp_dir) + + def test_new_file_per_instance(self): + # Documented behavior: "A new file is created for each new session that + # is opened on this backend." + email = EmailMessage(to=["to@example.com"]) + self.assertEqual(len(self.get_filenames()), 0) + + backend1 = mail.get_connection() + backend1.send_messages([email]) + self.assertEqual(len(self.get_filenames()), 1) - self.assertEqual(len(os.listdir(self.tmp_dir)), 1) - with open(os.path.join(self.tmp_dir, os.listdir(self.tmp_dir)[0]), "rb") as fp: - message = message_from_binary_file(fp, policy=policy.default) - self.assertEqual(message.get_content_type(), "text/plain") - self.assertEqual(message.get("subject"), "Subject") - self.assertEqual(message.get("from"), "from@example.com") - self.assertEqual(message.get("to"), "to@example.com") + backend2 = mail.get_connection() + backend2.send_messages([email]) + self.assertEqual(len(self.get_filenames()), 2) - connection2 = mail.get_connection() - connection2.send_messages([msg]) - self.assertEqual(len(os.listdir(self.tmp_dir)), 2) + def test_multiple_messages_same_connection_single_file_reused(self): + self.assertEqual(len(self.get_filenames()), 0) + backend = mail.get_connection() - connection.send_messages([msg]) - self.assertEqual(len(os.listdir(self.tmp_dir)), 2) + self.assertIs(backend.open(), True) + backend.send_messages([EmailMessage(to=["one@example.com"])]) + filenames = self.get_filenames() + self.assertEqual(len(filenames), 1) - msg.connection = mail.get_connection() - self.assertTrue(connection.open()) - msg.send() - self.assertEqual(len(os.listdir(self.tmp_dir)), 3) - msg.send() - self.assertEqual(len(os.listdir(self.tmp_dir)), 3) + # Send a second message while connection is still open. + backend.send_messages([EmailMessage(to=["two@example.com"])]) + self.assertEqual(self.get_filenames(), filenames) + + backend.close() + self.assertEqual(self.get_filenames(), filenames) - connection.close() + messages = self.get_messages_from_filename(filenames[0]) + self.assertEqual(len(messages), 2) + self.assertEqual(messages[0]["to"], "one@example.com") + self.assertEqual(messages[1]["to"], "two@example.com") + + def test_reopening_connection_uses_same_file(self): + self.assertEqual(len(self.get_filenames()), 0) + + backend = mail.get_connection() + self.assertIs(backend.open(), True) + backend.send_messages([EmailMessage(to=["one@example.com"])]) + backend.close() + filenames = self.get_filenames() + self.assertEqual(len(filenames), 1) + + # Reopen the connection. + self.assertIs(backend.open(), True) + backend.send_messages([EmailMessage(to=["two@example.com"])]) + self.assertEqual(self.get_filenames(), filenames) + backend.close() + self.assertEqual(self.get_filenames(), filenames) + + messages = self.get_messages_from_filename(filenames[0]) + self.assertEqual(len(messages), 2) + self.assertEqual(messages[0]["to"], "one@example.com") + self.assertEqual(messages[1]["to"], "two@example.com") class FileBackendPathLibTests(FileBackendTests): - """ - Repeat FileBackendTests cases using a Path object as file_path. - """ + """Repeat FileBackendTests cases using a Path object as file_path.""" def mkdtemp(self): tmp_dir = super().mkdtemp() return Path(tmp_dir) -class ConsoleBackendTests(BaseEmailBackendTests, SimpleTestCase): +class ConsoleBackendTests(SharedEmailBackendTests, SimpleTestCase): email_backend = "django.core.mail.backends.console.EmailBackend" def setUp(self): @@ -286,31 +381,15 @@ def get_mailbox_content(self): return [message_from_bytes(m.encode()) for m in messages if m] def test_console_stream_kwarg(self): - """ - The console backend can be pointed at an arbitrary stream. - """ s = StringIO() - connection = mail.get_connection( + backend = mail.get_connection( "django.core.mail.backends.console.EmailBackend", stream=s ) - send_mail( - "Subject", - "Content", - "from@example.com", - ["to@example.com"], - connection=connection, - ) + backend.send_messages([EmailMessage(to=["to@example.com"])]) message = s.getvalue().split("\n" + ("-" * 79) + "\n")[0].encode() self.assertMessageHasHeaders( message, - { - ("MIME-Version", "1.0"), - ("Content-Type", 'text/plain; charset="utf-8"'), - ("Content-Transfer-Encoding", "7bit"), - ("Subject", "Subject"), - ("From", "from@example.com"), - ("To", "to@example.com"), - }, + {("To", "to@example.com")}, ) self.assertIn(b"\nDate: ", message) @@ -378,7 +457,7 @@ def stop_smtp(cls): @skipUnless(HAS_AIOSMTPD, "No aiosmtpd library detected.") -class SMTPBackendTests(BaseEmailBackendTests, SMTPBackendTestsBase): +class SMTPBackendTests(SharedEmailBackendTests, SMTPBackendTestsBase): email_backend = "django.core.mail.backends.smtp.EmailBackend" def setUp(self): @@ -395,6 +474,38 @@ def get_mailbox_content(self): def get_smtp_envelopes(self): return self.smtp_handler.smtp_envelopes + @override_settings( + EMAIL_HOST="mail.example.com", + EMAIL_PORT=822, + ) + def test_email_host_use_settings(self): + backend = smtp.EmailBackend() + self.assertEqual(backend.host, "mail.example.com") + self.assertEqual(backend.port, 822) + + @override_settings( + EMAIL_HOST="mail.example.com", + EMAIL_PORT=822, + ) + def test_email_host_override_settings(self): + backend = smtp.EmailBackend(host="other.example.net", port=5322) + self.assertEqual(backend.host, "other.example.net") + self.assertEqual(backend.port, 5322) + + def test_smtp_connection_uses_host_and_port(self): + backend = smtp.EmailBackend(host="mail.example.com", port=5322) + self.assertEqual(backend.host, "mail.example.com") + self.assertEqual(backend.port, 5322) + with ( + mock.patch("django.core.mail.backends.smtp.smtplib.SMTP") as mock_smtp, + backend, + ): + # Using backend as context manager opens the connection. + pass + mock_smtp.assert_called_once_with( + "mail.example.com", 5322, local_hostname=mock.ANY + ) + @override_settings( EMAIL_HOST_USER="not empty username", EMAIL_HOST_PASSWORD="not empty password", @@ -430,17 +541,19 @@ def test_auth_attempted(self): backend = smtp.EmailBackend( username="not empty username", password="not empty password" ) - with self.assertRaisesMessage( - SMTPException, "SMTP AUTH extension not supported by server." - ): - with backend: - pass + with mock.patch("smtplib.SMTP.login") as mock_smtp_login, backend: + # Using backend as context manager opens the connection and + # attempts login. + pass + mock_smtp_login.assert_called_once_with( + "not empty username", "not empty password" + ) def test_server_open(self): """ open() returns whether it opened a connection. """ - backend = smtp.EmailBackend(username="", password="") + backend = smtp.EmailBackend() self.assertIsNone(backend.connection) opened = backend.open() backend.close() @@ -455,16 +568,16 @@ def test_reopen_connection(self): @override_settings(EMAIL_USE_TLS=True) def test_email_tls_use_settings(self): backend = smtp.EmailBackend() - self.assertTrue(backend.use_tls) + self.assertIs(backend.use_tls, True) @override_settings(EMAIL_USE_TLS=True) def test_email_tls_override_settings(self): backend = smtp.EmailBackend(use_tls=False) - self.assertFalse(backend.use_tls) + self.assertIs(backend.use_tls, False) def test_email_tls_default_disabled(self): backend = smtp.EmailBackend() - self.assertFalse(backend.use_tls) + self.assertIs(backend.use_tls, False) def test_ssl_tls_mutually_exclusive(self): msg = ( @@ -477,16 +590,16 @@ def test_ssl_tls_mutually_exclusive(self): @override_settings(EMAIL_USE_SSL=True) def test_email_ssl_use_settings(self): backend = smtp.EmailBackend() - self.assertTrue(backend.use_ssl) + self.assertIs(backend.use_ssl, True) @override_settings(EMAIL_USE_SSL=True) def test_email_ssl_override_settings(self): backend = smtp.EmailBackend(use_ssl=False) - self.assertFalse(backend.use_ssl) + self.assertIs(backend.use_ssl, False) def test_email_ssl_default_disabled(self): backend = smtp.EmailBackend() - self.assertFalse(backend.use_ssl) + self.assertIs(backend.use_ssl, False) @override_settings(EMAIL_SSL_CERTFILE="foo") def test_email_ssl_certfile_use_settings(self): @@ -516,10 +629,20 @@ def test_email_ssl_keyfile_default_disabled(self): backend = smtp.EmailBackend() self.assertIsNone(backend.ssl_keyfile) + def test_ssl_context_uses_ssl_certfile_and_keyfile(self): + backend = smtp.EmailBackend(ssl_certfile="certfile", ssl_keyfile="keyfile") + with mock.patch( + "django.core.mail.backends.smtp.ssl.SSLContext" + ) as mock_ssl_context: + ssl_context = backend.ssl_context + self.assertIs(ssl_context, mock_ssl_context.return_value) + mock_ssl_context.assert_called_once_with(protocol=ssl.PROTOCOL_TLS_CLIENT) + ssl_context.load_cert_chain.assert_called_once_with("certfile", "keyfile") + @override_settings(EMAIL_USE_TLS=True) def test_email_tls_attempts_starttls(self): backend = smtp.EmailBackend() - self.assertTrue(backend.use_tls) + self.assertIs(backend.use_tls, True) with self.assertRaisesMessage( SMTPException, "STARTTLS extension not supported by server." ): @@ -529,15 +652,14 @@ def test_email_tls_attempts_starttls(self): @override_settings(EMAIL_USE_SSL=True) def test_email_ssl_attempts_ssl_connection(self): backend = smtp.EmailBackend() - self.assertTrue(backend.use_ssl) + self.assertIs(backend.use_ssl, True) with self.assertRaises(SSLError): with backend: pass def test_connection_timeout_default(self): - """The connection's timeout value is None by default.""" - connection = mail.get_connection("django.core.mail.backends.smtp.EmailBackend") - self.assertIsNone(connection.timeout) + backend = mail.get_connection("django.core.mail.backends.smtp.EmailBackend") + self.assertIsNone(backend.timeout) def test_connection_timeout_custom(self): """The timeout parameter can be customized.""" @@ -554,45 +676,36 @@ def __init__(self, *args, **kwargs): myemailbackend.close() @override_settings(EMAIL_TIMEOUT=10) - def test_email_timeout_override_settings(self): + def test_email_timeout_use_settings(self): backend = smtp.EmailBackend() self.assertEqual(backend.timeout, 10) - def test_email_msg_uses_crlf(self): - """#23063 -- RFC-compliant messages are sent over SMTP.""" - send = SMTP.send - try: - smtp_messages = [] - - def mock_send(self, s): - smtp_messages.append(s) - return send(self, s) - - SMTP.send = mock_send - - email = EmailMessage( - "Subject", "Content", "from@example.com", ["to@example.com"] - ) - mail.get_connection().send_messages([email]) - - # Find the actual message - msg = None - for i, m in enumerate(smtp_messages): - if m[:4] == "data": - msg = smtp_messages[i + 1] - break - - self.assertTrue(msg) - - msg = msg.decode() - # The message only contains CRLF and not combinations of CRLF, LF, - # and CR. - msg = msg.replace("\r\n", "") - self.assertNotIn("\r", msg) - self.assertNotIn("\n", msg) + @override_settings(EMAIL_TIMEOUT=10) + def test_email_timeout_override_settings(self): + backend = smtp.EmailBackend(timeout=15) + self.assertEqual(backend.timeout, 15) + + def test_smtp_connection_uses_timeout(self): + backend = smtp.EmailBackend(timeout=10) + with backend: + self.assertEqual(backend.connection.timeout, 10) + + def test_serialized_message_uses_crlf_line_ending(self): + backend = mail.get_connection() + with ( + backend, + mock.patch.object(backend.connection, "sendmail") as mock_sendmail, + ): + backend.send_messages([EmailMessage(to=["to@example.com"])]) - finally: - SMTP.send = send + # The third argument to SMTP.sendmail() is the serialized message. + mock_sendmail.assert_called_once() + msg = mock_sendmail.call_args.args[2].decode() + # The message only contains CRLF and not combinations of CRLF, LF, and + # CR (#23063). + msg = msg.replace("\r\n", "") + self.assertNotIn("\r", msg) + self.assertNotIn("\n", msg) def test_send_messages_after_open_failed(self): """ @@ -602,23 +715,25 @@ def test_send_messages_after_open_failed(self): backend = smtp.EmailBackend() # Simulate connection initialization success and a subsequent # connection exception. - backend.connection = mock.Mock(spec=object()) + backend.connection = mock.Mock() backend.open = lambda: None email = EmailMessage(to=["to@example.com"]) self.assertEqual(backend.send_messages([email]), 0) - def test_send_messages_empty_list(self): + def test_send_messages_with_empty_list_does_not_open_connection(self): backend = smtp.EmailBackend() - backend.connection = mock.Mock(spec=object()) + backend.open = mock.Mock() self.assertEqual(backend.send_messages([]), 0) + backend.open.assert_not_called() def test_send_messages_zero_sent(self): """A message isn't sent if it doesn't have any recipients.""" backend = smtp.EmailBackend() - backend.connection = mock.Mock(spec=object()) + backend.connection = mock.Mock() email = EmailMessage("Subject", "Content", "from@example.com", to=[]) sent = backend.send_messages([email]) self.assertEqual(sent, 0) + backend.connection.sendmail.assert_not_called() def test_avoids_sending_to_invalid_addresses(self): """ @@ -731,7 +846,7 @@ class SMTPBackendStoppedServerTests(SMTPBackendTestsBase): @classmethod def setUpClass(cls): super().setUpClass() - cls.backend = smtp.EmailBackend(username="", password="") + cls.backend = smtp.EmailBackend() cls.smtp_controller.stop() @classmethod diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 902b1868ce27..b399dbfaa53e 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -35,6 +35,9 @@ from django.utils.deprecation import RemovedInDjango70Warning from django.utils.translation import gettext_lazy +from . import custombackend +from .custombackend import OptionsCapturingBackend + # Check whether python/cpython#128110 has been fixed by seeing if space between # encoded-words is ignored (as required by RFC 2047 section 6.2). NEEDS_CPYTHON_128110_WORKAROUND = ( @@ -234,9 +237,7 @@ def get_message_structure(self, message, level=0): class EmailMessageTests(MailTestsMixin, SimpleTestCase): - """ - Tests for django.core.mail.EmailMessage and EmailMultiAlternative. - """ + """Tests for django.core.mail.EmailMessage and EmailMultiAlternative.""" def test_ascii(self): email = EmailMessage( @@ -802,12 +803,13 @@ def test_none_body(self): # even an empty body. self.assertEqual(msg.message().get_payload(), "\n") - @mock.patch("socket.getfqdn", return_value="漢字") - def test_non_ascii_dns_non_unicode_email(self, mocked_getfqdn): + def test_non_ascii_dns_non_unicode_email(self): delattr(DNS_NAME, "_fqdn") email = EmailMessage() email.encoding = "iso-8859-1" - self.assertIn("@xn--p8s937b>", email.message()["Message-ID"]) + with mock.patch("socket.getfqdn", return_value="漢字"): + message = email.message() + self.assertIn("@xn--p8s937b>", message["Message-ID"]) def test_encoding(self): """ @@ -1685,8 +1687,8 @@ def test_all_params_can_be_set_before_send(self): """ # 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") + original_connection = mail.get_connection() + new_connection = mail.get_connection() email = EmailMessage( "original subject", "original body\n", @@ -1809,7 +1811,7 @@ def test_message_policy_compat32(self): ) def test_send_fail_silently_conflict(self): - email = mail.EmailMessage( + email = EmailMessage( "Subject", "Body", "from@example.com", @@ -1825,9 +1827,7 @@ def test_send_fail_silently_conflict(self): class SendMailTests(SimpleTestCase, MailTestsMixin): - """ - Tests for django.core.mail.send_mail(). - """ + """Tests for django.core.mail.send_mail().""" def test_plaintext_send_mail(self): """ @@ -1863,12 +1863,25 @@ def test_html_send_mail(self): self.assertEqual(message.get_payload(1).get_content(), "HTML Content\n") self.assertEqual(message.get_payload(1).get_content_type(), "text/html") + def test_returns_count_of_messages_sent(self): + cases = [ + (["to@example.com"], 1), + (["one@example.com", "two@example.com"], 1), + ([], 0), + ] + for recipient_list, expected_count in cases: + with self.subTest(recipient_list=recipient_list): + count = send_mail( + "Subject", "Content", "from@example.com", recipient_list + ) + self.assertEqual(count, expected_count) + def test_idn_addresses(self): """ IDNA encoding is applied to non-ASCII domains in address headers (#14301). """ - self.assertTrue(send_mail("Subject", "Content", "from@öäü.com", ["to@öäü.com"])) + send_mail("Subject", "Content", "from@öäü.com", ["to@öäü.com"]) message = self.get_outbox_message() self.assertEqual(message.get("from"), "from@xn--4ca9at.com") self.assertEqual(message.get("to"), "to@xn--4ca9at.com") @@ -1878,14 +1891,14 @@ def test_lazy_addresses(self): Email sending should support lazy email addresses (#24416). """ _ = gettext_lazy - self.assertTrue(send_mail("Subject", "Content", _("tester"), [_("django")])) + send_mail("Subject", "Content", _("tester"), [_("django")]) message = self.get_outbox_message() self.assertEqual(message.get("from"), "tester") self.assertEqual(message.get("to"), "django") def test_connection_arg(self): # Send using non-default connection. - connection = mail.get_connection("mail.custombackend.EmailBackend") + connection = custombackend.EmailBackend() send_mail( "Subject", "Content", @@ -1897,13 +1910,42 @@ def test_connection_arg(self): self.assertEqual(len(connection.test_outbox), 1) self.assertEqual(connection.test_outbox[0].subject, "Subject") + def test_auth_passed_to_backend_init(self): + self.addCleanup(OptionsCapturingBackend.reset) + with self.settings(EMAIL_BACKEND="mail.custombackend.OptionsCapturingBackend"): + send_mail( + "Subject", + "Content", + "from@example.com", + ["to@example.com"], + auth_user="user", + auth_password="password", + ) + # "auth_user" and "auth_password" become "username" and "password". + init_kwargs = OptionsCapturingBackend.init_kwargs[0] + self.assertEqual(init_kwargs["username"], "user") + self.assertEqual(init_kwargs["password"], "password") + + def test_fail_silently_passed_to_backend_init(self): + self.addCleanup(OptionsCapturingBackend.reset) + with self.settings(EMAIL_BACKEND="mail.custombackend.OptionsCapturingBackend"): + send_mail( + "Subject", + "Content", + "from@example.com", + ["to@example.com"], + fail_silently=True, + ) + init_kwargs = OptionsCapturingBackend.init_kwargs[0] + self.assertIs(init_kwargs["fail_silently"], True) + def test_fail_silently_conflict(self): msg = ( "fail_silently cannot be used with a connection. " "Pass fail_silently to get_connection() instead." ) with self.assertRaisesMessage(TypeError, msg): - mail.send_mail( + send_mail( "Subject", "Body", "from@example.com", @@ -1922,7 +1964,7 @@ def test_auth_conflict(self): self.subTest(param=param), self.assertRaisesMessage(TypeError, msg), ): - mail.send_mail( + send_mail( "subject", "body", "from@example.com", @@ -1933,13 +1975,34 @@ def test_auth_conflict(self): class SendMassMailTests(SimpleTestCase): - """ - Tests for django.core.mail.send_mass_mail(). - """ + """Tests for django.core.mail.send_mass_mail().""" + + def test_send_mass_mail(self): + count = send_mass_mail( + [ + ("Subject1", "Content1", "from1@example.com", ["to1@example.com"]), + ( + "Subject2", + "Content2", + "from2@example.com", + ["to2a@example.com", "to2b@example.com"], + ), + ], + ) + self.assertEqual(count, 2) + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[0].subject, "Subject1") + self.assertEqual(mail.outbox[0].body, "Content1") + self.assertEqual(mail.outbox[0].from_email, "from1@example.com") + self.assertEqual(mail.outbox[0].to, ["to1@example.com"]) + self.assertEqual(mail.outbox[1].subject, "Subject2") + self.assertEqual(mail.outbox[1].body, "Content2") + self.assertEqual(mail.outbox[1].from_email, "from2@example.com") + self.assertEqual(mail.outbox[1].to, ["to2a@example.com", "to2b@example.com"]) def test_connection_arg(self): # Send using non-default connection. - connection = mail.get_connection("mail.custombackend.EmailBackend") + connection = custombackend.EmailBackend() send_mass_mail( [ ("Subject1", "Content1", "from1@example.com", ["to1@example.com"]), @@ -1951,6 +2014,32 @@ def test_connection_arg(self): self.assertEqual(len(connection.test_outbox), 2) self.assertEqual(connection.test_outbox[0].subject, "Subject1") self.assertEqual(connection.test_outbox[1].subject, "Subject2") + # Connection is provided to EmailMessage objects (#17811). + self.assertIs(connection.test_outbox[0].connection, connection) + self.assertIs(connection.test_outbox[1].connection, connection) + + def test_auth_passed_to_backend_init(self): + self.addCleanup(OptionsCapturingBackend.reset) + with self.settings(EMAIL_BACKEND="mail.custombackend.OptionsCapturingBackend"): + send_mass_mail( + [("Subject1", "Content1", "from1@example.com", ["to1@example.com"])], + auth_user="user", + auth_password="password", + ) + # "auth_user" and "auth_password" become "username" and "password". + init_kwargs = OptionsCapturingBackend.init_kwargs[0] + self.assertEqual(init_kwargs["username"], "user") + self.assertEqual(init_kwargs["password"], "password") + + def test_fail_silently_passed_to_backend_init(self): + self.addCleanup(OptionsCapturingBackend.reset) + with self.settings(EMAIL_BACKEND="mail.custombackend.OptionsCapturingBackend"): + send_mass_mail( + [("Subject1", "Content1", "from1@example.com", ["to1@example.com"])], + fail_silently=True, + ) + init_kwargs = OptionsCapturingBackend.init_kwargs[0] + self.assertIs(init_kwargs["fail_silently"], True) def test_send_fail_silently_conflict(self): datatuple = (("Subject", "Message", "from@example.com", ["to@example.com"]),) @@ -1959,7 +2048,7 @@ def test_send_fail_silently_conflict(self): "Pass fail_silently to get_connection() instead." ) with self.assertRaisesMessage(TypeError, msg): - mail.send_mass_mail( + send_mass_mail( datatuple, fail_silently=True, connection=mail.get_connection() ) @@ -1974,15 +2063,13 @@ def test_send_auth_conflict(self): self.subTest(param=param), self.assertRaisesMessage(TypeError, msg), ): - mail.send_mass_mail( + send_mass_mail( datatuple, **{param: "value"}, connection=mail.get_connection() ) class MailAdminsAndManagersTests(SimpleTestCase, MailTestsMixin): - """ - Tests for django.core.mail.mail_admins() and mail_managers(). - """ + """Tests for django.core.mail.mail_admins() and mail_managers().""" def test_mail_admins_and_managers(self): tests = ( @@ -2128,7 +2215,7 @@ def test_wrong_admins_managers(self): @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") + connection = custombackend.EmailBackend() mail_admins("Admin message", "Content", connection=connection) self.assertEqual(mail.outbox, []) self.assertEqual(len(connection.test_outbox), 1) @@ -2137,7 +2224,7 @@ def test_connection_arg_mail_admins(self): @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") + connection = custombackend.EmailBackend() mail_managers("Manager message", "Content", connection=connection) self.assertEqual(mail.outbox, []) self.assertEqual(len(connection.test_outbox), 1) @@ -2149,7 +2236,7 @@ def test_mail_admins_fail_silently_conflict(self): "Pass fail_silently to get_connection() instead." ) with self.assertRaisesMessage(TypeError, msg): - mail.mail_admins( + mail_admins( "Subject", "Message", fail_silently=True, @@ -2162,7 +2249,7 @@ def test_mail_managers_fail_silently_conflict(self): "Pass fail_silently to get_connection() instead." ) with self.assertRaisesMessage(TypeError, msg): - mail.mail_managers( + mail_managers( "Subject", "Message", fail_silently=True, @@ -2171,9 +2258,18 @@ def test_mail_managers_fail_silently_conflict(self): class GetConnectionTests(SimpleTestCase): - """ - Tests for django.core.mail.get_connection(). - """ + """Tests for django.core.mail.get_connection().""" + + @override_settings(EMAIL_BACKEND="django.core.mail.backends.console.EmailBackend") + def test_uses_email_backend_setting(self): + connection = mail.get_connection() + self.assertIsInstance(connection, console.EmailBackend) + + @override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend") + def test_backend_specific_kwargs(self): + connection = mail.get_connection(host="mail.example.com") + self.assertIsInstance(connection, smtp.EmailBackend) + self.assertEqual(connection.host, "mail.example.com") def test_backend_arg(self): """Test backend argument of mail.get_connection()""" @@ -2226,7 +2322,7 @@ def test_arbitrary_keyword(self): used with custom backends. """ c = mail.get_connection(fail_silently=True, foo="bar") - self.assertTrue(c.fail_silently) + self.assertIs(c.fail_silently, True) # RemovedInDjango70Warning. diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index a61c4b147f61..a79fcf4577e5 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -7,6 +7,8 @@ from unittest import mock from urllib.parse import quote +from mail.custombackend import FailingEmailBackend + from django.conf import settings from django.core import mail from django.core.exceptions import PermissionDenied @@ -499,6 +501,13 @@ def test_referer_equal_to_requested_url_without_trailing_slash_with_no_append_sl BrokenLinkEmailsMiddleware(self.get_response)(self.req) self.assertEqual(len(mail.outbox), 1) + @override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend") + def test_sends_using_fail_silently(self): + self.addCleanup(FailingEmailBackend.reset) + self.req.META["HTTP_REFERER"] = "/another/url/" + BrokenLinkEmailsMiddleware(self.get_response)(self.req) + self.assertIs(FailingEmailBackend.init_kwargs[0]["fail_silently"], True) + @override_settings(ROOT_URLCONF="middleware.cond_get_urls") class ConditionalGetMiddlewareTest(SimpleTestCase): diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index b9c8e2ac5827..e54b9ef62426 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -4,6 +4,7 @@ import traceback import unittest import warnings +from contextlib import contextmanager from functools import partial from io import StringIO from unittest import mock @@ -47,6 +48,7 @@ isolate_apps, override_settings, setup_test_environment, + teardown_test_environment, ) from django.urls import NoReverseMatch, path, reverse, reverse_lazy from django.utils.html import VOID_ELEMENTS @@ -1813,15 +1815,49 @@ def test_setup_test_environment_calling_more_than_once(self): ): setup_test_environment() + @contextmanager + def mock_test_state(self): + """Mock Django's test state within a context. + + This allows testing setup/teardown_test_environment() without impacting + the real test state or breaking Django's test runner. + + Within the context, the state is initially reset so that calling + setup_test_environment() won't raise an "already called" error. The + original (real) state is restored at the end of the context. + """ + with mock.patch("django.test.utils._TestState") as test_state: + del test_state.saved_data + yield test_state + def test_allowed_hosts(self): for type_ in (list, tuple): with self.subTest(type_=type_): allowed_hosts = type_("*") - with mock.patch("django.test.utils._TestState") as x: - del x.saved_data - with self.settings(ALLOWED_HOSTS=allowed_hosts): - setup_test_environment() - self.assertEqual(settings.ALLOWED_HOSTS, ["*", "testserver"]) + with ( + self.mock_test_state(), + self.settings(ALLOWED_HOSTS=allowed_hosts), + ): + setup_test_environment() + self.assertEqual(settings.ALLOWED_HOSTS, ["*", "testserver"]) + + def test_email_backend_override(self): + with ( + self.mock_test_state(), + self.settings( + EMAIL_BACKEND="django.core.mail.backends.console.EmailBackend" + ), + ): + setup_test_environment() + self.assertEqual( + settings.EMAIL_BACKEND, + "django.core.mail.backends.locmem.EmailBackend", + ) + teardown_test_environment() + self.assertEqual( + settings.EMAIL_BACKEND, + "django.core.mail.backends.console.EmailBackend", + ) class OverrideSettingsTests(SimpleTestCase):