Commit 59ebe398 authored by Claude Paroz's avatar Claude Paroz
Browse files

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.
parent 684a606a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -184,6 +184,7 @@ EMAIL_PORT = 25
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_USE_TLS = False
EMAIL_USE_SSL = False

# List of strings representing installed apps.
INSTALLED_APPS = ()
+20 −18
Original line number Diff line number Diff line
@@ -15,22 +15,18 @@ class EmailBackend(BaseEmailBackend):
    A wrapper that manages the SMTP network connection.
    """
    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)
        self.host = host or settings.EMAIL_HOST
        self.port = port or settings.EMAIL_PORT
        if username is None:
            self.username = settings.EMAIL_HOST_USER
        else:
            self.username = username
        if password is None:
            self.password = settings.EMAIL_HOST_PASSWORD
        else:
            self.password = password
        if use_tls is None:
            self.use_tls = settings.EMAIL_USE_TLS
        else:
            self.use_tls = use_tls
        self.username = settings.EMAIL_HOST_USER if username is None else username
        self.password = settings.EMAIL_HOST_PASSWORD if password is None else password
        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
        if self.use_ssl and self.use_tls:
            raise ValueError(
                "EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set "
                "one of those settings to True.")
        self.connection = None
        self._lock = threading.RLock()

@@ -45,8 +41,14 @@ class EmailBackend(BaseEmailBackend):
        try:
            # If local_hostname is not specified, socket.getfqdn() gets used.
            # For performance, we use the cached FQDN for local_hostname.
            if self.use_ssl:
                self.connection = smtplib.SMTP_SSL(self.host, self.port,
                                           local_hostname=DNS_NAME.get_fqdn())
            else:
                self.connection = smtplib.SMTP(self.host, self.port,
                                           local_hostname=DNS_NAME.get_fqdn())
                # TLS/SSL are mutually exclusive, so only attempt TLS over
                # non-secure connections.
                if self.use_tls:
                    self.connection.ehlo()
                    self.connection.starttls()
+20 −0
Original line number Diff line number Diff line
@@ -1055,6 +1055,26 @@ EMAIL_USE_TLS
Default: ``False``

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

+4 −3
Original line number Diff line number Diff line
@@ -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_USER` and :setting:`EMAIL_HOST_PASSWORD` settings, if
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::

@@ -408,8 +409,8 @@ SMTP backend
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` and :setting:`EMAIL_USE_TLS` settings in your
settings file.
:setting:`EMAIL_HOST_PASSWORD`, :setting:`EMAIL_USE_TLS` and
:setting:`EMAIL_USE_SSL` 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::
+55 −0
Original line number Diff line number Diff line
@@ -9,6 +9,8 @@ import smtpd
import sys
import tempfile
import threading
from smtplib import SMTPException
from ssl import SSLError

from django.core import mail
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: '))


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):
    """
    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
    """
    channel_class = FakeSMTPChannel

    def __init__(self, *args, **kwargs):
        threading.Thread.__init__(self)
@@ -738,3 +752,44 @@ class SMTPBackendTests(BaseEmailBackendTests, TestCase):
            backend.close()
        except Exception as 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)