From 00535e8e6b402cab29ea3dbb7b4cc470f9839678 Mon Sep 17 00:00:00 2001 From: Andi Albrecht Date: Sun, 18 May 2014 09:56:55 +0200 Subject: [PATCH] Fixed #20743 -- Added support for keyfile/certfile in SMTP connections. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks jwmayfield, serg.partizan, and Wojciech Banaƛ for work on the patch. --- django/conf/global_settings.py | 2 ++ django/core/mail/backends/smtp.py | 10 ++++++++- docs/ref/settings.txt | 34 +++++++++++++++++++++++++++++++ docs/releases/1.8.txt | 4 ++++ docs/spelling_wordlist | 1 + docs/topics/email.txt | 12 ++++++++--- tests/mail/tests.py | 28 +++++++++++++++++++++++++ 7 files changed, 87 insertions(+), 4 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 2522337f29..7f0a1471b1 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -194,6 +194,8 @@ EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' EMAIL_USE_TLS = False EMAIL_USE_SSL = False +EMAIL_SSL_CERTFILE = None +EMAIL_SSL_KEYFILE = None # List of strings representing installed apps. INSTALLED_APPS = () diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py index dc6afb0a81..b3b23027f1 100644 --- a/django/core/mail/backends/smtp.py +++ b/django/core/mail/backends/smtp.py @@ -15,6 +15,7 @@ class EmailBackend(BaseEmailBackend): """ def __init__(self, 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): super(EmailBackend, self).__init__(fail_silently=fail_silently) self.host = host or settings.EMAIL_HOST @@ -24,6 +25,8 @@ class EmailBackend(BaseEmailBackend): self.use_tls = settings.EMAIL_USE_TLS if use_tls is None else use_tls self.use_ssl = settings.EMAIL_USE_SSL if use_ssl is None else use_ssl self.timeout = timeout + self.ssl_keyfile = settings.EMAIL_SSL_KEYFILE if ssl_keyfile is None else ssl_keyfile + self.ssl_certfile = settings.EMAIL_SSL_CERTFILE if ssl_certfile is None else ssl_certfile if self.use_ssl and self.use_tls: raise ValueError( "EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set " @@ -46,6 +49,11 @@ class EmailBackend(BaseEmailBackend): connection_params = {'local_hostname': DNS_NAME.get_fqdn()} if self.timeout is not None: connection_params['timeout'] = self.timeout + if self.use_ssl: + connection_params.update({ + 'keyfile': self.ssl_keyfile, + 'certfile': self.ssl_certfile, + }) try: self.connection = connection_class(self.host, self.port, **connection_params) @@ -53,7 +61,7 @@ class EmailBackend(BaseEmailBackend): # non-secure connections. if not self.use_ssl and self.use_tls: self.connection.ehlo() - self.connection.starttls() + self.connection.starttls(keyfile=self.ssl_keyfile, certfile=self.ssl_certfile) self.connection.ehlo() if self.username and self.password: self.connection.login(self.username, self.password) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 6c380dd2c3..00f9a1e629 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1228,6 +1228,38 @@ 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_CERTFILE + +EMAIL_SSL_CERTFILE +------------------ + +.. versionadded:: 1.8 + +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 chain file to use +for the SSL connection. + +.. setting:: EMAIL_SSL_KEYFILE + +EMAIL_SSL_KEYFILE +----------------- + +.. versionadded:: 1.8 + +Default: ``None`` + +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 +connection. Please refer to the documentation of Python's +:func:`python:ssl.wrap_socket` function for details on how the certificate chain +file and private key file are handled. + .. setting:: FILE_CHARSET FILE_CHARSET @@ -2926,6 +2958,8 @@ Email * :setting:`EMAIL_HOST_PASSWORD` * :setting:`EMAIL_HOST_USER` * :setting:`EMAIL_PORT` +* :setting:`EMAIL_SSL_CERTFILE` +* :setting:`EMAIL_SSL_KEYFILE` * :setting:`EMAIL_SUBJECT_PREFIX` * :setting:`EMAIL_USE_TLS` * :setting:`MANAGERS` diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 2c99172f9b..999b0ef295 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -140,6 +140,10 @@ Email * :ref:`Email backends ` now support the context manager protocol for opening and closing connections. +* The SMTP email backend now supports ``keyfile`` and ``certfile`` + authentication with the :setting:`EMAIL_SSL_CERTFILE` and + :setting:`EMAIL_SSL_KEYFILE` settings. + File Storage ^^^^^^^^^^^^ diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 8fbe5fd20d..60b07e9734 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -426,6 +426,7 @@ Palau params parens pdf +PEM perl permalink pessimization diff --git a/docs/topics/email.txt b/docs/topics/email.txt index eaf953dada..a7a9329974 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -445,13 +445,14 @@ can :ref:`write your own 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, **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, **kwargs]) This is the default backend. Email will be sent through a SMTP server. The server address and authentication credentials are set in the :setting:`EMAIL_HOST`, :setting:`EMAIL_PORT`, :setting:`EMAIL_HOST_USER`, - :setting:`EMAIL_HOST_PASSWORD`, :setting:`EMAIL_USE_TLS` and - :setting:`EMAIL_USE_SSL` settings in your settings file. + :setting:`EMAIL_HOST_PASSWORD`, :setting:`EMAIL_USE_TLS`, + :setting:`EMAIL_USE_SSL`, :setting:`EMAIL_SSL_CERTFILE` and + :setting:`EMAIL_SSL_KEYFILE` settings in your settings file. The SMTP backend is the default configuration inherited by Django. If you want to specify it explicitly, put the following in your settings:: @@ -481,6 +482,11 @@ SMTP backend If unspecified, the default ``timeout`` will be the one provided by :func:`socket.getdefaulttimeout()`, which defaults to ``None`` (no timeout). + .. versionchanged:: 1.8 + + The ``ssl_keyfile`` and ``ssl_certfile`` parameters and + corresponding settings were added. + .. _topic-email-console-backend: Console backend diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 11577ff159..cc5e3d8197 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -969,6 +969,34 @@ class SMTPBackendTests(BaseEmailBackendTests, SimpleTestCase): backend = smtp.EmailBackend() self.assertFalse(backend.use_ssl) + @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_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_certfile_default_disabled(self): + backend = smtp.EmailBackend() + self.assertEqual(backend.ssl_certfile, None) + + @override_settings(EMAIL_SSL_KEYFILE='foo') + def test_email_ssl_keyfile_use_settings(self): + backend = smtp.EmailBackend() + self.assertEqual(backend.ssl_keyfile, 'foo') + + @override_settings(EMAIL_SSL_KEYFILE='foo') + def test_email_ssl_keyfile_override_settings(self): + backend = smtp.EmailBackend(ssl_keyfile='bar') + self.assertEqual(backend.ssl_keyfile, 'bar') + + def test_email_ssl_keyfile_default_disabled(self): + backend = smtp.EmailBackend() + self.assertEqual(backend.ssl_keyfile, None) + @override_settings(EMAIL_USE_TLS=True) def test_email_tls_attempts_starttls(self): backend = smtp.EmailBackend()