Fixed #17471 -- Added smtplib.SMTP_SSL connection option for SMTP backend

Thanks dj.facebook at gmail.com for the report and initial patch
and Areski Belaid and senko for improvements.
This commit is contained in:
Claude Paroz 2013-07-11 20:58:06 +02:00
parent 684a606a4e
commit 59ebe39812
5 changed files with 100 additions and 21 deletions

View File

@ -184,6 +184,7 @@ EMAIL_PORT = 25
EMAIL_HOST_USER = '' EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = '' EMAIL_HOST_PASSWORD = ''
EMAIL_USE_TLS = False EMAIL_USE_TLS = False
EMAIL_USE_SSL = False
# List of strings representing installed apps. # List of strings representing installed apps.
INSTALLED_APPS = () INSTALLED_APPS = ()

View File

@ -15,22 +15,18 @@ class EmailBackend(BaseEmailBackend):
A wrapper that manages the SMTP network connection. A wrapper that manages the SMTP network connection.
""" """
def __init__(self, host=None, port=None, username=None, password=None, def __init__(self, host=None, port=None, username=None, password=None,
use_tls=None, fail_silently=False, **kwargs): use_tls=None, fail_silently=False, use_ssl=None, **kwargs):
super(EmailBackend, self).__init__(fail_silently=fail_silently) super(EmailBackend, self).__init__(fail_silently=fail_silently)
self.host = host or settings.EMAIL_HOST self.host = host or settings.EMAIL_HOST
self.port = port or settings.EMAIL_PORT self.port = port or settings.EMAIL_PORT
if username is None: self.username = settings.EMAIL_HOST_USER if username is None else username
self.username = settings.EMAIL_HOST_USER self.password = settings.EMAIL_HOST_PASSWORD if password is None else password
else: self.use_tls = settings.EMAIL_USE_TLS if use_tls is None else use_tls
self.username = username self.use_ssl = settings.EMAIL_USE_SSL if use_ssl is None else use_ssl
if password is None: if self.use_ssl and self.use_tls:
self.password = settings.EMAIL_HOST_PASSWORD raise ValueError(
else: "EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set "
self.password = password "one of those settings to True.")
if use_tls is None:
self.use_tls = settings.EMAIL_USE_TLS
else:
self.use_tls = use_tls
self.connection = None self.connection = None
self._lock = threading.RLock() self._lock = threading.RLock()
@ -45,12 +41,18 @@ class EmailBackend(BaseEmailBackend):
try: try:
# If local_hostname is not specified, socket.getfqdn() gets used. # If local_hostname is not specified, socket.getfqdn() gets used.
# For performance, we use the cached FQDN for local_hostname. # For performance, we use the cached FQDN for local_hostname.
self.connection = smtplib.SMTP(self.host, self.port, if self.use_ssl:
self.connection = smtplib.SMTP_SSL(self.host, self.port,
local_hostname=DNS_NAME.get_fqdn()) local_hostname=DNS_NAME.get_fqdn())
if self.use_tls: else:
self.connection.ehlo() self.connection = smtplib.SMTP(self.host, self.port,
self.connection.starttls() local_hostname=DNS_NAME.get_fqdn())
self.connection.ehlo() # TLS/SSL are mutually exclusive, so only attempt TLS over
# non-secure connections.
if self.use_tls:
self.connection.ehlo()
self.connection.starttls()
self.connection.ehlo()
if self.username and self.password: if self.username and self.password:
self.connection.login(self.username, self.password) self.connection.login(self.username, self.password)
return True return True

View File

@ -1055,6 +1055,26 @@ EMAIL_USE_TLS
Default: ``False`` Default: ``False``
Whether to use a TLS (secure) connection when talking to the SMTP server. Whether to use a TLS (secure) connection when talking to the SMTP server.
This is used for explicit TLS connections, generally on port 587. If you are
experiencing hanging connections, see the implicit TLS setting
:setting:`EMAIL_USE_SSL`.
.. setting:: EMAIL_USE_SSL
EMAIL_USE_SSL
-------------
.. versionadded:: 1.7
Default: ``False``
Whether to use an implicit TLS (secure) connection when talking to the SMTP
server. In most email documentation this type of TLS connection is referred
to as SSL. It is generally used on port 465. If you are experiencing problems,
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:: FILE_CHARSET .. setting:: FILE_CHARSET

View File

@ -27,7 +27,8 @@ Mail is sent using the SMTP host and port specified in the
:setting:`EMAIL_HOST` and :setting:`EMAIL_PORT` settings. The :setting:`EMAIL_HOST` and :setting:`EMAIL_PORT` settings. The
:setting:`EMAIL_HOST_USER` and :setting:`EMAIL_HOST_PASSWORD` settings, if :setting:`EMAIL_HOST_USER` and :setting:`EMAIL_HOST_PASSWORD` settings, if
set, are used to authenticate to the SMTP server, and the set, are used to authenticate to the SMTP server, and the
:setting:`EMAIL_USE_TLS` setting controls whether a secure connection is used. :setting:`EMAIL_USE_TLS` and :setting:`EMAIL_USE_SSL` settings control whether
a secure connection is used.
.. note:: .. note::
@ -408,8 +409,8 @@ SMTP backend
This is the default backend. Email will be sent through a SMTP server. This is the default backend. Email will be sent through a SMTP server.
The server address and authentication credentials are set in the The server address and authentication credentials are set in the
:setting:`EMAIL_HOST`, :setting:`EMAIL_PORT`, :setting:`EMAIL_HOST_USER`, :setting:`EMAIL_HOST`, :setting:`EMAIL_PORT`, :setting:`EMAIL_HOST_USER`,
:setting:`EMAIL_HOST_PASSWORD` and :setting:`EMAIL_USE_TLS` settings in your :setting:`EMAIL_HOST_PASSWORD`, :setting:`EMAIL_USE_TLS` and
settings file. :setting:`EMAIL_USE_SSL` settings in your settings file.
The SMTP backend is the default configuration inherited by Django. If you The SMTP backend is the default configuration inherited by Django. If you
want to specify it explicitly, put the following in your settings:: want to specify it explicitly, put the following in your settings::

View File

@ -9,6 +9,8 @@ import smtpd
import sys import sys
import tempfile import tempfile
import threading import threading
from smtplib import SMTPException
from ssl import SSLError
from django.core import mail from django.core import mail
from django.core.mail import (EmailMessage, mail_admins, mail_managers, from django.core.mail import (EmailMessage, mail_admins, mail_managers,
@ -621,11 +623,23 @@ class ConsoleBackendTests(BaseEmailBackendTests, TestCase):
self.assertTrue(s.getvalue().startswith('Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: Subject\nFrom: from@example.com\nTo: to@example.com\nDate: ')) self.assertTrue(s.getvalue().startswith('Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: Subject\nFrom: from@example.com\nTo: to@example.com\nDate: '))
class FakeSMTPChannel(smtpd.SMTPChannel):
def collect_incoming_data(self, data):
try:
super(FakeSMTPChannel, self).collect_incoming_data(data)
except UnicodeDecodeError:
# ignore decode error in SSL/TLS connection tests as we only care
# whether the connection attempt was made
pass
class FakeSMTPServer(smtpd.SMTPServer, threading.Thread): class FakeSMTPServer(smtpd.SMTPServer, threading.Thread):
""" """
Asyncore SMTP server wrapped into a thread. Based on DummyFTPServer from: Asyncore SMTP server wrapped into a thread. Based on DummyFTPServer from:
http://svn.python.org/view/python/branches/py3k/Lib/test/test_ftplib.py?revision=86061&view=markup http://svn.python.org/view/python/branches/py3k/Lib/test/test_ftplib.py?revision=86061&view=markup
""" """
channel_class = FakeSMTPChannel
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
threading.Thread.__init__(self) threading.Thread.__init__(self)
@ -738,3 +752,44 @@ class SMTPBackendTests(BaseEmailBackendTests, TestCase):
backend.close() backend.close()
except Exception as e: except Exception as e:
self.fail("close() unexpectedly raised an exception: %s" % e) self.fail("close() unexpectedly raised an exception: %s" % e)
@override_settings(EMAIL_USE_TLS=True)
def test_email_tls_use_settings(self):
backend = smtp.EmailBackend()
self.assertTrue(backend.use_tls)
@override_settings(EMAIL_USE_TLS=True)
def test_email_tls_override_settings(self):
backend = smtp.EmailBackend(use_tls=False)
self.assertFalse(backend.use_tls)
def test_email_tls_default_disabled(self):
backend = smtp.EmailBackend()
self.assertFalse(backend.use_tls)
@override_settings(EMAIL_USE_SSL=True)
def test_email_ssl_use_settings(self):
backend = smtp.EmailBackend()
self.assertTrue(backend.use_ssl)
@override_settings(EMAIL_USE_SSL=True)
def test_email_ssl_override_settings(self):
backend = smtp.EmailBackend(use_ssl=False)
self.assertFalse(backend.use_ssl)
def test_email_ssl_default_disabled(self):
backend = smtp.EmailBackend()
self.assertFalse(backend.use_ssl)
@override_settings(EMAIL_USE_TLS=True)
def test_email_tls_attempts_starttls(self):
backend = smtp.EmailBackend()
self.assertTrue(backend.use_tls)
self.assertRaisesMessage(SMTPException,
'STARTTLS extension not supported by server.', backend.open)
@override_settings(EMAIL_USE_SSL=True)
def test_email_ssl_attempts_ssl_connection(self):
backend = smtp.EmailBackend()
self.assertTrue(backend.use_ssl)
self.assertRaises(SSLError, backend.open)