diff options
| author | Barry Warsaw | 2011-01-02 23:52:22 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2011-01-02 23:52:22 -0500 |
| commit | 5d0d6a5afa34c61630a6442006e9ff2b87fb0c8d (patch) | |
| tree | 6296162e7f68e2af68f7ed8ab9ccc7f1394ec084 /src | |
| parent | 1b8c94f4ad4730b3251c9efd667db27245105b6c (diff) | |
| download | mailman-5d0d6a5afa34c61630a6442006e9ff2b87fb0c8d.tar.gz mailman-5d0d6a5afa34c61630a6442006e9ff2b87fb0c8d.tar.zst mailman-5d0d6a5afa34c61630a6442006e9ff2b87fb0c8d.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/config/schema.cfg | 5 | ||||
| -rw-r--r-- | src/mailman/mta/base.py | 5 | ||||
| -rw-r--r-- | src/mailman/mta/connection.py | 13 | ||||
| -rw-r--r-- | src/mailman/mta/docs/authentication.txt | 56 | ||||
| -rw-r--r-- | src/mailman/mta/docs/connection.txt | 50 | ||||
| -rw-r--r-- | src/mailman/testing/layers.py | 1 | ||||
| -rw-r--r-- | src/mailman/testing/mta.py | 33 |
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') |
