summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJ08nY2017-03-13 02:14:26 +0100
committerJ08nY2017-07-24 18:44:19 +0200
commitad463598885ecc91bee9fbca5aea9fcb4e9f627e (patch)
tree9363afa79afaf75c32742c57b6e1cee8754d7a32 /src
parent02826321d0430d7ffc1f674eeff4221941689ef7 (diff)
downloadmailman-mta-smtps-starttls.tar.gz
mailman-mta-smtps-starttls.tar.zst
mailman-mta-smtps-starttls.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/config/schema.cfg16
-rw-r--r--src/mailman/interfaces/configuration.py9
-rw-r--r--src/mailman/interfaces/smtp.py32
-rw-r--r--src/mailman/mta/base.py31
-rw-r--r--src/mailman/mta/connection.py103
-rw-r--r--src/mailman/mta/docs/connection.rst29
-rw-r--r--src/mailman/mta/tests/test_base.py71
-rw-r--r--src/mailman/mta/tests/test_connection.py89
-rw-r--r--src/mailman/testing/layers.py118
-rw-r--r--src/mailman/testing/mta.py53
-rw-r--r--src/mailman/testing/ssl_test_cert.crt21
-rw-r--r--src/mailman/testing/ssl_test_key.key28
12 files changed, 552 insertions, 48 deletions
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index 9568694be..35ff2f3ea 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -596,6 +596,22 @@ smtp_port: 25
smtp_user:
smtp_pass:
+# One of smtp/smtps/starttls, specifies the protocol Mailman will use when
+# connecting. The difference between smtps and startls is that with smtps the
+# connection starts with the SSL/TLS handshake, while starttls is an extension
+# of the SMTP protocol which starts the SSL/TLS handshake after the client
+# requests it.
+smtp_protocol: smtp
+
+# Whether to require a server cert and verify it. Verification in this
+# context means that the server needs to supply a valid certificate signed by
+# a CA from a set of the system's default CA certs.
+smtp_verify_cert: yes
+
+# Whether to check that the server certificate specifies the hostname as
+# in smtp_host. RFC 2818 and RFC 6125 rules are followed.
+smtp_verify_hostname: yes
+
# Where the LMTP server listens for connections. Use 127.0.0.1 instead of
# localhost for Postfix integration, because Postfix only consults DNS
# (e.g. not /etc/hosts).
diff --git a/src/mailman/interfaces/configuration.py b/src/mailman/interfaces/configuration.py
index 2a9369fd1..7cee9972a 100644
--- a/src/mailman/interfaces/configuration.py
+++ b/src/mailman/interfaces/configuration.py
@@ -31,6 +31,15 @@ class MissingConfigurationFileError(MailmanError):
@public
+class InvalidConfigurationError(MailmanError):
+ """A configuration key had an unexpected/invalid value."""
+
+ def __init__(self, key, value):
+ self.key = key
+ self.value = value
+
+
+@public
class IConfiguration(Interface):
"""Marker interface; used for adaptation in the REST API."""
diff --git a/src/mailman/interfaces/smtp.py b/src/mailman/interfaces/smtp.py
new file mode 100644
index 000000000..477152621
--- /dev/null
+++ b/src/mailman/interfaces/smtp.py
@@ -0,0 +1,32 @@
+# Copyright (C) 2009-2017 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Interface for a SMTP connection."""
+
+from public import public
+from zope.interface import Interface
+
+
+@public
+class ISMTPConnection(Interface):
+ """A SMTP connection. Like smtplib.SMTP."""
+
+ def sendmail(self, envsender, recipients, msgtext):
+ """Mimic `smtplib.SMTP.sendmail`."""
+
+ def quit(self):
+ """Mimic `smtplib.SMTP.quit`."""
diff --git a/src/mailman/mta/base.py b/src/mailman/mta/base.py
index 4cc7742e8..b182da027 100644
--- a/src/mailman/mta/base.py
+++ b/src/mailman/mta/base.py
@@ -22,9 +22,12 @@ import socket
import logging
import smtplib
+from lazr.config import as_boolean
from mailman.config import config
+from mailman.interfaces.configuration import InvalidConfigurationError
from mailman.interfaces.mta import IMailTransportAgentDelivery
-from mailman.mta.connection import Connection
+from mailman.mta.connection import (
+ SMTPConnection, SMTPSConnection, STARTTLSConnection)
from public import public
from zope.interface import implementer
@@ -39,12 +42,30 @@ class BaseDelivery:
def __init__(self):
"""Create a basic deliverer."""
+ host = config.mta.smtp_host
+ port = int(config.mta.smtp_port)
username = (config.mta.smtp_user if config.mta.smtp_user else None)
password = (config.mta.smtp_pass if config.mta.smtp_pass else None)
- self._connection = Connection(
- config.mta.smtp_host, int(config.mta.smtp_port),
- int(config.mta.max_sessions_per_connection),
- username, password)
+ protocol = config.mta.smtp_protocol
+ verify_cert = as_boolean(config.mta.smtp_verify_cert)
+ verify_hostname = as_boolean(config.mta.smtp_verify_hostname)
+ max_session_count = int(config.mta.max_sessions_per_connection)
+
+ proto = protocol.lower()
+ if proto == 'smtp':
+ self._connection = SMTPConnection(host, port, max_session_count,
+ username, password)
+ elif proto == 'smtps':
+ self._connection = SMTPSConnection(host, port, max_session_count,
+ verify_cert, verify_hostname,
+ username, password)
+ elif proto == 'starttls':
+ self._connection = STARTTLSConnection(host, port,
+ max_session_count,
+ verify_cert, verify_hostname,
+ username, password)
+ else:
+ raise InvalidConfigurationError('mta.smtp_protocol', protocol)
def _deliver_to_recipients(self, mlist, msg, msgdata, recipients):
"""Low-level delivery to a set of recipients.
diff --git a/src/mailman/mta/connection.py b/src/mailman/mta/connection.py
index 797b979b2..d6824bccf 100644
--- a/src/mailman/mta/connection.py
+++ b/src/mailman/mta/connection.py
@@ -17,21 +17,23 @@
"""MTA connections."""
+import ssl
import logging
import smtplib
from contextlib import suppress
from lazr.config import as_boolean
from mailman.config import config
+from mailman.interfaces.smtp import ISMTPConnection
from public import public
-
+from zope.interface import implementer
log = logging.getLogger('mailman.smtp')
-@public
+@implementer(ISMTPConnection)
class Connection:
- """Manage a connection to the SMTP server."""
+
def __init__(self, host, port, sessions_per_connection,
smtp_user=None, smtp_pass=None):
"""Create a connection manager.
@@ -60,24 +62,20 @@ class Connection:
self._session_count = None
self._connection = None
- def _connect(self):
- """Open a new connection."""
- self._connection = smtplib.SMTP()
- log.debug('Connecting to %s:%s', self._host, self._port)
- self._connection.connect(self._host, self._port)
+ def _login(self):
+ """Send login if both username and password are specified."""
if self._username is not None and self._password is not None:
log.debug('Logging in')
self._connection.login(self._username, self._password)
- self._session_count = self._sessions_per_connection
def sendmail(self, envsender, recipients, msgtext):
- """Mimic `smtplib.SMTP.sendmail`."""
if as_boolean(config.devmode.enabled):
# Force the recipients to the specified address, but still deliver
# to the same number of recipients.
recipients = [config.devmode.recipient] * len(recipients)
if self._connection is None:
self._connect()
+ self._login()
try:
log.debug('envsender: %s, recipients: %s, size(msgtext): %s',
envsender, recipients, len(msgtext))
@@ -97,9 +95,92 @@ class Connection:
return results
def quit(self):
- """Mimic `smtplib.SMTP.quit`."""
if self._connection is None:
return
with suppress(smtplib.SMTPException):
self._connection.quit()
self._connection = None
+
+
+@public
+class SMTPConnection(Connection):
+ """Manage a clear connection to the SMTP server."""
+
+ def _connect(self):
+ """Open a new connection."""
+ log.debug('Connecting to %s:%s', self._host, self._port)
+ self._connection = smtplib.SMTP(self._host, self._port)
+ self._session_count = self._sessions_per_connection
+
+
+class _SSLConnection(Connection):
+ """Manage a SMTP connection with a SSL context(either SMTPS or STARTTLS)"""
+
+ def __init__(self, host, port, sessions_per_connection,
+ verify_cert=False, verify_hostname=False,
+ smtp_user=None, smtp_pass=None):
+ """
+ Create a connection manager with and SSL context.
+
+ :param host: The host name of the SMTP server to connect to.
+ :type host: string
+ :param port: The port number of the SMTP server to connect to.
+ :type port: integer
+ :param sessions_per_connection: The number of SMTP sessions per
+ connection to the SMTP server. After this number of sessions
+ has been reached, the connection is closed and a new one is
+ opened. Set to zero for an unlimited number of sessions per
+ connection (i.e. your MTA has no limit).
+ :type sessions_per_connection: integer
+ :param verify_cert: Whether to require a server cert and verify it.
+ Verification in this context means that the server needs to supply
+ a valid certificate signed by a CA from a set of the system's
+ default CA certs.
+ :type verify_cert: bool
+ :param verify_hostname: Whether to check that the server certificate
+ specifies the hostname as passed to this constructor.
+ RFC 2818 and RFC 6125 rules are followed.
+ :type verify_hostname: bool
+ :param smtp_user: Optional SMTP authentication user name. If given,
+ `smtp_pass` must also be given.
+ :type smtp_user: str
+ :param smtp_pass: Optional SMTP authentication password. If given,
+ `smtp_user` must also be given.
+ """
+ super().__init__(host, port, sessions_per_connection,
+ smtp_user, smtp_pass)
+ self._tls_context = self._get_context(verify_cert, verify_hostname)
+
+ def _get_context(self, verify_cert, verify_hostname):
+ """Create and return a new SSLContext."""
+ ssl_context = ssl.create_default_context()
+ ssl_context.check_hostname = verify_hostname
+ if verify_cert:
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
+ else:
+ ssl_context.verify_mode = ssl.CERT_NONE
+ return ssl_context
+
+
+@public
+class SMTPSConnection(_SSLConnection):
+ """Manage a SMTPS connection."""
+
+ def _connect(self):
+ log.debug('Connecting to %s:%s', self._host, self._port)
+ self._connection = smtplib.SMTP_SSL(self._host, self._port,
+ context=self._tls_context)
+ self._session_count = self._sessions_per_connection
+
+
+@public
+class STARTTLSConnection(_SSLConnection, SMTPConnection):
+ """Manage a plain connection with STARTTLS."""
+
+ def _connect(self):
+ super()._connect()
+ log.debug('Starttls')
+ try:
+ self._connection.starttls(context=self._tls_context)
+ except smtplib.SMTPNotSupportedError as notls:
+ log.error('Starttls failed: ' + str(notls))
diff --git a/src/mailman/mta/docs/connection.rst b/src/mailman/mta/docs/connection.rst
index 8b11605cb..687ec06fb 100644
--- a/src/mailman/mta/docs/connection.rst
+++ b/src/mailman/mta/docs/connection.rst
@@ -3,16 +3,17 @@ MTA connections
===============
Outgoing connections to the outgoing mail transport agent (MTA) are mitigated
-through a ``Connection`` class, which can transparently manage multiple
+through a ``SMTPConnection`` class, which can transparently manage multiple
sessions in a single connection.
- >>> from mailman.mta.connection import Connection
+ >>> from mailman.mta.connection import SMTPConnection, SMTPSConnection
+ >>> from lazr.config import as_boolean
-The number of sessions per connections is specified when the ``Connection``
+The number of sessions per connections is specified when the ``SMTPConnection``
object is created, as is the host and port number of the SMTP server. Zero
means there's an unlimited number of sessions per connection.
- >>> connection = Connection(
+ >>> connection = SMTPConnection(
... config.mta.smtp_host, int(config.mta.smtp_port), 0)
At the start, there have been no connections to the server.
@@ -59,7 +60,7 @@ will authenticate with the mail server after each new connection.
... smtp_pass: testpass
... """)
- >>> connection = Connection(
+ >>> connection = SMTPConnection(
... config.mta.smtp_host, int(config.mta.smtp_port), 0,
... config.mta.smtp_user, config.mta.smtp_pass)
>>> connection.sendmail('anne@example.com', ['bart@example.com'], """\
@@ -75,6 +76,17 @@ will authenticate with the mail server after each new connection.
>>> reset()
>>> config.pop('auth')
+SMTPS and STARTTLS support works through ``SMTPSConnection``
+and ``STARTTLSConnection`` classes. These provide the same interface
+as ``SMTPConnection``.
+::
+
+ >>> connection = SMTPSConnection(
+ ... config.mta.smtp_host, int(config.mta.smtp_port), 0,
+ ... as_boolean(config.mta.smtp_verify_cert),
+ ... as_boolean(config.mta.smtp_verify_hostname),
+ ... config.mta.smtp_user, config.mta.smtp_pass)
+
Sessions per connection
=======================
@@ -86,13 +98,13 @@ created.
The connection count starts at zero.
::
- >>> connection = Connection(
+ >>> connection = SMTPConnection(
... config.mta.smtp_host, int(config.mta.smtp_port), 2)
>>> smtpd.get_connection_count()
0
-We send two messages through the ``Connection`` object. Only one connection
+We send two messages through the ``SMTPConnection`` object. Only one connection
is opened.
::
@@ -168,7 +180,7 @@ No maximum
A value of zero means that there is an unlimited number of sessions per
connection.
- >>> connection = Connection(
+ >>> connection = SMTPConnection(
... config.mta.smtp_host, int(config.mta.smtp_port), 0)
>>> reset()
@@ -230,3 +242,4 @@ about accidental deliveries to unintended recipients.
<BLANKLINE>
>>> config.pop('devmode')
+
diff --git a/src/mailman/mta/tests/test_base.py b/src/mailman/mta/tests/test_base.py
new file mode 100644
index 000000000..52fa63430
--- /dev/null
+++ b/src/mailman/mta/tests/test_base.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2014-2017 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test BaseDelivery."""
+
+import unittest
+
+from mailman.config import config
+from mailman.interfaces.configuration import InvalidConfigurationError
+from mailman.mta.base import BaseDelivery
+from mailman.mta.connection import SMTPSConnection, STARTTLSConnection
+from mailman.testing.layers import SMTPLayer, SMTPSLayer, STARTTLSLayer
+
+
+class BaseDeliveryTester(BaseDelivery):
+ @property
+ def connection(self):
+ return self._connection
+
+
+class TestSMTPSDelivery(unittest.TestCase):
+ layer = SMTPSLayer
+
+ def test_smtps_config(self):
+ config.push('smtps_config', """\
+[mta]
+smtp_protocol: smtps
+""")
+ delivery = BaseDeliveryTester()
+ self.assertIsInstance(delivery.connection, SMTPSConnection)
+ config.pop('smtps_config')
+
+
+class TestSTARTTLSDelivery(unittest.TestCase):
+ layer = STARTTLSLayer
+
+ def test_starttls_config(self):
+ config.push('starttls_config', """\
+[mta]
+smtp_protocol: starttls
+""")
+ delivery = BaseDeliveryTester()
+ self.assertIsInstance(delivery.connection, STARTTLSConnection)
+ config.pop('starttls_config')
+
+
+class TestInvalidDelivery(unittest.TestCase):
+ layer = SMTPLayer
+
+ def test_invalid_config(self):
+ config.push('invalid_config', """\
+[mta]
+smtp_protocol: invalid
+""")
+ with self.assertRaises(InvalidConfigurationError):
+ BaseDeliveryTester()
+ config.pop('invalid_config')
diff --git a/src/mailman/mta/tests/test_connection.py b/src/mailman/mta/tests/test_connection.py
index b742f7abc..566e19daa 100644
--- a/src/mailman/mta/tests/test_connection.py
+++ b/src/mailman/mta/tests/test_connection.py
@@ -20,9 +20,11 @@
import unittest
from mailman.config import config
-from mailman.mta.connection import Connection
-from mailman.testing.layers import SMTPLayer
-from smtplib import SMTP, SMTPAuthenticationError
+from mailman.mta.connection import (
+ SMTPConnection, SMTPSConnection, STARTTLSConnection)
+from mailman.testing.helpers import LogFileMark
+from mailman.testing.layers import SMTPLayer, SMTPSLayer, STARTTLSLayer
+from smtplib import SMTPAuthenticationError
class TestConnection(unittest.TestCase):
@@ -32,7 +34,7 @@ class TestConnection(unittest.TestCase):
# Logging in to the MTA with a bad user name and password produces a
# 571 Bad Authentication error.
with self.assertRaises(SMTPAuthenticationError) as cm:
- connection = Connection(
+ connection = SMTPConnection(
config.mta.smtp_host, int(config.mta.smtp_port), 0,
'baduser', 'badpass')
connection.sendmail('anne@example.com', ['bart@example.com'], """\
@@ -46,7 +48,7 @@ Subject: aardvarks
def test_authentication_good_path(self):
# Logging in with the correct user name and password succeeds.
- connection = Connection(
+ connection = SMTPConnection(
config.mta.smtp_host, int(config.mta.smtp_port), 0,
'testuser', 'testpass')
connection.sendmail('anne@example.com', ['bart@example.com'], """\
@@ -59,27 +61,31 @@ Subject: aardvarks
'AHRlc3R1c2VyAHRlc3RwYXNz')
-class TestConnectionCount(unittest.TestCase):
- layer = SMTPLayer
+class _ConnectionCounter:
+ layer = None
def setUp(self):
- self.connection = Connection(
- config.mta.smtp_host, int(config.mta.smtp_port), 0)
+ self.connection = None
self.msg_text = """\
From: anne@example.com
To: bart@example.com
Subject: aardvarks
-
"""
def test_count_0(self):
# So far, no connections.
- self.assertEqual(SMTPLayer.smtpd.get_connection_count(), 0)
+ unittest.TestCase.assertEqual(self,
+ self.layer.smtpd.get_connection_count(),
+ 0)
+ pass
def test_count_1(self):
self.connection.sendmail(
'anne@example.com', ['bart@example.com'], self.msg_text)
- self.assertEqual(SMTPLayer.smtpd.get_connection_count(), 1)
+ unittest.TestCase.assertEqual(self,
+ self.layer.smtpd.get_connection_count(),
+ 1)
+ pass
def test_count_2(self):
self.connection.sendmail(
@@ -88,7 +94,10 @@ Subject: aardvarks
self.connection.sendmail(
'cate@example.com', ['dave@example.com'], self.msg_text)
self.connection.quit()
- self.assertEqual(SMTPLayer.smtpd.get_connection_count(), 2)
+ unittest.TestCase.assertEqual(self,
+ self.layer.smtpd.get_connection_count(),
+ 2)
+ pass
def test_count_reset(self):
self.connection.sendmail(
@@ -98,7 +107,53 @@ Subject: aardvarks
'cate@example.com', ['dave@example.com'], self.msg_text)
self.connection.quit()
# Issue the fake SMTP command to reset the count.
- client = SMTP()
- client.connect(config.mta.smtp_host, int(config.mta.smtp_port))
- client.docmd('RSET')
- self.assertEqual(SMTPLayer.smtpd.get_connection_count(), 0)
+ self.layer.smtpd.reset()
+ unittest.TestCase.assertEqual(self,
+ self.layer.smtpd.get_connection_count(),
+ 0)
+
+
+class TestSMTPConnectionCount(_ConnectionCounter, unittest.TestCase):
+ layer = SMTPLayer
+
+ def setUp(self):
+ super().setUp()
+ self.connection = SMTPConnection(
+ config.mta.smtp_host, int(config.mta.smtp_port), 0)
+
+
+class TestSMTPSConnectionCount(_ConnectionCounter, unittest.TestCase):
+ layer = SMTPSLayer
+
+ def setUp(self):
+ super().setUp()
+ self.connection = SMTPSConnection(
+ config.mta.smtp_host, int(config.mta.smtp_port), 0)
+
+
+class TestSTARTTLSConnectionCount(_ConnectionCounter, unittest.TestCase):
+ layer = STARTTLSLayer
+
+ def setUp(self):
+ super().setUp()
+ self.connection = STARTTLSConnection(
+ config.mta.smtp_host, int(config.mta.smtp_port), 0)
+
+
+class TestSTARTTLSNotSupported(unittest.TestCase):
+ layer = SMTPLayer
+
+ def test_not_supported(self):
+ connection = STARTTLSConnection(
+ config.mta.smtp_host, int(config.mta.smtp_port), 0)
+ msg_text = """\
+From: anne@example.com
+To: bart@example.com
+Subject: aardvarks
+"""
+ smtp_log = LogFileMark('mailman.smtp')
+ connection.sendmail(
+ 'anne@example.com', ['bart@example.com'], msg_text)
+ lines = smtp_log.read()
+ self.assertIn('Starttls failed', lines)
+ connection.quit()
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index dbed1fccf..54ba4f5a1 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -25,6 +25,7 @@
# get rid of the layers and use something like testresources or some such.
import os
+import ssl
import sys
import shutil
import logging
@@ -40,14 +41,15 @@ from mailman.database.transaction import transaction
from mailman.interfaces.domain import IDomainManager
from mailman.testing.helpers import (
TestableMaster, get_lmtp_client, reset_the_world, wait_for_webservice)
-from mailman.testing.mta import ConnectionCountingController
+from mailman.testing.mta import (
+ ConnectionCountingController, ConnectionCountingSSLController,
+ ConnectionCountingSTARTLSController)
from mailman.utilities.string import expand
-from pkg_resources import resource_string as resource_bytes
+from pkg_resources import resource_filename, resource_string as resource_bytes
from public import public
from textwrap import dedent
from zope.component import getUtility
-
TEST_TIMEOUT = datetime.timedelta(seconds=5)
NL = '\n'
@@ -259,6 +261,116 @@ class SMTPLayer(ConfigLayer):
@public
+class SMTPSLayer(ConfigLayer):
+ """Layer for starting, stopping, and accessing a test SMTPS server."""
+
+ smtpd = None
+
+ @classmethod
+ def setUp(cls):
+ assert cls.smtpd is None, 'Layer already set up'
+ # Use a different port than the SMTP layer, since that one might
+ # still be in use.
+ config.push('smtps', """
+ [mta]
+ smtp_port: 9026
+ smtp_protocol: smtps
+ """)
+ test_cert_path = resource_filename('mailman.testing',
+ 'ssl_test_cert.crt')
+ test_key_path = resource_filename('mailman.testing',
+ 'ssl_test_key.key')
+
+ client_context = ssl.create_default_context()
+ client_context.load_verify_locations(cafile=test_cert_path)
+
+ server_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ server_context.load_cert_chain(test_cert_path, test_key_path)
+
+ host = config.mta.smtp_host
+ port = int(config.mta.smtp_port)
+
+ cls.smtpd = ConnectionCountingSSLController(
+ host, port,
+ client_context=client_context,
+ server_context=server_context)
+ cls.smtpd.start()
+
+ @classmethod
+ def testSetUp(cls):
+ # Make sure we don't call our superclass's testSetUp(), otherwise the
+ # example.com domain will get added twice.
+ pass
+
+ @classmethod
+ def testTearDown(cls):
+ cls.smtpd.reset()
+ cls.smtpd.clear()
+
+ @classmethod
+ def tearDown(cls):
+ assert cls.smtpd is not None, 'Layer not set up'
+ cls.smtpd.clear()
+ cls.smtpd.stop()
+ config.pop('smtps')
+
+
+@public
+class STARTTLSLayer(ConfigLayer):
+ """Layer for starting and stopping a test SMTP server with STARTTLS."""
+
+ smtpd = None
+
+ @classmethod
+ def setUp(cls):
+ assert cls.smtpd is None, 'Layer already set up'
+ # Use a different port than the SMTP and SMTPS layers, since that one
+ # might still be in use.
+ config.push('starttls', """
+ [mta]
+ smtp_port: 9027
+ smtp_protocol: starttls
+ """)
+ test_cert_path = resource_filename('mailman.testing',
+ 'ssl_test_cert.crt')
+ test_key_path = resource_filename('mailman.testing',
+ 'ssl_test_key.key')
+
+ client_context = ssl.create_default_context()
+ client_context.load_verify_locations(cafile=test_cert_path)
+
+ server_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ server_context.load_cert_chain(test_cert_path, test_key_path)
+
+ host = config.mta.smtp_host
+ port = int(config.mta.smtp_port)
+
+ cls.smtpd = ConnectionCountingSTARTLSController(
+ host, port,
+ client_context=client_context,
+ server_context=server_context)
+ cls.smtpd.start()
+
+ @classmethod
+ def testSetUp(cls):
+ # Make sure we don't call our superclass's testSetUp(), otherwise the
+ # example.com domain will get added twice.
+ pass
+
+ @classmethod
+ def testTearDown(cls):
+ cls.smtpd.reset()
+ cls.smtpd.clear()
+
+ @classmethod
+ def tearDown(cls):
+ assert cls.smtpd is not None, 'Layer not set up'
+ cls.smtpd.clear()
+ cls.smtpd.stop()
+ config.pop('starttls')
+
+
+@public
class LMTPLayer(ConfigLayer):
"""Layer for starting, stopping, and accessing a test LMTP server."""
diff --git a/src/mailman/testing/mta.py b/src/mailman/testing/mta.py
index ef4d39233..02a3b83d2 100644
--- a/src/mailman/testing/mta.py
+++ b/src/mailman/testing/mta.py
@@ -166,12 +166,14 @@ class ConnectionCountingSMTP(SMTP):
class ConnectionCountingController(Controller):
"""Count the number of SMTP connections opened."""
- def __init__(self, host, port):
+ def __init__(self, host, port, ssl_context=None):
self._msg_queue = Queue()
self._oob_queue = Queue()
self.err_queue = Queue()
+
handler = ConnectionCountingHandler(self._msg_queue)
- super().__init__(handler, hostname=host, port=port)
+ super().__init__(handler, hostname=host, port=port,
+ ssl_context=ssl_context)
def factory(self):
return ConnectionCountingSMTP(
@@ -184,8 +186,7 @@ class ConnectionCountingController(Controller):
self.reset()
def _connect(self):
- client = smtplib.SMTP()
- client.connect(self.hostname, self.port)
+ client = smtplib.SMTP(self.hostname, self.port)
return client
def get_connection_count(self):
@@ -223,3 +224,47 @@ class ConnectionCountingController(Controller):
def reset(self):
client = self._connect()
client.docmd('RSET')
+
+
+class ConnectionCountingSSLController(ConnectionCountingController):
+ """Count the number of SMTPS connections opened."""
+
+ def __init__(self, host, port, client_context=None, server_context=None):
+ super().__init__(host, port, ssl_context=server_context)
+ self._client_context = client_context
+
+ def _connect(self):
+ client = smtplib.SMTP_SSL(self.hostname, self.port,
+ context=self._client_context)
+ return client
+
+
+class ConnectionCountingSTARTLSController(ConnectionCountingController):
+ """Count the number of SMTP connections with STARTTLS opened."""
+
+ def __init__(self, host, port, client_context=None, server_context=None):
+ super().__init__(host, port)
+ self._client_context = client_context
+ self._server_context = server_context
+
+ def factory(self):
+ return ConnectionCountingSMTP(
+ self.handler, self._oob_queue,
+ self.err_queue, tls_context=self._server_context,
+ require_starttls=True)
+
+ def _connect(self):
+ client = smtplib.SMTP(self.hostname, self.port)
+ client.starttls(context=self._client_context)
+ return client
+
+ def get_connection_count(self):
+ count = super().get_connection_count()
+ # This is a hack, since when using STARTTLS for every connection the
+ # SMTP.connection_made(transport) method is called twice.
+ # The -1 is there for the last pair of connections made when checking
+ # the connection count, since one of them is already subtracted in
+ # ConnectionCountingSMTP.handle_STAT.
+ # The //2 is there for the rest of the connections which are counted
+ # twice.
+ return (count - 1) // 2
diff --git a/src/mailman/testing/ssl_test_cert.crt b/src/mailman/testing/ssl_test_cert.crt
new file mode 100644
index 000000000..06ae8ac9d
--- /dev/null
+++ b/src/mailman/testing/ssl_test_cert.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDeDCCAmCgAwIBAgIJANA3MRKsRL/OMA0GCSqGSIb3DQEBCwUAMFExCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJNQTEhMB8GA1UECgwYTWFpbG1hbiB0ZXN0IGNlcnRp
+ZmljYXRlMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTcwNTI3MTYxNzIzWhcNMjcw
+NTI1MTYxNzIzWjBRMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExITAfBgNVBAoM
+GE1haWxtYW4gdGVzdCBjZXJ0aWZpY2F0ZTESMBAGA1UEAwwJbG9jYWxob3N0MIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAycCl/y9Cpm0jW921eIraIieh
+cxXPrYed66SxZwoZNt8StaFfNOeqykIiFpUR/5KYc1Sk0yF2tGfFv6l17xG0fuCI
+OkH1EynhXZiPbdcvmqF2W6Fa1g5kZJ//KbhK4rRsUEVp/2pHVjMG6toenM+S/1LE
+35XiNyIHF6ZeENAb/zR3ikjYH0MSEq2doBmKd9/m48+z5Vb+MbyIG7jbvN3MQ9f/
+NXAIwcGf37RO5+yNIcvqTV9SMBAZb1l+rMrsks6Ghr3NoBP1kuqJF5vFropKBx7R
+1MKvYm2ISL5UaWbFPYs9A4VhtzhMWBLZGHQPFHnR7dub7et8eiC7fbsdx4uyGwID
+AQABo1MwUTAdBgNVHQ4EFgQUe1Q+FbXTQjLoeTuSlxQQCeE+jpQwHwYDVR0jBBgw
+FoAUe1Q+FbXTQjLoeTuSlxQQCeE+jpQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
+9w0BAQsFAAOCAQEANbliLrarZfinHQ+/TQIOdqolsscw7RtyFtB7wA2uqWNsH/77
+EYDiv8AhpAQB3dzKwGdapgYgsWUe2awSD67TAJg5XS/h9F1PbbjDYGC9v6T7HDPd
+SfGV34LbWmH60rE6B1lwyCACnOXhByFGrdicCm2Z073kTp5L7WuAqpwC1f3aMC0u
+9goUWHSfqSIPPIv8yiV0vgKk4kP4KV/gi20x74c31pujExCiVGA4GGTXV9hqmAm7
+tZuPTYFN5n60U1tzbFXOuwK6CvCDzWeq27mYwNHTXSJw1GyG4XXM5t2wfSlu/4e4
+xDuZwvP0P7hTME5wC7KGeS7daYSWNCDiHa7g6A==
+-----END CERTIFICATE-----
diff --git a/src/mailman/testing/ssl_test_key.key b/src/mailman/testing/ssl_test_key.key
new file mode 100644
index 000000000..619556893
--- /dev/null
+++ b/src/mailman/testing/ssl_test_key.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJwKX/L0KmbSNb
+3bV4itoiJ6FzFc+th53rpLFnChk23xK1oV8056rKQiIWlRH/kphzVKTTIXa0Z8W/
+qXXvEbR+4Ig6QfUTKeFdmI9t1y+aoXZboVrWDmRkn/8puEritGxQRWn/akdWMwbq
+2h6cz5L/UsTfleI3IgcXpl4Q0Bv/NHeKSNgfQxISrZ2gGYp33+bjz7PlVv4xvIgb
+uNu83cxD1/81cAjBwZ/ftE7n7I0hy+pNX1IwEBlvWX6syuySzoaGvc2gE/WS6okX
+m8WuikoHHtHUwq9ibYhIvlRpZsU9iz0DhWG3OExYEtkYdA8UedHt25vt63x6ILt9
+ux3Hi7IbAgMBAAECggEATRjFVmLlAVwrauuqcUn+WZbzZ1sqZZGxk174O/vr7sAI
+Ekh8bWcqKOhkxmRo4FVQ1KG/6r6a8g3Fz5weaSFG7EU5Sany0UPrzyyBguP8WQbi
+h9l9MNeHHbzWcUbvtvpjeblM7EHcyN/vAMghcqMP9WnXuek47QCf3TXCNIKScE8a
+sU01o7Lw4CkZOLP9UyLmoKiQpurj7jWOMq10QOG4693dnfcgz7voPZPrt8OnAo7x
+ap4ojK9nWfjFHFxpsddBhXBxzXw/U8FxtzYtCbzxL7iYpxpq0GEc/5eF8O+RE4MX
+D1uqrkLfVM4HMOgQoFLrSGcPahYSVRrQ3X+lYV+OQQKBgQDotmodstq4T3D+7buY
+J3zi+saEDvcPxgy31934jm465WfNiYSrnq5PqJCkzVpYMPuj0O8za75OoyRK2viT
+glBKjP0vxNXh91tTi4eU7LCFPNwOwnt+e3tu+eA4klFpQIEUI8PHlEe4VBGlvpsD
+DoPHuIHCouAEXhpu/ck58+QxKQKBgQDd8Rshc7h8PY26Rxrzgjob9PvDHxQODodU
+iRUAELscSkr4gwYTgmDL4vL+lOog7IAYGmSOsHLJ19wTbLsaPMPfhUh+zXuvwlA5
+/01uYFp0UM4ax4DdTGPA5qOAIkLY3rL1x6U0eBf6XJhdG8mTW9SjbD1aCbDGdZk6
+9KJVUlDdowKBgD4NHeCLZ1zL+gJP27ynktpnKfXek6xGD/AZhFuZhvT3ZKVerNyi
+NDKTbPY0t4lajk7REGcyrI0FXVEEcFHM5qHqVDyfjLRzI4v0YZOpRSxR3Q+mdg10
+2aXuxQXwpfqds41uN+8Ir9MLv6TlXSoEfckMfrUqfvdLLFs6GqT0Tn15AoGBANG0
+05HUKekasCPms8yKrCVmYcyIPQbbK3vw2uro5CNi/1u5UbB1bMi5dCigxGi/jnk3
+1vQMPSoC0Gt6PYAZEmrNISbPOaNk0zE5zgwQ9ucYwuYCw/xWBZtrUensdYU9R5N8
+RNlC8EUb4Mt5Sgn2pwCTcZT1uxaKX3KZXBmKYeZJAoGAAtWlzPxgwDglNhhuLpgW
+vSOH0lpHlR//Ne7kMPCfG4lrAaPo5VSfQrjlzoF9tlhu0IuRKnwOg4HmFD9COaLA
+RWTNDLMxBct5lZNAjgIzI5nZm6VLioCQkAdThzSBTxpGrIR0iDR99RWf/M4p7cK6
+tiqX285r1nogUpROl0uraV4=
+-----END PRIVATE KEY-----