summaryrefslogtreecommitdiff
path: root/src/mailman/mta/connection.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/mta/connection.py')
-rw-r--r--src/mailman/mta/connection.py103
1 files changed, 92 insertions, 11 deletions
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))