1
0
mirror of https://github.com/django/django.git synced 2025-06-02 10:09:12 +00:00

Fixed #36138 -- Changed ADMINS and MANAGERS settings to lists of strings.

Previously, the ADMINS and MANAGERS settings were lists of (name, address)
tuples (where the name had been unused). Deprecated use of tuples.

Updated settings value sanity checks, and changed from ValueError to
ImproperlyConfigured.
This commit is contained in:
Mike Edmunds 2025-02-14 10:58:01 -08:00 committed by Sarah Boyce
parent aed303aff5
commit e295033144
10 changed files with 102 additions and 52 deletions

View File

@ -2,7 +2,10 @@
Tools for sending email. Tools for sending email.
""" """
import warnings
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
# Imported for backwards compatibility and for the sake # Imported for backwards compatibility and for the sake
# of a cleaner namespace. These symbols used to be in # of a cleaner namespace. These symbols used to be in
@ -21,6 +24,8 @@ from django.core.mail.message import (
make_msgid, make_msgid,
) )
from django.core.mail.utils import DNS_NAME, CachedDnsName from django.core.mail.utils import DNS_NAME, CachedDnsName
from django.utils.deprecation import RemovedInDjango70Warning
from django.utils.functional import Promise
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
__all__ = [ __all__ = [
@ -132,14 +137,28 @@ def _send_server_message(
if not recipients: if not recipients:
return return
if not all(isinstance(a, (list, tuple)) and len(a) == 2 for a in recipients): # RemovedInDjango70Warning.
raise ValueError(f"The {setting_name} setting must be a list of 2-tuples.") if all(isinstance(a, (list, tuple)) and len(a) == 2 for a in recipients):
warnings.warn(
f"Using (name, address) pairs in the {setting_name} setting is deprecated."
" Replace with a list of email address strings.",
RemovedInDjango70Warning,
stacklevel=2,
)
recipients = [a[1] for a in recipients]
if not isinstance(recipients, (list, tuple)) or not all(
isinstance(address, (str, Promise)) for address in recipients
):
raise ImproperlyConfigured(
f"The {setting_name} setting must be a list of email address strings."
)
mail = EmailMultiAlternatives( mail = EmailMultiAlternatives(
subject="%s%s" % (settings.EMAIL_SUBJECT_PREFIX, subject), subject="%s%s" % (settings.EMAIL_SUBJECT_PREFIX, subject),
body=message, body=message,
from_email=settings.SERVER_EMAIL, from_email=settings.SERVER_EMAIL,
to=[a[1] for a in recipients], to=recipients,
connection=connection, connection=connection,
) )
if html_message: if html_message:

View File

@ -606,15 +606,7 @@ example:
Default: ``[]`` (Empty list) Default: ``[]`` (Empty list)
A list of all the people who get code error notifications. When A list of all the people who get code error notifications...
``DEBUG=False`` and a view raises an exception, Django will email these people
with the full exception information. Each member of the list should be a tuple
of (Full name, email address). Example::
[("John", "john@example.com"), ("Mary", "mary@example.com")]
Note that Django will email *all* of these people whenever an error happens.
See :doc:`/howto/error-reporting` for more information.
This marks up the following header as the "canonical" target for the This marks up the following header as the "canonical" target for the
setting ``ADMINS``. This means any time I talk about ``ADMINS``, setting ``ADMINS``. This means any time I talk about ``ADMINS``,

View File

@ -28,6 +28,9 @@ details on these changes.
* The ``URLIZE_ASSUME_HTTPS`` transitional setting will be removed. * The ``URLIZE_ASSUME_HTTPS`` transitional setting will be removed.
* Support for setting the ``ADMINS`` or ``MANAGERS`` settings to a list of
(name, address) tuples will be removed.
.. _deprecation-removed-in-6.1: .. _deprecation-removed-in-6.1:
6.1 6.1

View File

@ -52,9 +52,13 @@ A list of all the people who get code error notifications. When
is configured in :setting:`LOGGING` (done by default), Django emails these is configured in :setting:`LOGGING` (done by default), Django emails these
people the details of exceptions raised in the request/response cycle. people the details of exceptions raised in the request/response cycle.
Each item in the list should be a tuple of (Full name, email address). Example:: Each item in the list should be an email address string. Example::
[("John", "john@example.com"), ("Mary", "mary@example.com")] ADMINS = ["john@example.com", '"Ng, Mary" <mary@example.com>']
.. versionchanged:: 6.0
In older versions, required a list of (name, address) tuples.
.. setting:: ALLOWED_HOSTS .. setting:: ALLOWED_HOSTS
@ -2074,6 +2078,10 @@ A list in the same format as :setting:`ADMINS` that specifies who should get
broken link notifications when broken link notifications when
:class:`~django.middleware.common.BrokenLinkEmailsMiddleware` is enabled. :class:`~django.middleware.common.BrokenLinkEmailsMiddleware` is enabled.
.. versionchanged:: 6.0
In older versions, required a list of (name, address) tuples.
.. setting:: MEDIA_ROOT .. setting:: MEDIA_ROOT
``MEDIA_ROOT`` ``MEDIA_ROOT``

View File

@ -328,6 +328,11 @@ Miscellaneous
* ``URLIZE_ASSUME_HTTPS`` transitional setting is deprecated. * ``URLIZE_ASSUME_HTTPS`` transitional setting is deprecated.
* Setting :setting:`ADMINS` or :setting:`MANAGERS` to a list of (name, address)
tuples is deprecated. Set to a list of email address strings instead. Django
never used the name portion. To include a name, format the address string as
``'"Name" <address>'`` or use Python's :func:`email.utils.formataddr`.
Features removed in 6.0 Features removed in 6.0
======================= =======================

View File

@ -248,7 +248,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
self.assertTrue(admin_email_handler.connection().fail_silently) self.assertTrue(admin_email_handler.connection().fail_silently)
@override_settings( @override_settings(
ADMINS=[("whatever admin", "admin@example.com")], ADMINS=["admin@example.com"],
EMAIL_SUBJECT_PREFIX="-SuperAwesomeSubject-", EMAIL_SUBJECT_PREFIX="-SuperAwesomeSubject-",
) )
def test_accepts_args(self): def test_accepts_args(self):
@ -280,7 +280,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
admin_email_handler.filters = orig_filters admin_email_handler.filters = orig_filters
@override_settings( @override_settings(
ADMINS=[("whatever admin", "admin@example.com")], ADMINS=["admin@example.com"],
EMAIL_SUBJECT_PREFIX="-SuperAwesomeSubject-", EMAIL_SUBJECT_PREFIX="-SuperAwesomeSubject-",
INTERNAL_IPS=["127.0.0.1"], INTERNAL_IPS=["127.0.0.1"],
) )
@ -319,7 +319,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
admin_email_handler.filters = orig_filters admin_email_handler.filters = orig_filters
@override_settings( @override_settings(
ADMINS=[("admin", "admin@example.com")], ADMINS=["admin@example.com"],
EMAIL_SUBJECT_PREFIX="", EMAIL_SUBJECT_PREFIX="",
DEBUG=False, DEBUG=False,
) )
@ -341,7 +341,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
self.assertEqual(mail.outbox[0].subject, expected_subject) self.assertEqual(mail.outbox[0].subject, expected_subject)
@override_settings( @override_settings(
ADMINS=[("admin", "admin@example.com")], ADMINS=["admin@example.com"],
DEBUG=False, DEBUG=False,
) )
def test_uses_custom_email_backend(self): def test_uses_custom_email_backend(self):
@ -372,7 +372,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
admin_email_handler.email_backend = orig_email_backend admin_email_handler.email_backend = orig_email_backend
@override_settings( @override_settings(
ADMINS=[("whatever admin", "admin@example.com")], ADMINS=["admin@example.com"],
) )
def test_emit_non_ascii(self): def test_emit_non_ascii(self):
""" """
@ -393,7 +393,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
self.assertIn("Report at %s" % url_path, msg.body) self.assertIn("Report at %s" % url_path, msg.body)
@override_settings( @override_settings(
MANAGERS=[("manager", "manager@example.com")], MANAGERS=["manager@example.com"],
DEBUG=False, DEBUG=False,
) )
def test_customize_send_mail_method(self): def test_customize_send_mail_method(self):
@ -435,7 +435,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
admin_email_handler = self.get_admin_email_handler(self.logger) admin_email_handler = self.get_admin_email_handler(self.logger)
self.assertEqual(admin_email_handler.reporter_class, ExceptionReporter) self.assertEqual(admin_email_handler.reporter_class, ExceptionReporter)
@override_settings(ADMINS=[("A.N.Admin", "admin@example.com")]) @override_settings(ADMINS=["admin@example.com"])
def test_custom_exception_reporter_is_used(self): def test_custom_exception_reporter_is_used(self):
record = self.logger.makeRecord( record = self.logger.makeRecord(
"name", logging.ERROR, "function", "lno", "message", None, None "name", logging.ERROR, "function", "lno", "message", None, None
@ -449,7 +449,7 @@ class AdminEmailHandlerTest(SimpleTestCase):
msg = mail.outbox[0] msg = mail.outbox[0]
self.assertEqual(msg.body, "message\n\ncustom traceback text") self.assertEqual(msg.body, "message\n\ncustom traceback text")
@override_settings(ADMINS=[("admin", "admin@example.com")]) @override_settings(ADMINS=["admin@example.com"])
def test_emit_no_form_tag(self): def test_emit_no_form_tag(self):
"""HTML email doesn't contain forms.""" """HTML email doesn't contain forms."""
handler = AdminEmailHandler(include_html=True) handler = AdminEmailHandler(include_html=True)
@ -567,7 +567,7 @@ class SecurityLoggerTest(LoggingAssertionMixin, SimpleTestCase):
) )
@override_settings( @override_settings(
ADMINS=[("admin", "admin@example.com")], ADMINS=["admin@example.com"],
DEBUG=False, DEBUG=False,
) )
def test_suspicious_email_admins(self): def test_suspicious_email_admins(self):

View File

@ -4,14 +4,8 @@ from django.test import SimpleTestCase, override_settings
@override_settings( @override_settings(
ADMINS=( ADMINS=["admin@example.com", "admin_and_manager@example.com"],
("Admin", "admin@example.com"), MANAGERS=["manager@example.com", "admin_and_manager@example.com"],
("Admin and Manager", "admin_and_manager@example.com"),
),
MANAGERS=(
("Manager", "manager@example.com"),
("Admin and Manager", "admin_and_manager@example.com"),
),
) )
class SendTestEmailManagementCommand(SimpleTestCase): class SendTestEmailManagementCommand(SimpleTestCase):
""" """

View File

@ -19,6 +19,7 @@ from textwrap import dedent
from unittest import mock, skipUnless from unittest import mock, skipUnless
from django.core import mail from django.core import mail
from django.core.exceptions import ImproperlyConfigured
from django.core.mail import ( from django.core.mail import (
DNS_NAME, DNS_NAME,
BadHeaderError, BadHeaderError,
@ -35,6 +36,7 @@ from django.core.mail.backends import console, dummy, filebased, locmem, smtp
from django.core.mail.message import sanitize_address from django.core.mail.message import sanitize_address
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
from django.test.utils import requires_tz_support from django.test.utils import requires_tz_support
from django.utils.deprecation import RemovedInDjango70Warning
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
try: try:
@ -1139,7 +1141,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
@override_settings( @override_settings(
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
ADMINS=[("nobody", "nobody@example.com")], ADMINS=["nobody@example.com"],
) )
def test_connection_arg_mail_admins(self): def test_connection_arg_mail_admins(self):
mail.outbox = [] mail.outbox = []
@ -1152,7 +1154,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
@override_settings( @override_settings(
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
MANAGERS=[("nobody", "nobody@example.com")], MANAGERS=["nobody@example.com"],
) )
def test_connection_arg_mail_managers(self): def test_connection_arg_mail_managers(self):
mail.outbox = [] mail.outbox = []
@ -1779,13 +1781,13 @@ class BaseEmailBackendTests(MailTestsMixin):
def test_mail_admins_and_managers(self): def test_mail_admins_and_managers(self):
tests = ( tests = (
# The ADMINS and MANAGERS settings are lists of (name, address) tuples. # The ADMINS and MANAGERS settings are lists of email strings.
[("Name, Full", "test@example.com")], ['"Name, Full" <test@example.com>'],
# Lists and tuples are interchangeable. # Lists and tuples are interchangeable.
[["Name, Full", "test@example.com"], ["ignored", "other@example.com"]], ["test@example.com", "other@example.com"],
(("", "test@example.com"), ("", "other@example.com")), ("test@example.com", "other@example.com"),
# Lazy strings are supported. # Lazy strings are supported.
[(gettext_lazy("Name, Full"), gettext_lazy("test@example.com"))], [gettext_lazy("test@example.com")],
) )
for setting, mail_func in ( for setting, mail_func in (
("ADMINS", mail_admins), ("ADMINS", mail_admins),
@ -1799,10 +1801,10 @@ class BaseEmailBackendTests(MailTestsMixin):
): ):
mail_func("subject", "content") mail_func("subject", "content")
message = self.get_the_message() message = self.get_the_message()
expected_to = ", ".join([str(address) for _, address in value]) expected_to = ", ".join([str(address) for address in value])
self.assertEqual(message.get_all("to"), [expected_to]) self.assertEqual(message.get_all("to"), [expected_to])
@override_settings(MANAGERS=[("nobody", "nobody@example.com")]) @override_settings(MANAGERS=["nobody@example.com"])
def test_html_mail_managers(self): def test_html_mail_managers(self):
"""Test html_message argument to mail_managers""" """Test html_message argument to mail_managers"""
mail_managers("Subject", "Content", html_message="HTML Content") mail_managers("Subject", "Content", html_message="HTML Content")
@ -1817,7 +1819,7 @@ class BaseEmailBackendTests(MailTestsMixin):
self.assertEqual(message.get_payload(1).get_payload(), "HTML Content") self.assertEqual(message.get_payload(1).get_payload(), "HTML Content")
self.assertEqual(message.get_payload(1).get_content_type(), "text/html") self.assertEqual(message.get_payload(1).get_content_type(), "text/html")
@override_settings(ADMINS=[("nobody", "nobody@example.com")]) @override_settings(ADMINS=["nobody@example.com"])
def test_html_mail_admins(self): def test_html_mail_admins(self):
"""Test html_message argument to mail_admins""" """Test html_message argument to mail_admins"""
mail_admins("Subject", "Content", html_message="HTML Content") mail_admins("Subject", "Content", html_message="HTML Content")
@ -1833,8 +1835,8 @@ class BaseEmailBackendTests(MailTestsMixin):
self.assertEqual(message.get_payload(1).get_content_type(), "text/html") self.assertEqual(message.get_payload(1).get_content_type(), "text/html")
@override_settings( @override_settings(
ADMINS=[("nobody", "nobody+admin@example.com")], ADMINS=["nobody+admin@example.com"],
MANAGERS=[("nobody", "nobody+manager@example.com")], MANAGERS=["nobody+manager@example.com"],
) )
def test_manager_and_admin_mail_prefix(self): def test_manager_and_admin_mail_prefix(self):
""" """
@ -1859,13 +1861,40 @@ class BaseEmailBackendTests(MailTestsMixin):
mail_func("hi", "there") mail_func("hi", "there")
self.assertEqual(self.get_mailbox_content(), []) self.assertEqual(self.get_mailbox_content(), [])
# RemovedInDjango70Warning.
def test_deprecated_admins_managers_tuples(self):
tests = (
[("nobody", "nobody@example.com"), ("other", "other@example.com")],
[["nobody", "nobody@example.com"], ["other", "other@example.com"]],
)
for setting, mail_func in (
("ADMINS", mail_admins),
("MANAGERS", mail_managers),
):
msg = (
f"Using (name, address) pairs in the {setting} setting is deprecated."
" Replace with a list of email address strings."
)
for value in tests:
self.flush_mailbox()
with (
self.subTest(setting=setting, value=value),
self.settings(**{setting: value}),
):
with self.assertWarnsMessage(RemovedInDjango70Warning, msg):
mail_func("subject", "content")
message = self.get_the_message()
expected_to = ", ".join([str(address) for _, address in value])
self.assertEqual(message.get_all("to"), [expected_to])
def test_wrong_admins_managers(self): def test_wrong_admins_managers(self):
tests = ( tests = (
"test@example.com", "test@example.com",
gettext_lazy("test@example.com"), gettext_lazy("test@example.com"),
("test@example.com",), # RemovedInDjango70Warning: uncomment these cases when support for
["test@example.com", "other@example.com"], # deprecated (name, address) tuples is removed.
("test@example.com", "other@example.com"), # [("nobody", "nobody@example.com"), ("other", "other@example.com")],
# [["nobody", "nobody@example.com"], ["other", "other@example.com"]],
[("name", "test", "example.com")], [("name", "test", "example.com")],
[("Name <test@example.com",)], [("Name <test@example.com",)],
[[]], [[]],
@ -1874,13 +1903,13 @@ class BaseEmailBackendTests(MailTestsMixin):
("ADMINS", mail_admins), ("ADMINS", mail_admins),
("MANAGERS", mail_managers), ("MANAGERS", mail_managers),
): ):
msg = "The %s setting must be a list of 2-tuples." % setting msg = f"The {setting} setting must be a list of email address strings."
for value in tests: for value in tests:
with ( with (
self.subTest(setting=setting, value=value), self.subTest(setting=setting, value=value),
self.settings(**{setting: value}), self.settings(**{setting: value}),
): ):
with self.assertRaisesMessage(ValueError, msg): with self.assertRaisesMessage(ImproperlyConfigured, msg):
mail_func("subject", "content") mail_func("subject", "content")
def test_message_cc_header(self): def test_message_cc_header(self):

View File

@ -389,7 +389,7 @@ class CommonMiddlewareTest(SimpleTestCase):
@override_settings( @override_settings(
IGNORABLE_404_URLS=[re.compile(r"foo")], IGNORABLE_404_URLS=[re.compile(r"foo")],
MANAGERS=[("PHD", "PHB@dilbert.com")], MANAGERS=["manager@example.com"],
) )
class BrokenLinkEmailsMiddlewareTest(SimpleTestCase): class BrokenLinkEmailsMiddlewareTest(SimpleTestCase):
rf = RequestFactory() rf = RequestFactory()

View File

@ -1451,7 +1451,7 @@ class ExceptionReportTestMixin:
""" """
Asserts that potentially sensitive info are displayed in the email report. Asserts that potentially sensitive info are displayed in the email report.
""" """
with self.settings(ADMINS=[("Admin", "admin@fattie-breakie.com")]): with self.settings(ADMINS=["admin@example.com"]):
mail.outbox = [] # Empty outbox mail.outbox = [] # Empty outbox
request = self.rf.post("/some_url/", self.breakfast_data) request = self.rf.post("/some_url/", self.breakfast_data)
if iscoroutinefunction(view): if iscoroutinefunction(view):
@ -1487,7 +1487,7 @@ class ExceptionReportTestMixin:
""" """
Asserts that certain sensitive info are not displayed in the email report. Asserts that certain sensitive info are not displayed in the email report.
""" """
with self.settings(ADMINS=[("Admin", "admin@fattie-breakie.com")]): with self.settings(ADMINS=["admin@example.com"]):
mail.outbox = [] # Empty outbox mail.outbox = [] # Empty outbox
request = self.rf.post("/some_url/", self.breakfast_data) request = self.rf.post("/some_url/", self.breakfast_data)
if iscoroutinefunction(view): if iscoroutinefunction(view):
@ -1530,7 +1530,7 @@ class ExceptionReportTestMixin:
""" """
Asserts that no variables or POST parameters are displayed in the email report. Asserts that no variables or POST parameters are displayed in the email report.
""" """
with self.settings(ADMINS=[("Admin", "admin@fattie-breakie.com")]): with self.settings(ADMINS=["admin@example.com"]):
mail.outbox = [] # Empty outbox mail.outbox = [] # Empty outbox
request = self.rf.post("/some_url/", self.breakfast_data) request = self.rf.post("/some_url/", self.breakfast_data)
view(request) view(request)