diff options
| -rw-r--r-- | src/mailman/mta/connection.py | 93 | ||||
| -rw-r--r-- | src/mailman/mta/docs/connection.txt | 16 | ||||
| -rw-r--r-- | src/mailman/mta/smtp_direct.py | 43 | ||||
| -rw-r--r-- | src/mailman/testing/layers.py | 18 | ||||
| -rw-r--r-- | src/mailman/testing/mta.py | 103 |
5 files changed, 214 insertions, 59 deletions
diff --git a/src/mailman/mta/connection.py b/src/mailman/mta/connection.py new file mode 100644 index 000000000..5c9ba7474 --- /dev/null +++ b/src/mailman/mta/connection.py @@ -0,0 +1,93 @@ +# Copyright (C) 2009 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/>. + +"""MTA connections.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'Connection', + ] + + +import logging +import smtplib + +from mailman.config import config + + +log = logging.getLogger('mailman.smtp') + + + +class Connection: + """Manage a connection to the SMTP server.""" + def __init__(self, host, port, sessions_per_connection): + """Create a connection manager. + + :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 + """ + self._host = host + self._port = port + self._connections_per_session = sessions_per_connection + 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 sendmail(self, envsender, recips, msgtext): + """Mimic `smtplib.SMTP.sendmail`.""" + if self._connection is None: + self._connect() + try: + results = self._connection.sendmail(envsender, recips, msgtext) + except smtplib.SMTPException: + # For safety, close this connection. The next send attempt will + # automatically re-open it. Pass the exception on up. + self.quit() + raise + # This session has been successfully completed. + self._sessions_per_connection -= 1 + # By testing exactly for equality to 0, we automatically handle the + # case for SMTP_MAX_SESSIONS_PER_CONNECTION <= 0 meaning never close + # the connection. We won't worry about wraparound <wink>. + if self._sessions_per_connection == 0: + self.quit() + return results + + def quit(self): + """Mimic `smtplib.SMTP.quit`.""" + if self._connection is None: + return + try: + self._connection.quit() + except smtplib.SMTPException: + pass + self._connection = None diff --git a/src/mailman/mta/docs/connection.txt b/src/mailman/mta/docs/connection.txt new file mode 100644 index 000000000..d9d1ad1d0 --- /dev/null +++ b/src/mailman/mta/docs/connection.txt @@ -0,0 +1,16 @@ +=============== +MTA connections +=============== + +Outgoing connections to the outgoing mail transport agent (MTA) are mitigated +through a Connection class, which can transparently manage multiple sessions +to a single physical MTA. + + >>> from mailman.mta.connection import Connection + +The number of connections per session is specified when the Connection object +is created, as is the host and port number of the SMTP server. + + >>> connection = Connection( + ... config.mta.smtp_host, int(config.mta.smtp_port), 1) + diff --git a/src/mailman/mta/smtp_direct.py b/src/mailman/mta/smtp_direct.py index b1011c904..419d4ce96 100644 --- a/src/mailman/mta/smtp_direct.py +++ b/src/mailman/mta/smtp_direct.py @@ -60,49 +60,6 @@ log = logging.getLogger('mailman.smtp') -# Manage a connection to the SMTP server -class Connection: - def __init__(self): - self._conn = None - - def _connect(self): - self._conn = smtplib.SMTP() - host = config.mta.smtp_host - port = int(config.mta.smtp_port) - log.debug('Connecting to %s:%s', host, port) - self._conn.connect(host, port) - self._numsessions = int(config.mta.max_sessions_per_connection) - - def sendmail(self, envsender, recips, msgtext): - if self._conn is None: - self._connect() - try: - results = self._conn.sendmail(envsender, recips, msgtext) - except smtplib.SMTPException: - # For safety, close this connection. The next send attempt will - # automatically re-open it. Pass the exception on up. - self.quit() - raise - # This session has been successfully completed. - self._numsessions -= 1 - # By testing exactly for equality to 0, we automatically handle the - # case for SMTP_MAX_SESSIONS_PER_CONNECTION <= 0 meaning never close - # the connection. We won't worry about wraparound <wink>. - if self._numsessions == 0: - self.quit() - return results - - def quit(self): - if self._conn is None: - return - try: - self._conn.quit() - except smtplib.SMTPException: - pass - self._conn = None - - - def process(mlist, msg, msgdata): recips = msgdata.get('recips') if not recips: diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py index d4db9ebf2..034e26f83 100644 --- a/src/mailman/testing/layers.py +++ b/src/mailman/testing/layers.py @@ -35,7 +35,6 @@ import logging import datetime import tempfile -from lazr.smtptest.controller import QueueController from pkg_resources import resource_string from textwrap import dedent from urllib2 import urlopen, URLError @@ -48,6 +47,7 @@ from mailman.i18n import _ from mailman.interfaces.domain import IDomainManager from mailman.interfaces.messages import IMessageStore from mailman.testing.helpers import TestableMaster +from mailman.testing.mta import SessionCountingController from mailman.utilities.datetime import factory from mailman.utilities.string import expand @@ -210,20 +210,6 @@ class ConfigLayer(MockAndMonkeyLayer): -class ExtendedQueueController(QueueController): - """QueueController with a little extra API.""" - - @property - def messages(self): - """Return all the messages received by the SMTP server.""" - for message in self: - yield message - - def clear(self): - """Clear all the messages from the queue.""" - list(self) - - class SMTPLayer(ConfigLayer): """Layer for starting, stopping, and accessing a test SMTP server.""" @@ -234,7 +220,7 @@ class SMTPLayer(ConfigLayer): assert cls.smtpd is None, 'Layer already set up' host = config.mta.smtp_host port = int(config.mta.smtp_port) - cls.smtpd = ExtendedQueueController(host, port) + cls.smtpd = SessionCountingController(host, port) cls.smtpd.start() @classmethod diff --git a/src/mailman/testing/mta.py b/src/mailman/testing/mta.py index a10ba3c81..e89cc7cfb 100644 --- a/src/mailman/testing/mta.py +++ b/src/mailman/testing/mta.py @@ -25,11 +25,20 @@ __all__ = [ ] +import logging + +from Queue import Queue + +from lazr.smtptest.controller import QueueController +from lazr.smtptest.server import Channel, QueueServer from zope.interface import implements from mailman.interfaces.mta import IMailTransportAgent +log = logging.getLogger('lazr.smtptest') + + class FakeMTA: """Fake MTA for testing purposes.""" @@ -44,3 +53,97 @@ class FakeMTA: def regenerate(self): pass + + + +class SessionCountingChannel(Channel): + """Count the number of SMTP sessions opened and closed.""" + + def smtp_HELO(self, arg): + """See `smtpd.SMTPChannel.smtp_HELO`.""" + # Store this on the server because while the channel has access to the + # server, the server does not have access to the individual channels. + self._server.helo_count += 1 + Channel.smtp_HELO(self, arg) + + def smtp_QUIT(self, arg): + """See `smtpd.SMTPChannel.smtp_QUIT`.""" + # Store this on the server because while the channel has access to the + # server, the server does not have access to the individual channels. + self._server.quit_count += 1 + Channel.smtp_QUIT(self, arg) + + def smtp_STAT(self, arg): + """Cause the server to send statistics to its controller.""" + self._server.send_statistics() + self.push('250 Ok') + + + +class SessionCountingServer(QueueServer): + """Count the number of SMTP sessions opened and closed.""" + + def __init__(self, host, port, queue, oob_queue): + """See `lazr.smtptest.server.QueueServer`.""" + QueueServer.__init__(self, host, port, queue) + # Store these on the server because while the channel has access to + # the server, the server does not have access to the individual + # channels. + self.helo_count = 0 + self.quit_count = 0 + # The out-of-band queue is where the server sends statistics to the + # controller upon request. + self._oob_queue = oob_queue + + def handle_accept(self): + """See `lazr.smtp.server.Server`.""" + connection, address = self.accept() + log.info('[SessionCountingServer] accepted: %s', address) + SessionCountingChannel(self, connection, address) + + def reset(self): + """See `lazr.smtp.server.Server`.""" + QueueServer.reset(self) + self.helo_count = 0 + self.quit_count = 0 + + def send_statistics(self): + """Send the current connection statistics to the controller.""" + self._oob_queue.put((self.helo_count, self.quit_count)) + + + +class SessionCountingController(QueueController): + """Count the number of SMTP sessions opened and closed.""" + + def __init__(self, host, port): + """See `lazr.smtptest.controller.QueueController`.""" + self.oob_queue = Queue() + QueueController.__init__(self, host, port) + + def _make_server(self, host, port): + """See `lazr.smtptest.controller.QueueController`.""" + self.server = SessionCountingServer( + host, port, self.queue, self.oob_queue) + + def get_statistics(self): + """Retrieve connection statistics from the server. + + :return: a 2-tuple of the format (HELO count, QUIT count) + :rtype 2-tuple of integers + """ + smtpd = self._connect() + smtpd.docmd('STAT') + # An Empty exception will occur if the data isn't available in 10 + # seconds. Let that propagate. + return self.queue.get(block=True, timeout=10) + + @property + def messages(self): + """Return all the messages received by the SMTP server.""" + for message in self: + yield message + + def clear(self): + """Clear all the messages from the queue.""" + list(self) |
