From 495420eec33f568c438ddcc2d2d38d5cec30c194 Mon Sep 17 00:00:00 2001 From: Igor Scheller <Igor.Scheller@deutschebahn.com> Date: Wed, 7 Aug 2024 13:49:15 +0200 Subject: [PATCH] Fixed 35653 -- Added ssl_cafile to smtp email backend. --- django/conf/global_settings.py | 1 + django/core/mail/backends/smtp.py | 12 ++++++++++-- docs/ref/settings.txt | 17 +++++++++++++++-- docs/releases/5.2.txt | 3 +++ docs/topics/email.txt | 3 ++- tests/mail/tests.py | 14 ++++++++++++++ 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index f4535acb09..ab46034d61 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -206,6 +206,7 @@ EMAIL_HOST_USER = "" EMAIL_HOST_PASSWORD = "" EMAIL_USE_TLS = False EMAIL_USE_SSL = False +EMAIL_SSL_CAFILE = None EMAIL_SSL_CERTFILE = None EMAIL_SSL_KEYFILE = None EMAIL_TIMEOUT = None diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py index 6820148ac1..7dc51165b2 100644 --- a/django/core/mail/backends/smtp.py +++ b/django/core/mail/backends/smtp.py @@ -28,6 +28,7 @@ class EmailBackend(BaseEmailBackend): timeout=None, ssl_keyfile=None, ssl_certfile=None, + ssl_cafile=None, **kwargs, ): super().__init__(fail_silently=fail_silently) @@ -44,6 +45,9 @@ class EmailBackend(BaseEmailBackend): self.ssl_certfile = ( settings.EMAIL_SSL_CERTFILE if ssl_certfile is None else ssl_certfile ) + self.ssl_cafile = ( + settings.EMAIL_SSL_CAFILE if ssl_cafile is None else ssl_cafile + ) if self.use_ssl and self.use_tls: raise ValueError( "EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set " @@ -61,9 +65,13 @@ class EmailBackend(BaseEmailBackend): if self.ssl_certfile or self.ssl_keyfile: ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile) - return ssl_context else: - return ssl.create_default_context() + ssl_context = ssl.create_default_context() + + if self.ssl_cafile: + ssl_context.load_verify_locations(cafile=self.ssl_cafile) + + return ssl_context def open(self): """ diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index e3a0f6d32a..77ecd550f6 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1495,6 +1495,17 @@ see the explicit TLS setting :setting:`EMAIL_USE_TLS`. Note that :setting:`EMAIL_USE_TLS`/:setting:`EMAIL_USE_SSL` are mutually exclusive, so only set one of those settings to ``True``. +.. setting:: EMAIL_SSL_CAFILE + +``EMAIL_SSL_CAFILE`` +---------------------- + +Default: ``None`` + +If :setting:`EMAIL_USE_SSL` or :setting:`EMAIL_USE_TLS` is ``True``, you can +optionally specify the path to a PEM-formatted certificate authority +root certificate to validate the SSL connection. + .. setting:: EMAIL_SSL_CERTFILE ``EMAIL_SSL_CERTFILE`` @@ -1517,8 +1528,9 @@ If :setting:`EMAIL_USE_SSL` or :setting:`EMAIL_USE_TLS` is ``True``, you can optionally specify the path to a PEM-formatted private key file to use for the SSL connection. -Note that setting :setting:`EMAIL_SSL_CERTFILE` and :setting:`EMAIL_SSL_KEYFILE` -doesn't result in any certificate checking. They're passed to the underlying SSL +Note that setting :setting:`EMAIL_SSL_CERTFILE`, :setting:`EMAIL_SSL_KEYFILE` +or :setting:`EMAIL_SSL_CAFILE` doesn't result in any certificate checking. +They're passed to the underlying SSL connection. Please refer to the documentation of Python's :meth:`python:ssl.SSLContext.wrap_socket` function for details on how the certificate chain file and private key file are handled. @@ -3631,6 +3643,7 @@ Email * :setting:`EMAIL_HOST_PASSWORD` * :setting:`EMAIL_HOST_USER` * :setting:`EMAIL_PORT` +* :setting:`EMAIL_SSL_CAFILE` * :setting:`EMAIL_SSL_CERTFILE` * :setting:`EMAIL_SSL_KEYFILE` * :setting:`EMAIL_SUBJECT_PREFIX` diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 4b05fd3279..419ea210ce 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -220,6 +220,9 @@ Email returns a boolean indicating whether a provided text is contained in the email ``body`` and in all attached MIME type ``text/*`` alternatives. +* The SMTP email backend now supports certificate validation using a ``cafile`` + with the :setting:`EMAIL_SSL_CAFILE` setting. + Error Reporting ~~~~~~~~~~~~~~~ diff --git a/docs/topics/email.txt b/docs/topics/email.txt index 74468063b8..36be9b96d5 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -605,7 +605,7 @@ can :ref:`write your own email backend <topic-custom-email-backend>`. SMTP backend ~~~~~~~~~~~~ -.. class:: backends.smtp.EmailBackend(host=None, port=None, username=None, password=None, use_tls=None, fail_silently=False, use_ssl=None, timeout=None, ssl_keyfile=None, ssl_certfile=None, **kwargs) +.. class:: backends.smtp.EmailBackend(host=None, port=None, username=None, password=None, use_tls=None, fail_silently=False, use_ssl=None, timeout=None, ssl_keyfile=None, ssl_certfile=None, ssl_cafile=None, **kwargs) This is the default backend. Email will be sent through a SMTP server. @@ -621,6 +621,7 @@ SMTP backend * ``timeout``: :setting:`EMAIL_TIMEOUT` * ``ssl_keyfile``: :setting:`EMAIL_SSL_KEYFILE` * ``ssl_certfile``: :setting:`EMAIL_SSL_CERTFILE` + * ``ssl_cafile``: :setting:`EMAIL_SSL_CAFILE` The SMTP backend is the default configuration inherited by Django. If you want to specify it explicitly, put the following in your settings:: diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 12cbc8e874..309571c691 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -2269,16 +2269,30 @@ class SMTPBackendTests(BaseEmailBackendTests, SMTPBackendTestsBase): backend = smtp.EmailBackend() self.assertFalse(backend.use_ssl) + @override_settings(EMAIL_SSL_CAFILE="foo") + def test_email_ssl_cafile_use_settings(self): + backend = smtp.EmailBackend() + self.assertEqual(backend.ssl_cafile, "foo") + @override_settings(EMAIL_SSL_CERTFILE="foo") def test_email_ssl_certfile_use_settings(self): backend = smtp.EmailBackend() self.assertEqual(backend.ssl_certfile, "foo") + @override_settings(EMAIL_SSL_CAFILE="foo") + def test_email_ssl_cafile_override_settings(self): + backend = smtp.EmailBackend(ssl_cafile="bar") + self.assertEqual(backend.ssl_cafile, "bar") + @override_settings(EMAIL_SSL_CERTFILE="foo") def test_email_ssl_certfile_override_settings(self): backend = smtp.EmailBackend(ssl_certfile="bar") self.assertEqual(backend.ssl_certfile, "bar") + def test_email_ssl_cafile_default_disabled(self): + backend = smtp.EmailBackend() + self.assertIsNone(backend.ssl_cafile) + def test_email_ssl_certfile_default_disabled(self): backend = smtp.EmailBackend() self.assertIsNone(backend.ssl_certfile)