1
0
mirror of https://github.com/django/django.git synced 2025-04-04 21:46:40 +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.
"""
import warnings
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
# Imported for backwards compatibility and for the sake
# of a cleaner namespace. These symbols used to be in
@ -21,6 +24,8 @@ from django.core.mail.message import (
make_msgid,
)
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
__all__ = [
@ -132,14 +137,28 @@ def _send_server_message(
if not recipients:
return
if not all(isinstance(a, (list, tuple)) and len(a) == 2 for a in recipients):
raise ValueError(f"The {setting_name} setting must be a list of 2-tuples.")
# RemovedInDjango70Warning.
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(
subject="%s%s" % (settings.EMAIL_SUBJECT_PREFIX, subject),
body=message,
from_email=settings.SERVER_EMAIL,
to=[a[1] for a in recipients],
to=recipients,
connection=connection,
)
if html_message:

View File

@ -606,15 +606,7 @@ example:
Default: ``[]`` (Empty list)
A list of all the people who get code error notifications. When
``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.
A list of all the people who get code error notifications...
This marks up the following header as the "canonical" target for the
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.
* Support for setting the ``ADMINS`` or ``MANAGERS`` settings to a list of
(name, address) tuples will be removed.
.. _deprecation-removed-in-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
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
@ -2074,6 +2078,10 @@ A list in the same format as :setting:`ADMINS` that specifies who should get
broken link notifications when
:class:`~django.middleware.common.BrokenLinkEmailsMiddleware` is enabled.
.. versionchanged:: 6.0
In older versions, required a list of (name, address) tuples.
.. setting:: MEDIA_ROOT
``MEDIA_ROOT``

View File

@ -328,6 +328,11 @@ Miscellaneous
* ``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
=======================

View File

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

View File

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

View File

@ -19,6 +19,7 @@ from textwrap import dedent
from unittest import mock, skipUnless
from django.core import mail
from django.core.exceptions import ImproperlyConfigured
from django.core.mail import (
DNS_NAME,
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.test import SimpleTestCase, override_settings
from django.test.utils import requires_tz_support
from django.utils.deprecation import RemovedInDjango70Warning
from django.utils.translation import gettext_lazy
try:
@ -1139,7 +1141,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
@override_settings(
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
ADMINS=[("nobody", "nobody@example.com")],
ADMINS=["nobody@example.com"],
)
def test_connection_arg_mail_admins(self):
mail.outbox = []
@ -1152,7 +1154,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
@override_settings(
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
MANAGERS=[("nobody", "nobody@example.com")],
MANAGERS=["nobody@example.com"],
)
def test_connection_arg_mail_managers(self):
mail.outbox = []
@ -1779,13 +1781,13 @@ class BaseEmailBackendTests(MailTestsMixin):
def test_mail_admins_and_managers(self):
tests = (
# The ADMINS and MANAGERS settings are lists of (name, address) tuples.
[("Name, Full", "test@example.com")],
# The ADMINS and MANAGERS settings are lists of email strings.
['"Name, Full" <test@example.com>'],
# 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.
[(gettext_lazy("Name, Full"), gettext_lazy("test@example.com"))],
[gettext_lazy("test@example.com")],
)
for setting, mail_func in (
("ADMINS", mail_admins),
@ -1799,10 +1801,10 @@ class BaseEmailBackendTests(MailTestsMixin):
):
mail_func("subject", "content")
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])
@override_settings(MANAGERS=[("nobody", "nobody@example.com")])
@override_settings(MANAGERS=["nobody@example.com"])
def test_html_mail_managers(self):
"""Test html_message argument to mail_managers"""
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_content_type(), "text/html")
@override_settings(ADMINS=[("nobody", "nobody@example.com")])
@override_settings(ADMINS=["nobody@example.com"])
def test_html_mail_admins(self):
"""Test html_message argument to mail_admins"""
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")
@override_settings(
ADMINS=[("nobody", "nobody+admin@example.com")],
MANAGERS=[("nobody", "nobody+manager@example.com")],
ADMINS=["nobody+admin@example.com"],
MANAGERS=["nobody+manager@example.com"],
)
def test_manager_and_admin_mail_prefix(self):
"""
@ -1859,13 +1861,40 @@ class BaseEmailBackendTests(MailTestsMixin):
mail_func("hi", "there")
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):
tests = (
"test@example.com",
gettext_lazy("test@example.com"),
("test@example.com",),
["test@example.com", "other@example.com"],
("test@example.com", "other@example.com"),
# RemovedInDjango70Warning: uncomment these cases when support for
# deprecated (name, address) tuples is removed.
# [("nobody", "nobody@example.com"), ("other", "other@example.com")],
# [["nobody", "nobody@example.com"], ["other", "other@example.com"]],
[("name", "test", "example.com")],
[("Name <test@example.com",)],
[[]],
@ -1874,13 +1903,13 @@ class BaseEmailBackendTests(MailTestsMixin):
("ADMINS", mail_admins),
("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:
with (
self.subTest(setting=setting, value=value),
self.settings(**{setting: value}),
):
with self.assertRaisesMessage(ValueError, msg):
with self.assertRaisesMessage(ImproperlyConfigured, msg):
mail_func("subject", "content")
def test_message_cc_header(self):

View File

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

View File

@ -1451,7 +1451,7 @@ class ExceptionReportTestMixin:
"""
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
request = self.rf.post("/some_url/", self.breakfast_data)
if iscoroutinefunction(view):
@ -1487,7 +1487,7 @@ class ExceptionReportTestMixin:
"""
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
request = self.rf.post("/some_url/", self.breakfast_data)
if iscoroutinefunction(view):
@ -1530,7 +1530,7 @@ class ExceptionReportTestMixin:
"""
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
request = self.rf.post("/some_url/", self.breakfast_data)
view(request)