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)