summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Edmunds <medmunds@gmail.com>2025-07-16 15:01:49 -0700
committernessita <124304+nessita@users.noreply.github.com>2025-07-17 14:01:16 -0300
commitfc793fc303a3d516ab51bb21aa317031caabe7b4 (patch)
treedfcb559adacc3b5dd7e276a4d3b4740a06dc7064
parent5289ce65b9a1963707767cc11c476679ab445135 (diff)
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.
-rw-r--r--django/core/mail/__init__.py42
-rw-r--r--django/core/mail/message.py38
-rw-r--r--docs/internals/deprecation.txt3
-rw-r--r--docs/releases/6.0.txt20
-rw-r--r--docs/topics/email.txt66
-rw-r--r--tests/auth_tests/test_forms.py2
-rw-r--r--tests/mail/tests.py145
7 files changed, 277 insertions, 39 deletions
diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py
index e822f1d2a2..4f49d85756 100644
--- a/django/core/mail/__init__.py
+++ b/django/core/mail/__init__.py
@@ -24,7 +24,7 @@ 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.deprecation import RemovedInDjango70Warning, deprecate_posargs
from django.utils.functional import Promise
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.
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)
+@deprecate_posargs(
+ RemovedInDjango70Warning,
+ [
+ "fail_silently",
+ "auth_user",
+ "auth_password",
+ "connection",
+ "html_message",
+ ],
+)
def send_mail(
subject,
message,
from_email,
recipient_list,
+ *,
fail_silently=False,
auth_user=None,
auth_password=None,
@@ -97,8 +109,22 @@ def send_mail(
return mail.send()
+@deprecate_posargs(
+ RemovedInDjango70Warning,
+ [
+ "fail_silently",
+ "auth_user",
+ "auth_password",
+ "connection",
+ ],
+)
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
@@ -166,8 +192,11 @@ def _send_server_message(
mail.send(fail_silently=fail_silently)
+@deprecate_posargs(
+ RemovedInDjango70Warning, ["fail_silently", "connection", "html_message"]
+)
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_server_message(
@@ -180,8 +209,11 @@ def mail_admins(
)
+@deprecate_posargs(
+ RemovedInDjango70Warning, ["fail_silently", "connection", "html_message"]
+)
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_server_message(
diff --git a/django/core/mail/message.py b/django/core/mail/message.py
index 51af560b12..93269d0310 100644
--- a/django/core/mail/message.py
+++ b/django/core/mail/message.py
@@ -17,6 +17,7 @@ from pathlib import Path
from django.conf import settings
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
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
@@ -202,12 +203,24 @@ class EmailMessage:
mixed_subtype = "mixed"
encoding = None # None => use settings default
+ @deprecate_posargs(
+ RemovedInDjango70Warning,
+ [
+ "bcc",
+ "connection",
+ "attachments",
+ "headers",
+ "cc",
+ "reply_to",
+ ],
+ )
def __init__(
self,
subject="",
body="",
from_email=None,
to=None,
+ *,
bcc=None,
connection=None,
attachments=None,
@@ -455,12 +468,25 @@ class EmailMultiAlternatives(EmailMessage):
alternative_subtype = "alternative"
+ @deprecate_posargs(
+ RemovedInDjango70Warning,
+ [
+ "bcc",
+ "connection",
+ "attachments",
+ "headers",
+ "alternatives",
+ "cc",
+ "reply_to",
+ ],
+ )
def __init__(
self,
subject="",
body="",
from_email=None,
to=None,
+ *,
bcc=None,
connection=None,
attachments=None,
@@ -478,12 +504,12 @@ class EmailMultiAlternatives(EmailMessage):
body,
from_email,
to,
- bcc,
- connection,
- attachments,
- headers,
- cc,
- reply_to,
+ bcc=bcc,
+ connection=connection,
+ attachments=attachments,
+ headers=headers,
+ cc=cc,
+ reply_to=reply_to,
)
self.alternatives = [
EmailAlternative(*alternative) for alternative in (alternatives or [])
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index fb37d43849..98b8eade75 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -38,6 +38,9 @@ details on these changes.
argument of ``django.core.paginator.Paginator`` and
``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:
6.1
diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt
index a7306ba755..3f4ce44a75 100644
--- a/docs/releases/6.0.txt
+++ b/docs/releases/6.0.txt
@@ -405,6 +405,26 @@ Miscellaneous
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
-------------
diff --git a/docs/topics/email.txt b/docs/topics/email.txt
index 77419d4eab..ec87f0337b 100644
--- a/docs/topics/email.txt
+++ b/docs/topics/email.txt
@@ -76,7 +76,7 @@ a secure connection is used.
``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()``.
@@ -90,6 +90,10 @@ are required.
* ``recipient_list``: A list of strings, each an email address. Each
member of ``recipient_list`` will see the other recipients in the "To:"
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
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
@@ -112,10 +116,15 @@ are required.
The return value will be the number of successfully delivered messages (which
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()``
====================
-.. 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.
@@ -123,8 +132,9 @@ can be ``0`` or ``1`` since it can only send one message).
(subject, message, from_email, recipient_list)
-``fail_silently``, ``auth_user`` and ``auth_password`` have the same functions
-as in :meth:`~django.core.mail.send_mail()`.
+``fail_silently``, ``auth_user``, ``auth_password`` and ``connection`` have the
+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.
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.
+.. deprecated:: 6.0
+
+ Passing ``fail_silently`` and later parameters as positional arguments is
+ deprecated.
+
``send_mass_mail()`` vs. ``send_mail()``
----------------------------------------
@@ -164,7 +179,7 @@ a single connection for all of its messages. This makes
``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
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/html` content type.
+.. deprecated:: 6.0
+
+ Passing ``fail_silently`` and later parameters as positional arguments is
+ deprecated.
+
``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
sends an email to the site managers, as defined in the :setting:`MANAGERS`
setting.
+.. deprecated:: 6.0
+
+ Passing ``fail_silently`` and later parameters as positional arguments is
+ deprecated.
+
Examples
========
@@ -294,9 +319,11 @@ email backend API :ref:`provides an alternative
.. class:: EmailMessage
The :class:`~django.core.mail.EmailMessage` class is initialized with the
-following parameters (in the given order, if positional arguments are used).
-All parameters are optional and can be set at any time prior to calling the
-``send()`` method.
+following parameters. All parameters are optional and can be set at any time
+prior to calling the ``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.
@@ -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.
+The following parameters must be given as keyword arguments if used:
+
* ``bcc``: A list or tuple of addresses used in the "Bcc" header when
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"
header when sending the email.
+.. deprecated:: 6.0
+
+ Passing all except the first four parameters as positional arguments is
+ deprecated.
+
For example::
from django.core.mail import EmailMessage
@@ -347,7 +381,7 @@ For example::
"Body goes here",
"from@example.com",
["to1@example.com", "to2@example.com"],
- ["bcc@example.com"],
+ bcc=["bcc@example.com"],
reply_to=["another@example.com"],
headers={"Message-ID": "foo"},
)
@@ -582,15 +616,15 @@ instance of the email backend that you can use.
.. 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
email backend specified in :setting:`EMAIL_BACKEND`. If you specify the
``backend`` argument, an instance of that backend will be instantiated.
-The ``fail_silently`` argument controls how the backend should handle errors.
-If ``fail_silently`` is True, exceptions during the email sending process
-will be silently ignored.
+The keyword-only ``fail_silently`` argument controls how the backend should
+handle errors. If ``fail_silently`` is True, exceptions during the email
+sending process will be silently ignored.
All other keyword arguments are passed directly to the constructor of the
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
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:
SMTP backend
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
index df91f100f5..ebfaa8b051 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -1266,7 +1266,7 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
"Sorry to hear you forgot your password.",
None,
[to_email],
- ["site_monitor@example.com"],
+ bcc=["site_monitor@example.com"],
headers={"Reply-To": "webmaster@example.com"},
alternatives=[
("Really sorry to hear you forgot your password.", "text/html")
diff --git a/tests/mail/tests.py b/tests/mail/tests.py
index 8501fc97e7..bab1b4be83 100644
--- a/tests/mail/tests.py
+++ b/tests/mail/tests.py
@@ -1751,13 +1751,13 @@ class MailTests(MailTestsMixin, SimpleTestCase):
"body\n",
"from@example.com",
["to@example.com"],
- ["bcc@example.com"],
- connection,
- [EmailAttachment("file.txt", "attachment\n", "text/plain")],
- {"X-Header": "custom header"},
- ["cc@example.com"],
- ["reply-to@example.com"],
- # (New options can be added below here, ideally as keyword-only args.)
+ # (New options can be added below here as keyword-only args.)
+ bcc=["bcc@example.com"],
+ connection=connection,
+ attachments=[EmailAttachment("file.txt", "attachment\n", "text/plain")],
+ headers={"X-Header": "custom header"},
+ cc=["cc@example.com"],
+ reply_to=["reply-to@example.com"],
)
message = email.message()
@@ -1791,12 +1791,14 @@ class MailTests(MailTestsMixin, SimpleTestCase):
"original body\n",
"original-from@example.com",
["original-to@example.com"],
- ["original-bcc@example.com"],
- original_connection,
- [EmailAttachment("original.txt", "original attachment\n", "text/plain")],
- {"X-Header": "original header"},
- ["original-cc@example.com"],
- ["original-reply-to@example.com"],
+ bcc=["original-bcc@example.com"],
+ connection=original_connection,
+ attachments=[
+ EmailAttachment("original.txt", "original attachment\n", "text/plain")
+ ],
+ headers={"X-Header": "original header"},
+ cc=["original-cc@example.com"],
+ reply_to=["original-reply-to@example.com"],
)
email.subject = "new subject"
email.body = "new body\n"
@@ -1837,6 +1839,123 @@ class MailTests(MailTestsMixin, SimpleTestCase):
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
class MailTimeZoneTests(MailTestsMixin, SimpleTestCase):
@override_settings(