summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2011-01-02 23:52:22 -0500
committerBarry Warsaw2011-01-02 23:52:22 -0500
commit5d0d6a5afa34c61630a6442006e9ff2b87fb0c8d (patch)
tree6296162e7f68e2af68f7ed8ab9ccc7f1394ec084
parent1b8c94f4ad4730b3251c9efd667db27245105b6c (diff)
downloadmailman-5d0d6a5afa34c61630a6442006e9ff2b87fb0c8d.tar.gz
mailman-5d0d6a5afa34c61630a6442006e9ff2b87fb0c8d.tar.zst
mailman-5d0d6a5afa34c61630a6442006e9ff2b87fb0c8d.zip
-rw-r--r--src/mailman/config/schema.cfg5
-rw-r--r--src/mailman/mta/base.py5
-rw-r--r--src/mailman/mta/connection.py13
-rw-r--r--src/mailman/mta/docs/authentication.txt56
-rw-r--r--src/mailman/mta/docs/connection.txt50
-rw-r--r--src/mailman/testing/layers.py1
-rw-r--r--src/mailman/testing/mta.py33
7 files changed, 160 insertions, 3 deletions
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index 0f2c9e107..f789d28f9 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -341,9 +341,12 @@ incoming: mailman.mta.postfix.LMTP
# message metadata dictionary.
outgoing: mailman.mta.deliver.deliver
-# How to connect to the outgoing MTA.
+# How to connect to the outgoing MTA. If smtp_user and smtp_pass is given,
+# then Mailman will attempt to log into the MTA when making a new connection.
smtp_host: localhost
smtp_port: 25
+smtp_user:
+smtp_pass:
# Where the LMTP server listens for connections. Use 127.0.0.1 instead of
# localhost for Postfix integration, because Postfix only consults DNS
diff --git a/src/mailman/mta/base.py b/src/mailman/mta/base.py
index d1033eb87..e90fbcf8f 100644
--- a/src/mailman/mta/base.py
+++ b/src/mailman/mta/base.py
@@ -49,9 +49,12 @@ class BaseDelivery:
def __init__(self):
"""Create a basic deliverer."""
+ 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))
+ int(config.mta.max_sessions_per_connection),
+ username, password)
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 e832c2447..369e43570 100644
--- a/src/mailman/mta/connection.py
+++ b/src/mailman/mta/connection.py
@@ -38,7 +38,8 @@ log = logging.getLogger('mailman.smtp')
class Connection:
"""Manage a connection to the SMTP server."""
- def __init__(self, host, port, sessions_per_connection):
+ def __init__(self, host, port, sessions_per_connection,
+ smtp_user=None, smtp_pass=None):
"""Create a connection manager.
:param host: The host name of the SMTP server to connect to.
@@ -51,10 +52,17 @@ class Connection:
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 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.
"""
self._host = host
self._port = port
self._sessions_per_connection = sessions_per_connection
+ self._username = smtp_user
+ self._password = smtp_pass
self._session_count = None
self._connection = None
@@ -63,6 +71,9 @@ class Connection:
self._connection = smtplib.SMTP()
log.debug('Connecting to %s:%s', self._host, self._port)
self._connection.connect(self._host, self._port)
+ 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):
diff --git a/src/mailman/mta/docs/authentication.txt b/src/mailman/mta/docs/authentication.txt
new file mode 100644
index 000000000..9f494be61
--- /dev/null
+++ b/src/mailman/mta/docs/authentication.txt
@@ -0,0 +1,56 @@
+===================
+SMTP authentication
+===================
+
+The SMTP server may require authentication. Mailman supports setting the SMTP
+user name and password. When the user name and password match what's expected
+by the server, everything is a-okay.
+
+ >>> mlist = create_list('test@example.com')
+
+By default there is no user name and password, but this matches what's
+expected by the test server.
+
+ >>> config.push('auth', """
+ ... [mta]
+ ... smtp_user: testuser
+ ... smtp_pass: testpass
+ ... """)
+
+Attempting delivery first must authorize with the mail server.
+::
+
+ >>> from mailman.mta.bulk import BulkDelivery
+ >>> bulk = BulkDelivery()
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: test@example.com
+ ... Subject: My first post
+ ... Message-ID: <first>
+ ...
+ ... First post!
+ ... """)
+
+ >>> bulk.deliver(mlist, msg, dict(recipients=['bperson@example.com']))
+ {}
+
+ >>> print smtpd.get_authentication_credentials()
+ PLAIN AHRlc3R1c2VyAHRlc3RwYXNz
+ >>> config.pop('auth')
+
+But if the user name and password does not match, the connection will fail.
+
+ >>> config.push('auth', """
+ ... [mta]
+ ... smtp_user: baduser
+ ... smtp_pass: badpass
+ ... """)
+
+ >>> bulk = BulkDelivery()
+ >>> response = bulk.deliver(
+ ... mlist, msg, dict(recipients=['bperson@example.com']))
+ >>> dump_msgdata(response)
+ bperson@example.com: (571, 'Bad authentication')
+
+ >>> config.pop('auth')
diff --git a/src/mailman/mta/docs/connection.txt b/src/mailman/mta/docs/connection.txt
index 7da16a771..515a773bd 100644
--- a/src/mailman/mta/docs/connection.txt
+++ b/src/mailman/mta/docs/connection.txt
@@ -49,6 +49,56 @@ We can reset the connection count back to zero.
>>> connection.quit()
+By providing an SMTP user name and password in the configuration file, Mailman
+will authenticate with the mail server after each new connection.
+::
+
+ >>> config.push('auth', """
+ ... [mta]
+ ... smtp_user: testuser
+ ... smtp_pass: testpass
+ ... """)
+
+ >>> connection = Connection(
+ ... 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'], """\
+ ... From: anne@example.com
+ ... To: bart@example.com
+ ... Subject: aardvarks
+ ...
+ ... """)
+ {}
+ >>> print smtpd.get_authentication_credentials()
+ PLAIN AHRlc3R1c2VyAHRlc3RwYXNz
+
+ >>> reset()
+ >>> config.pop('auth')
+
+However, a bad user name or password generates an error.
+
+ >>> config.push('auth', """
+ ... [mta]
+ ... smtp_user: baduser
+ ... smtp_pass: badpass
+ ... """)
+
+ >>> connection = Connection(
+ ... 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'], """\
+ ... From: anne@example.com
+ ... To: bart@example.com
+ ... Subject: aardvarks
+ ...
+ ... """)
+ Traceback (most recent call last):
+ ...
+ SMTPAuthenticationError: (571, 'Bad authentication')
+
+ >>> reset()
+ >>> config.pop('auth')
+
Sessions per connection
=======================
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index a16dbebc8..319248ebb 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -259,6 +259,7 @@ class SMTPLayer(ConfigLayer):
@classmethod
def testTearDown(cls):
+ cls.smtpd.reset()
cls.smtpd.clear()
diff --git a/src/mailman/testing/mta.py b/src/mailman/testing/mta.py
index f44143972..8fae233fa 100644
--- a/src/mailman/testing/mta.py
+++ b/src/mailman/testing/mta.py
@@ -59,11 +59,31 @@ class FakeMTA:
class StatisticsChannel(Channel):
"""A channel that can answers to the fake STAT command."""
+ def smtp_EHLO(self, arg):
+ if not arg:
+ self.push(b'501 Syntax: HELO hostname')
+ return
+ if self._SMTPChannel__greeting:
+ self.push(b'503 Duplicate HELO/EHLO')
+ else:
+ self._SMTPChannel__greeting = arg
+ self.push(b'250-%s' % self._SMTPChannel__fqdn)
+ self.push(b'250 AUTH PLAIN')
+
def smtp_STAT(self, arg):
"""Cause the server to send statistics to its controller."""
self._server.send_statistics()
self.push(b'250 Ok')
+ def smtp_AUTH(self, arg):
+ """Record that the AUTH occurred."""
+ if arg == 'PLAIN AHRlc3R1c2VyAHRlc3RwYXNz':
+ # testuser:testpass
+ self.push(b'235 Ok')
+ self._server.send_auth(arg)
+ else:
+ self.push(b'571 Bad authentication')
+
def smtp_RCPT(self, arg):
"""For testing, sometimes cause a non-25x response."""
code = self._server.next_error('rcpt')
@@ -103,6 +123,7 @@ class ConnectionCountingServer(QueueServer):
"""
QueueServer.__init__(self, host, port, queue)
self._connection_count = 0
+ self.last_auth = None
# The out-of-band queue is where the server sends statistics to the
# controller upon request.
self._oob_queue = oob_queue
@@ -152,6 +173,10 @@ class ConnectionCountingServer(QueueServer):
self._connection_count -= 1
self._oob_queue.put(self._connection_count)
+ def send_auth(self, arg):
+ """Echo back the authentication data."""
+ self._oob_queue.put(arg)
+
class ConnectionCountingController(QueueController):
@@ -187,6 +212,10 @@ class ConnectionCountingController(QueueController):
# seconds. Let that propagate.
return self.oob_queue.get(block=True, timeout=10)
+ def get_authentication_credentials(self):
+ """Retrieve the last authentication credentials."""
+ return self.oob_queue.get(block=True, timeout=10)
+
@property
def messages(self):
"""Return all the messages received by the SMTP server."""
@@ -196,3 +225,7 @@ class ConnectionCountingController(QueueController):
def clear(self):
"""Clear all the messages from the queue."""
list(self)
+
+ def reset(self):
+ smtpd = self._connect()
+ smtpd.docmd(b'RSET')