Переглянути джерело

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.
Claude Paroz 11 роки тому
батько
коміт
59ebe39812

+ 1 - 0
django/conf/global_settings.py

@@ -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
django/core/mail/backends/smtp.py

@@ -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,12 +41,18 @@ class EmailBackend(BaseEmailBackend):
         try:
             # If local_hostname is not specified, socket.getfqdn() gets used.
             # 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())
-            if self.use_tls:
-                self.connection.ehlo()
-                self.connection.starttls()
-                self.connection.ehlo()
+            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()
+                    self.connection.ehlo()
             if self.username and self.password:
                 self.connection.login(self.username, self.password)
             return True

+ 20 - 0
docs/ref/settings.txt

@@ -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
docs/topics/email.txt

@@ -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
tests/mail/tests.py

@@ -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)