summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/mta/connection.py93
-rw-r--r--src/mailman/mta/docs/connection.txt16
-rw-r--r--src/mailman/mta/smtp_direct.py43
-rw-r--r--src/mailman/testing/layers.py18
-rw-r--r--src/mailman/testing/mta.py103
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)