diff --git a/django/core/checks/mail.py b/django/core/checks/mail.py index 49fcf0d8e4fc..02fb2dd2a5ac 100644 --- a/django/core/checks/mail.py +++ b/django/core/checks/mail.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.core.checks import Tags, Warning, register +from django.core.checks import Error, Tags, Warning, register from django.core.mail import DEFAULT_MAILER_ALIAS @@ -27,3 +27,37 @@ def check_mailers_default_alias(app_configs, **kwargs): id="mail.W001", ) ] + + +NON_PRODUCTION_EMAIL_BACKENDS = { + "django.core.mail.backends.console.EmailBackend", + "django.core.mail.backends.dummy.EmailBackend", + "django.core.mail.backends.filebased.EmailBackend", + "django.core.mail.backends.locmem.EmailBackend", +} + + +@register(Tags.mail, deploy=True) +def check_mailers_production_backend(app_configs, **kwargs): + try: + backend = settings.MAILERS[DEFAULT_MAILER_ALIAS]["BACKEND"] + except (AttributeError, KeyError): + # There is no "default" backend to inspect: either MAILERS is not + # defined, or there is no "default" entry, or no "BACKEND" key in it. + # An omitted BACKEND defaults to SMTP, which is fine. + return [] + + if backend not in NON_PRODUCTION_EMAIL_BACKENDS: + return [] + + return [ + Error( + f"Your MAILERS setting uses a development-only email backend in the " + f"'{DEFAULT_MAILER_ALIAS}' entry ({backend}).", + hint=( + "Use a production-ready email backend, such as the SMTP backend, " + "otherwise email will not be sent." + ), + id="mail.E001", + ) + ] diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 3928057c84d8..68d1e49b1df1 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -158,6 +158,9 @@ Mail The following checks verify that your :setting:`MAILERS` setting is correctly configured: +* **mail.E001**: Your :setting:`MAILERS` setting uses a development-only email + backend in the ``'default'`` entry (````). (Only checked when the + :option:`check --deploy` option is used.) * **mail.W001**: Your :setting:`MAILERS` setting has no ``'default'`` entry. Sending email without a valid mailer will fail. diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index c441709057e4..43c770e5c169 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -240,6 +240,10 @@ CSP Email ~~~~~ +* A new ``mail.E001`` deployment-only system check prevents using one of + Django's email backends that is not intended for production use in the + ``'default'`` :setting:`MAILERS` entry. + * A new ``mail.W001`` system check warns when :setting:`MAILERS` is defined but does not include a ``'default'`` entry. diff --git a/tests/check_framework/test_mail.py b/tests/check_framework/test_mail.py index a67e147e9282..a8df37e19f8e 100644 --- a/tests/check_framework/test_mail.py +++ b/tests/check_framework/test_mail.py @@ -1,5 +1,8 @@ -from django.core.checks import Warning -from django.core.checks.mail import check_mailers_default_alias +from django.core.checks import Error, Warning, registry +from django.core.checks.mail import ( + check_mailers_default_alias, + check_mailers_production_backend, +) from django.test import SimpleTestCase, override_settings @@ -53,3 +56,60 @@ def test_mailers_without_default_alias(self): ) ], ) + + +class MailersProductionBackendTests(SimpleTestCase): + def test_is_deployment_only(self): + self.assertIn( + check_mailers_production_backend, registry.registry.deployment_checks + ) + self.assertNotIn( + check_mailers_production_backend, registry.registry.registered_checks + ) + + def test_mailers_not_defined(self): + self.assertEqual(check_mailers_production_backend(None), []) + + def test_production_backends(self): + production_backends = [ + "django.core.mail.backends.smtp.EmailBackend", + "any.third.party.EmailBackend", + ] + for backend in production_backends: + with ( + self.subTest(backend=backend), + self.settings(MAILERS={"default": {"BACKEND": backend}}), + ): + self.assertEqual(check_mailers_production_backend(None), []) + + def test_non_production_backends(self): + hint = ( + "Use a production-ready email backend, such as the SMTP backend, " + "otherwise email will not be sent." + ) + for backend_type in ["console", "dummy", "filebased", "locmem"]: + backend = f"django.core.mail.backends.{backend_type}.EmailBackend" + msg = ( + f"Your MAILERS setting uses a development-only email backend in " + f"the 'default' entry ({backend})." + ) + with ( + self.subTest(backend=backend), + self.settings(MAILERS={"default": {"BACKEND": backend}}), + ): + self.assertEqual( + check_mailers_production_backend(None), + [Error(msg, hint=hint, id="mail.E001")], + ) + + @override_settings( + MAILERS={ + "default": {"BACKEND": "django.core.mail.backends.smtp.EmailBackend"}, + "secondary": {"BACKEND": "django.core.mail.backends.console.EmailBackend"}, + } + ) + def test_only_applies_to_default_mailer(self): + self.assertEqual( + check_mailers_production_backend(None), + [], + )