1
0
mirror of https://github.com/django/django.git synced 2025-08-21 17:29:13 +00:00

Fixed #36163 -- Deprecated most positional arguments in django.core.mail.

In public mail APIs, changed less frequently used parameters from
keyword-or-positional to keyword-only, emitting a warning during the
required deprecation period.
This commit is contained in:
Mike Edmunds 2025-07-16 15:01:49 -07:00 committed by nessita
parent 5289ce65b9
commit fc793fc303
7 changed files with 277 additions and 39 deletions

View File

@ -24,7 +24,7 @@ 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.deprecation import RemovedInDjango70Warning, deprecate_posargs
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
@ -49,7 +49,8 @@ __all__ = [
] ]
def get_connection(backend=None, fail_silently=False, **kwds): @deprecate_posargs(RemovedInDjango70Warning, ["fail_silently"])
def get_connection(backend=None, *, fail_silently=False, **kwds):
"""Load an email backend and return an instance of it. """Load an email backend and return an instance of it.
If backend is None (default), use settings.EMAIL_BACKEND. If backend is None (default), use settings.EMAIL_BACKEND.
@ -61,11 +62,22 @@ def get_connection(backend=None, fail_silently=False, **kwds):
return klass(fail_silently=fail_silently, **kwds) return klass(fail_silently=fail_silently, **kwds)
@deprecate_posargs(
RemovedInDjango70Warning,
[
"fail_silently",
"auth_user",
"auth_password",
"connection",
"html_message",
],
)
def send_mail( def send_mail(
subject, subject,
message, message,
from_email, from_email,
recipient_list, recipient_list,
*,
fail_silently=False, fail_silently=False,
auth_user=None, auth_user=None,
auth_password=None, auth_password=None,
@ -97,8 +109,22 @@ def send_mail(
return mail.send() return mail.send()
@deprecate_posargs(
RemovedInDjango70Warning,
[
"fail_silently",
"auth_user",
"auth_password",
"connection",
],
)
def send_mass_mail( def send_mass_mail(
datatuple, fail_silently=False, auth_user=None, auth_password=None, connection=None datatuple,
*,
fail_silently=False,
auth_user=None,
auth_password=None,
connection=None,
): ):
""" """
Given a datatuple of (subject, message, from_email, recipient_list), send Given a datatuple of (subject, message, from_email, recipient_list), send
@ -166,8 +192,11 @@ def _send_server_message(
mail.send(fail_silently=fail_silently) mail.send(fail_silently=fail_silently)
@deprecate_posargs(
RemovedInDjango70Warning, ["fail_silently", "connection", "html_message"]
)
def mail_admins( def mail_admins(
subject, message, fail_silently=False, connection=None, html_message=None subject, message, *, fail_silently=False, connection=None, html_message=None
): ):
"""Send a message to the admins, as defined by the ADMINS setting.""" """Send a message to the admins, as defined by the ADMINS setting."""
_send_server_message( _send_server_message(
@ -180,8 +209,11 @@ def mail_admins(
) )
@deprecate_posargs(
RemovedInDjango70Warning, ["fail_silently", "connection", "html_message"]
)
def mail_managers( def mail_managers(
subject, message, fail_silently=False, connection=None, html_message=None subject, message, *, fail_silently=False, connection=None, html_message=None
): ):
"""Send a message to the managers, as defined by the MANAGERS setting.""" """Send a message to the managers, as defined by the MANAGERS setting."""
_send_server_message( _send_server_message(

View File

@ -17,6 +17,7 @@ from pathlib import Path
from django.conf import settings from django.conf import settings
from django.core.mail.utils import DNS_NAME from django.core.mail.utils import DNS_NAME
from django.utils.deprecation import RemovedInDjango70Warning, deprecate_posargs
from django.utils.encoding import force_bytes, force_str, punycode from django.utils.encoding import force_bytes, force_str, punycode
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
@ -202,12 +203,24 @@ class EmailMessage:
mixed_subtype = "mixed" mixed_subtype = "mixed"
encoding = None # None => use settings default encoding = None # None => use settings default
@deprecate_posargs(
RemovedInDjango70Warning,
[
"bcc",
"connection",
"attachments",
"headers",
"cc",
"reply_to",
],
)
def __init__( def __init__(
self, self,
subject="", subject="",
body="", body="",
from_email=None, from_email=None,
to=None, to=None,
*,
bcc=None, bcc=None,
connection=None, connection=None,
attachments=None, attachments=None,
@ -455,12 +468,25 @@ class EmailMultiAlternatives(EmailMessage):
alternative_subtype = "alternative" alternative_subtype = "alternative"
@deprecate_posargs(
RemovedInDjango70Warning,
[
"bcc",
"connection",
"attachments",
"headers",
"alternatives",
"cc",
"reply_to",
],
)
def __init__( def __init__(
self, self,
subject="", subject="",
body="", body="",
from_email=None, from_email=None,
to=None, to=None,
*,
bcc=None, bcc=None,
connection=None, connection=None,
attachments=None, attachments=None,
@ -478,12 +504,12 @@ class EmailMultiAlternatives(EmailMessage):
body, body,
from_email, from_email,
to, to,
bcc, bcc=bcc,
connection, connection=connection,
attachments, attachments=attachments,
headers, headers=headers,
cc, cc=cc,
reply_to, reply_to=reply_to,
) )
self.alternatives = [ self.alternatives = [
EmailAlternative(*alternative) for alternative in (alternatives or []) EmailAlternative(*alternative) for alternative in (alternatives or [])

View File

@ -38,6 +38,9 @@ details on these changes.
argument of ``django.core.paginator.Paginator`` and argument of ``django.core.paginator.Paginator`` and
``django.core.paginator.AsyncPaginator`` will no longer be allowed. ``django.core.paginator.AsyncPaginator`` will no longer be allowed.
* The :mod:`django.core.mail` APIs will no longer accept certain parameters as
positional arguments. These must be passed as keyword arguments instead.
.. _deprecation-removed-in-6.1: .. _deprecation-removed-in-6.1:
6.1 6.1

View File

@ -405,6 +405,26 @@ Miscellaneous
Features deprecated in 6.0 Features deprecated in 6.0
========================== ==========================
Positional arguments in ``django.core.mail`` APIs
-------------------------------------------------
.. currentmodule:: django.core.mail
:mod:`django.core.mail` APIs now require keyword arguments for less commonly
used parameters. Using positional arguments for these now emits a deprecation
warning and will raise a :exc:`TypeError` when the deprecation period ends.
* All *optional* parameters (``fail_silently`` and later) must be passed as
keyword arguments to :func:`get_connection`, :func:`mail_admins`,
:func:`mail_managers`, :func:`send_mail`, and :func:`send_mass_mail`.
* All parameters must be passed as keyword arguments when creating an
:class:`EmailMessage` or :class:`EmailMultiAlternatives` instance, except for
the first four (``subject``, ``body``, ``from_email``, and ``to``), which may
still be passed either as positional or keyword arguments.
.. currentmodule:: None
Miscellaneous Miscellaneous
------------- -------------

View File

@ -76,7 +76,7 @@ a secure connection is used.
``send_mail()`` ``send_mail()``
=============== ===============
.. function:: send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None) .. function:: send_mail(subject, message, from_email, recipient_list, *, fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None)
In most cases, you can send email using ``django.core.mail.send_mail()``. In most cases, you can send email using ``django.core.mail.send_mail()``.
@ -90,6 +90,10 @@ are required.
* ``recipient_list``: A list of strings, each an email address. Each * ``recipient_list``: A list of strings, each an email address. Each
member of ``recipient_list`` will see the other recipients in the "To:" member of ``recipient_list`` will see the other recipients in the "To:"
field of the email message. field of the email message.
The following parameters are optional, and must be given as keyword arguments
if used.
* ``fail_silently``: A boolean. When it's ``False``, ``send_mail()`` will raise * ``fail_silently``: A boolean. When it's ``False``, ``send_mail()`` will raise
an :exc:`smtplib.SMTPException` if an error occurs. See the :mod:`smtplib` an :exc:`smtplib.SMTPException` if an error occurs. See the :mod:`smtplib`
docs for a list of possible exceptions, all of which are subclasses of docs for a list of possible exceptions, all of which are subclasses of
@ -112,10 +116,15 @@ are required.
The return value will be the number of successfully delivered messages (which The return value will be the number of successfully delivered messages (which
can be ``0`` or ``1`` since it can only send one message). can be ``0`` or ``1`` since it can only send one message).
.. deprecated:: 6.0
Passing ``fail_silently`` and later parameters as positional arguments is
deprecated.
``send_mass_mail()`` ``send_mass_mail()``
==================== ====================
.. function:: send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None, connection=None) .. function:: send_mass_mail(datatuple, *, fail_silently=False, auth_user=None, auth_password=None, connection=None)
``django.core.mail.send_mass_mail()`` is intended to handle mass emailing. ``django.core.mail.send_mass_mail()`` is intended to handle mass emailing.
@ -123,8 +132,9 @@ can be ``0`` or ``1`` since it can only send one message).
(subject, message, from_email, recipient_list) (subject, message, from_email, recipient_list)
``fail_silently``, ``auth_user`` and ``auth_password`` have the same functions ``fail_silently``, ``auth_user``, ``auth_password`` and ``connection`` have the
as in :meth:`~django.core.mail.send_mail()`. same functions as in :meth:`~django.core.mail.send_mail()`. They must be given
as keyword arguments if used.
Each separate element of ``datatuple`` results in a separate email message. Each separate element of ``datatuple`` results in a separate email message.
As in :meth:`~django.core.mail.send_mail()`, recipients in the same As in :meth:`~django.core.mail.send_mail()`, recipients in the same
@ -151,6 +161,11 @@ mail server would be opened::
The return value will be the number of successfully delivered messages. The return value will be the number of successfully delivered messages.
.. deprecated:: 6.0
Passing ``fail_silently`` and later parameters as positional arguments is
deprecated.
``send_mass_mail()`` vs. ``send_mail()`` ``send_mass_mail()`` vs. ``send_mail()``
---------------------------------------- ----------------------------------------
@ -164,7 +179,7 @@ a single connection for all of its messages. This makes
``mail_admins()`` ``mail_admins()``
================= =================
.. function:: mail_admins(subject, message, fail_silently=False, connection=None, html_message=None) .. function:: mail_admins(subject, message, *, fail_silently=False, connection=None, html_message=None)
``django.core.mail.mail_admins()`` is a shortcut for sending an email to the ``django.core.mail.mail_admins()`` is a shortcut for sending an email to the
site admins, as defined in the :setting:`ADMINS` setting. site admins, as defined in the :setting:`ADMINS` setting.
@ -182,15 +197,25 @@ If ``html_message`` is provided, the resulting email will be a
:mimetype:`text/plain` content type and ``html_message`` as the :mimetype:`text/plain` content type and ``html_message`` as the
:mimetype:`text/html` content type. :mimetype:`text/html` content type.
.. deprecated:: 6.0
Passing ``fail_silently`` and later parameters as positional arguments is
deprecated.
``mail_managers()`` ``mail_managers()``
=================== ===================
.. function:: mail_managers(subject, message, fail_silently=False, connection=None, html_message=None) .. function:: mail_managers(subject, message, *, fail_silently=False, connection=None, html_message=None)
``django.core.mail.mail_managers()`` is just like ``mail_admins()``, except it ``django.core.mail.mail_managers()`` is just like ``mail_admins()``, except it
sends an email to the site managers, as defined in the :setting:`MANAGERS` sends an email to the site managers, as defined in the :setting:`MANAGERS`
setting. setting.
.. deprecated:: 6.0
Passing ``fail_silently`` and later parameters as positional arguments is
deprecated.
Examples Examples
======== ========
@ -294,9 +319,11 @@ email backend API :ref:`provides an alternative
.. class:: EmailMessage .. class:: EmailMessage
The :class:`~django.core.mail.EmailMessage` class is initialized with the The :class:`~django.core.mail.EmailMessage` class is initialized with the
following parameters (in the given order, if positional arguments are used). following parameters. All parameters are optional and can be set at any time
All parameters are optional and can be set at any time prior to calling the prior to calling the ``send()`` method.
``send()`` method.
The first four parameters can be passed as positional or keyword arguments,
but must be in the given order if positional arguments are used:
* ``subject``: The subject line of the email. * ``subject``: The subject line of the email.
@ -308,6 +335,8 @@ All parameters are optional and can be set at any time prior to calling the
* ``to``: A list or tuple of recipient addresses. * ``to``: A list or tuple of recipient addresses.
The following parameters must be given as keyword arguments if used:
* ``bcc``: A list or tuple of addresses used in the "Bcc" header when * ``bcc``: A list or tuple of addresses used in the "Bcc" header when
sending the email. sending the email.
@ -338,6 +367,11 @@ All parameters are optional and can be set at any time prior to calling the
* ``reply_to``: A list or tuple of recipient addresses used in the "Reply-To" * ``reply_to``: A list or tuple of recipient addresses used in the "Reply-To"
header when sending the email. header when sending the email.
.. deprecated:: 6.0
Passing all except the first four parameters as positional arguments is
deprecated.
For example:: For example::
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
@ -347,7 +381,7 @@ For example::
"Body goes here", "Body goes here",
"from@example.com", "from@example.com",
["to1@example.com", "to2@example.com"], ["to1@example.com", "to2@example.com"],
["bcc@example.com"], bcc=["bcc@example.com"],
reply_to=["another@example.com"], reply_to=["another@example.com"],
headers={"Message-ID": "foo"}, headers={"Message-ID": "foo"},
) )
@ -582,15 +616,15 @@ instance of the email backend that you can use.
.. currentmodule:: django.core.mail .. currentmodule:: django.core.mail
.. function:: get_connection(backend=None, fail_silently=False, **kwargs) .. function:: get_connection(backend=None, *, fail_silently=False, **kwargs)
By default, a call to ``get_connection()`` will return an instance of the By default, a call to ``get_connection()`` will return an instance of the
email backend specified in :setting:`EMAIL_BACKEND`. If you specify the email backend specified in :setting:`EMAIL_BACKEND`. If you specify the
``backend`` argument, an instance of that backend will be instantiated. ``backend`` argument, an instance of that backend will be instantiated.
The ``fail_silently`` argument controls how the backend should handle errors. The keyword-only ``fail_silently`` argument controls how the backend should
If ``fail_silently`` is True, exceptions during the email sending process handle errors. If ``fail_silently`` is True, exceptions during the email
will be silently ignored. sending process will be silently ignored.
All other keyword arguments are passed directly to the constructor of the All other keyword arguments are passed directly to the constructor of the
email backend. email backend.
@ -600,6 +634,10 @@ SMTP backend (which is the default), these backends are only useful during
testing and development. If you have special email sending requirements, you testing and development. If you have special email sending requirements, you
can :ref:`write your own email backend <topic-custom-email-backend>`. can :ref:`write your own email backend <topic-custom-email-backend>`.
.. deprecated:: 6.0
Passing ``fail_silently`` as positional argument is deprecated.
.. _topic-email-smtp-backend: .. _topic-email-smtp-backend:
SMTP backend SMTP backend

View File

@ -1266,7 +1266,7 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
"Sorry to hear you forgot your password.", "Sorry to hear you forgot your password.",
None, None,
[to_email], [to_email],
["site_monitor@example.com"], bcc=["site_monitor@example.com"],
headers={"Reply-To": "webmaster@example.com"}, headers={"Reply-To": "webmaster@example.com"},
alternatives=[ alternatives=[
("Really sorry to hear you forgot your password.", "text/html") ("Really sorry to hear you forgot your password.", "text/html")

View File

@ -1751,13 +1751,13 @@ class MailTests(MailTestsMixin, SimpleTestCase):
"body\n", "body\n",
"from@example.com", "from@example.com",
["to@example.com"], ["to@example.com"],
["bcc@example.com"], # (New options can be added below here as keyword-only args.)
connection, bcc=["bcc@example.com"],
[EmailAttachment("file.txt", "attachment\n", "text/plain")], connection=connection,
{"X-Header": "custom header"}, attachments=[EmailAttachment("file.txt", "attachment\n", "text/plain")],
["cc@example.com"], headers={"X-Header": "custom header"},
["reply-to@example.com"], cc=["cc@example.com"],
# (New options can be added below here, ideally as keyword-only args.) reply_to=["reply-to@example.com"],
) )
message = email.message() message = email.message()
@ -1791,12 +1791,14 @@ class MailTests(MailTestsMixin, SimpleTestCase):
"original body\n", "original body\n",
"original-from@example.com", "original-from@example.com",
["original-to@example.com"], ["original-to@example.com"],
["original-bcc@example.com"], bcc=["original-bcc@example.com"],
original_connection, connection=original_connection,
[EmailAttachment("original.txt", "original attachment\n", "text/plain")], attachments=[
{"X-Header": "original header"}, EmailAttachment("original.txt", "original attachment\n", "text/plain")
["original-cc@example.com"], ],
["original-reply-to@example.com"], headers={"X-Header": "original header"},
cc=["original-cc@example.com"],
reply_to=["original-reply-to@example.com"],
) )
email.subject = "new subject" email.subject = "new subject"
email.body = "new body\n" email.body = "new body\n"
@ -1837,6 +1839,123 @@ class MailTests(MailTestsMixin, SimpleTestCase):
self.assertNotIn("original", message.as_string()) self.assertNotIn("original", message.as_string())
# RemovedInDjango70Warning.
class MailDeprecatedPositionalArgsTests(SimpleTestCase):
def assertDeprecatedIn70(self, params, name):
return self.assertWarnsMessage(
RemovedInDjango70Warning,
f"Passing positional argument(s) {params} to {name}() is deprecated.",
)
def test_get_connection(self):
with self.assertDeprecatedIn70("'fail_silently'", "get_connection"):
mail.get_connection(
"django.core.mail.backends.dummy.EmailBackend",
# Deprecated positional arg:
True,
)
def test_send_mail(self):
with self.assertDeprecatedIn70(
"'fail_silently', 'auth_user', 'auth_password', 'connection', "
"'html_message'",
"send_mail",
):
send_mail(
"subject",
"message",
"from@example.com",
["to@example.com"],
# Deprecated positional args:
True,
"username",
"password",
mail.get_connection(),
"html message",
)
def test_send_mass_mail(self):
with self.assertDeprecatedIn70(
"'fail_silently', 'auth_user', 'auth_password', 'connection'",
"send_mass_mail",
):
send_mass_mail(
[],
# Deprecated positional args:
True,
"username",
"password",
mail.get_connection(),
)
def test_mail_admins(self):
with self.assertDeprecatedIn70(
"'fail_silently', 'connection', 'html_message'", "mail_admins"
):
mail_admins(
"subject",
"message",
# Deprecated positional args:
True,
mail.get_connection(),
"html message",
)
def test_mail_managers(self):
with self.assertDeprecatedIn70(
"'fail_silently', 'connection', 'html_message'", "mail_managers"
):
mail_managers(
"subject",
"message",
# Deprecated positional args:
True,
mail.get_connection(),
"html message",
)
def test_email_message_init(self):
with self.assertDeprecatedIn70(
"'bcc', 'connection', 'attachments', 'headers', 'cc', 'reply_to'",
"EmailMessage",
):
EmailMessage(
"subject",
"body\n",
"from@example.com",
["to@example.com"],
# Deprecated positional args:
["bcc@example.com"],
mail.get_connection(),
[EmailAttachment("file.txt", "attachment\n", "text/plain")],
{"X-Header": "custom header"},
["cc@example.com"],
["reply-to@example.com"],
)
def test_email_multi_alternatives_init(self):
with self.assertDeprecatedIn70(
"'bcc', 'connection', 'attachments', 'headers', 'alternatives', 'cc', "
"'reply_to'",
"EmailMultiAlternatives",
):
EmailMultiAlternatives(
"subject",
"body\n",
"from@example.com",
["to@example.com"],
# Deprecated positional args:
["bcc@example.com"],
mail.get_connection(),
[EmailAttachment("file.txt", "attachment\n", "text/plain")],
{"X-Header": "custom header"},
[EmailAlternative("html body", "text/html")],
["cc@example.com"],
["reply-to@example.com"],
)
@requires_tz_support @requires_tz_support
class MailTimeZoneTests(MailTestsMixin, SimpleTestCase): class MailTimeZoneTests(MailTestsMixin, SimpleTestCase):
@override_settings( @override_settings(