Repository: privacyidea/privacyidea

Issue:
Allow SSL over SMTP and not only STARTTLS
Hello

I configured my SMTP server to disable STARTTLS and only allow SSL over SMTP (465).
This kind of configuration is not uncommon I think, however I couldn't find a way to implement it.

In the end, I had to patch the lib/smtpserver.py by replacing (line 107) : 

> mail = smtplib.**SMTP**(config['server'], port=int(config['port']),
>                             timeout=config.get('timeout', TIMEOUT))

with

> mail = smtplib.**SMTP_SSL**(config['server'], port=int(config['port']),
>                             timeout=config.get('timeout', TIMEOUT))

It's working fine for me right now but I'm wondering if it would be a good feature to have another checkbox with "SSL" near the "TLS" checkbox in the SMTP server configuration.

I'm not a python guy so I cant' propose a patch for the whole functionnaly
Allow SSL over SMTP and not only STARTTLS
Hello

I configured my SMTP server to disable STARTTLS and only allow SSL over SMTP (465).
This kind of configuration is not uncommon I think, however I couldn't find a way to implement it.

In the end, I had to patch the lib/smtpserver.py by replacing (line 107) : 

> mail = smtplib.**SMTP**(config['server'], port=int(config['port']),
>                             timeout=config.get('timeout', TIMEOUT))

with

> mail = smtplib.**SMTP_SSL**(config['server'], port=int(config['port']),
>                             timeout=config.get('timeout', TIMEOUT))

It's working fine for me right now but I'm wondering if it would be a good feature to have another checkbox with "SSL" near the "TLS" checkbox in the SMTP server configuration.

I'm not a python guy so I cant' propose a patch for the whole functionnaly


Discussion:
Thank you for filing an issue and sharing your observations or ideas. Please be sure to provide as many information as possible to help us working on this issue.
To my understanding the configuration would either be TLS **or** SSL, right?
Could there be SMTPoverSSL with an additional StartTLS? (sounds weird)

@maxbeth this should be reflected in the configuration. Currently I do not know how. Maybe we would only do this in the UI using javascript.
Hello

Yes, it's EITHER TLS or SSL. Actually the difference between both is:
- STARTTLS is a special protocol, starting plaintext then upgrading the connection using TLS/SSL. 
- SSL is "only" the standard protocol but on a SSL connection instead of a cleartext one

In a way, STARTTLS leak more informatique because the beginning of the conversation is not encrypted.

So STARTTLS on SSL would have no meaning. That's why Python use 2 different methods
Thank you for filing an issue and sharing your observations or ideas. Please be sure to provide as many information as possible to help us working on this issue.
To my understanding the configuration would either be TLS **or** SSL, right?
Could there be SMTPoverSSL with an additional StartTLS? (sounds weird)

@maxbeth this should be reflected in the configuration. Currently I do not know how. Maybe we would only do this in the UI using javascript.
Hello

Yes, it's EITHER TLS or SSL. Actually the difference between both is:
- STARTTLS is a special protocol, starting plaintext then upgrading the connection using TLS/SSL. 
- SSL is "only" the standard protocol but on a SSL connection instead of a cleartext one

In a way, STARTTLS leak more informatique because the beginning of the conversation is not encrypted.

So STARTTLS on SSL would have no meaning. That's why Python use 2 different methods

Resolving patch:
diff --git a/privacyidea/lib/smtpserver.py b/privacyidea/lib/smtpserver.py
--- a/privacyidea/lib/smtpserver.py
+++ b/privacyidea/lib/smtpserver.py
@@ -18,7 +18,7 @@
 #
 #
 import six
-
+from six.moves.urllib.parse import urlparse
 from privacyidea.lib.framework import get_app_config_value
 from privacyidea.lib.queue import job, wrap_job, has_job_queue
 from privacyidea.models import SMTPServer as SMTPServerDB
@@ -104,14 +104,25 @@ def test_email(config, recipient, subject, body, sender=None,
         msg['Date'] = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
         msg['Reply-To'] = reply_to
 
-        mail = smtplib.SMTP(config['server'], port=int(config['port']),
-                            timeout=config.get('timeout', TIMEOUT))
+        srv = config['server']
+        # urllib looks for a '//' to identify the host in the string. If it is
+        # missing we add one.
+        smtp_server = ''.join(['//', srv]) if srv.find('//') < 0 else srv
+        smtp_url = urlparse(smtp_server, scheme='smtp')
+        if smtp_url.scheme == 'smtps':
+            mail = smtplib.SMTP_SSL(smtp_url.hostname,
+                                    port=smtp_url.port or int(config['port']),
+                                    timeout=config.get('timeout', TIMEOUT))
+        else:
+            mail = smtplib.SMTP(smtp_url.hostname,
+                                port=smtp_url.port or int(config['port']),
+                                timeout=config.get('timeout', TIMEOUT))
         log.debug(u"submitting message to {0!s}".format(msg["To"]))
         log.debug("Saying EHLO to mailserver {0!s}".format(config['server']))
         r = mail.ehlo()
         log.debug("mailserver responded with {0!s}".format(r))
         # Start TLS if required
-        if config.get('tls', False):
+        if not smtp_url.scheme == 'smtps' and config.get('tls', False):
             log.debug("Trying to STARTTLS: {0!s}".format(config['tls']))
             mail.starttls()
         # Authenticate, if a username is given.
@@ -136,8 +147,8 @@ def test_email(config, recipient, subject, body, sender=None,
             if res_id != 200 and res_text != "OK":
                 success = False
                 log.error("Failed to send email to {0!r}: {1!r}, {2!r}".format(one_recipient,
-                                                                  res_id,
-                                                                  res_text))
+                                                                               res_id,
+                                                                               res_text))
         mail.quit()
         log.debug("I am done sending your email.")
         return success


Test patch:
diff --git a/tests/smtpmock.py b/tests/smtpmock.py
--- a/tests/smtpmock.py
+++ b/tests/smtpmock.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 """
-2016-01-20 Cornelus Kölbel <cornelius@privacyidea.org>
+2016-01-20 Cornelius Kölbel <cornelius@privacyidea.org>
            Support STARTTLS mock
 
 2015-01-30 Cornelius Kölbel <cornelius@privacyidea.org>
@@ -28,6 +28,7 @@
 )
 
 import six
+import smtplib
 
 try:
     from inspect import formatargspec, getfullargspec as getargspec
@@ -95,12 +96,16 @@ class SmtpMock(object):
     def __init__(self):
         self._calls = CallList()
         self.sent_message = None
+        self.smtp_ssl = False
         self.reset()
 
     def reset(self):
         self._request_data = {}
         self._calls.reset()
 
+    def get_smtp_ssl(self):
+        return self.smtp_ssl
+
     def setdata(self, response=None, authenticated=True,
                 config=None, exception=False, support_tls=True):
         if response is None:
@@ -149,9 +154,16 @@ def _on_login(self, SMTP_instance, username, password):
             response = (535, "authentication failed (#5.7.1)")
         return {self._request_data.get("recipient"): response}
 
-    def _on_init(self, SMTP_instance, host, port=25, timeout=3):
+#    def _on_init(self, SMTP_instance, host, port=25, timeout=3):
+    def _on_init(self, *args, **kwargs):
+        SMTP_instance = args[0]
+        host = args[1]
+        if isinstance(SMTP_instance, smtplib.SMTP_SSL):
+            # in case we need sth. to do with SMTL_SSL
+            self.smtp_ssl = True
         # mangle request packet
-        self.timeout = timeout
+        self.timeout = kwargs.get("timeout", 10)
+        self.port = kwargs.get("port", 25)
         self.esmtp_features = {}
         return None
 
diff --git a/tests/test_lib_smtpserver.py b/tests/test_lib_smtpserver.py
--- a/tests/test_lib_smtpserver.py
+++ b/tests/test_lib_smtpserver.py
@@ -96,7 +96,7 @@ def test_04_missing_configuration(self):
         self.assertRaises(ResourceNotFoundError, get_smtpserver, "notExisting")
 
     @smtpmock.activate
-    def test_05_test_email(self):
+    def test_05_test_email_smtp(self):
         smtpmock.setdata(response={"recp@example.com": (200, "OK")},
                          support_tls=True)
         identifier = "newConfig"
@@ -104,7 +104,7 @@ def test_05_test_email(self):
         port = 25
         username = "mailsender"
         password = "secret"
-        sender = "mailsender@exmaple.com"
+        sender = "mailsender@example.com"
         tls = True
         recipient = "user@example.com"
 
@@ -130,6 +130,46 @@ def test_05_test_email(self):
         self.assertEqual(parsed_email.get_content_type(), 'image/png', parsed_email)
         self.assertEqual(parsed_email.get('To'), recipient, parsed_email)
         self.assertEqual(parsed_email.get('Subject'), "Test Email with image", parsed_email)
+        # Check, that the mock SMTP server actually has NOT been configured as SMTP_SSL
+        self.assertFalse(smtpmock.get_smtp_ssl())
+
+    @smtpmock.activate
+    def test_06_test_email_smtp_ssl(self):
+        smtpmock.setdata(response={"recp@example.com": (200, "OK")},
+                         support_tls=False)
+        identifier = "newConfig"
+        server = "smtps://mailserver"
+        port = 25
+        username = "mailsender"
+        password = "secret"
+        sender = "mailsender@example.com"
+        tls = False
+        recipient = "user@example.com"
+
+        s = dict(identifier=identifier, server=server, port=port,
+                 username=username, password=password, sender=sender,
+                 tls=tls)
+        r = SMTPServer.test_email(s, recipient,
+                                  "Test Email from privacyIDEA",
+                                  "This is a test email from privacyIDEA. "
+                                  "The configuration %s is working." % identifier)
+        self.assertTrue(r)
+        parsed_email = email.message_from_string(smtpmock.get_sent_message())
+        self.assertEqual(parsed_email.get_content_type(), 'text/plain', parsed_email)
+        self.assertEqual(parsed_email.get('To'), recipient, parsed_email)
+        self.assertEqual(parsed_email.get('Subject'), "Test Email from privacyIDEA", parsed_email)
+
+        # Now with an already prepared MIME email
+        msg = MIMEImage(binascii.a2b_base64(PNG_IMG))
+        r = SMTPServer.test_email(s, recipient, "Test Email with image",
+                                  msg)
+        self.assertTrue(r)
+        parsed_email = email.message_from_string(smtpmock.get_sent_message())
+        self.assertEqual(parsed_email.get_content_type(), 'image/png', parsed_email)
+        self.assertEqual(parsed_email.get('To'), recipient, parsed_email)
+        self.assertEqual(parsed_email.get('Subject'), "Test Email with image", parsed_email)
+        # Check, if the mock SMTP server actually has been configured as SMTP_SSL
+        self.assertTrue(smtpmock.get_smtp_ssl())
 
 
 class SMTPServerQueueTestCase(MockQueueTestCase):