diff options
| -rw-r--r-- | src/mailman/app/bounces.py | 106 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_bounces.py | 49 | ||||
| -rw-r--r-- | src/mailman/queue/bounce.py | 4 | ||||
| -rw-r--r-- | src/mailman/utilities/uid.py | 30 |
4 files changed, 120 insertions, 69 deletions
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py index 8d2526a66..8c42ede46 100644 --- a/src/mailman/app/bounces.py +++ b/src/mailman/app/bounces.py @@ -21,8 +21,8 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'StandardVERP', 'bounce_message', - 'get_verp', 'scan_message', ] @@ -39,6 +39,7 @@ from mailman.config import config from mailman.core.i18n import _ from mailman.email.message import UserNotification from mailman.interfaces.bounce import IBounceDetector +from mailman.interfaces.pending import IPendings from mailman.utilities.email import split_email from mailman.utilities.string import oneline @@ -103,46 +104,75 @@ def scan_message(mlist, msg): -def get_verp(mlist, msg): - """Extract a set of VERP bounce addresses. +class _BaseVERPParser: + """Base class for parsing VERP messages. Sadly not every MTA bounces VERP messages correctly, or consistently. First, the To: header is checked, then Delivered-To: (Postfix), Envelope-To: (Exim) and Apparently-To:. Note that there can be multiple - Delivered-To: headers so we need to search them all (and we don't worry - about false positives for forwarded email, because only one should match - `verp_regexp`). - - :param mlist: The mailing list being checked. - :type mlist: `IMailingList` - :param msg: The message being parsed. - :type msg: `email.message.Message` - :return: The set of addresses extracted from the VERP headers. - :rtype: set of strings + headers so we need to search them all """ - cre = re.compile(config.mta.verp_regexp, re.IGNORECASE) - blocal, bdomain = split_email(mlist.bounces_address) - values = set() - verp_matches = set() - for header in ('to', 'delivered-to', 'envelope-to', 'apparently-to'): - values.update(msg.get_all(header, [])) - for field in values: - address = parseaddr(field)[1] - if not address: - # This header was empty. - continue - mo = cre.search(address) - if not mo: - # This did not match the VERP regexp. - continue - try: - if blocal != mo.group('bounces'): - # This was not a bounce to our mailing list. + + def __init__(self, pattern): + self._pattern = pattern + self._cre = re.compile(pattern, re.IGNORECASE) + + def _get_addresses(self, match_object): + raise NotImplementedError + + def get_verp(self, mlist, msg): + """Extract a set of VERP bounce addresses. + + + :param mlist: The mailing list being checked. + :type mlist: `IMailingList` + :param msg: The message being parsed. + :type msg: `email.message.Message` + :return: The set of addresses extracted from the VERP headers. + :rtype: set of strings + """ + blocal, bdomain = split_email(mlist.bounces_address) + values = set() + verp_matches = set() + for header in ('to', 'delivered-to', 'envelope-to', 'apparently-to'): + values.update(msg.get_all(header, [])) + for field in values: + address = parseaddr(field)[1] + if not address: + # This header was empty. continue - original_address = '{0}@{1}'.format(*mo.group('local', 'domain')) - except IndexError: - elog.error("verp_regexp doesn't yield the right match groups") - return set() - else: - verp_matches.add(original_address) - return verp_matches + mo = self._cre.search(address) + if not mo: + # This did not match the VERP regexp. + continue + try: + if blocal != mo.group('bounces'): + # This was not a bounce to our mailing list. + continue + original_address = self._get_address(mo) + except IndexError: + elog.error('Bad VERP pattern: {0}'.format(self._pattern)) + return set() + else: + verp_matches.add(original_address) + return verp_matches + + + +class StandardVERP(_BaseVERPParser): + def __init__(self): + super(StandardVERP, self).__init__(config.mta.verp_regexp) + + def _get_address(self, match_object): + return '{0}@{1}'.format(*match_object.group('local', 'domain')) + + +class ProbeVERP(_BaseVERPParser): + def __init__(self): + super(ProbeVERP, self).__init__(config.mta.verp_probe_regexp) + + def _get_address(self, match_object): + # Extract the token and get the matching address. + token = match_object.group('token') + op, address, bmsg = getUtility(IPendings).confirm(token) + return address diff --git a/src/mailman/app/tests/test_bounces.py b/src/mailman/app/tests/test_bounces.py index c6a9394ad..024868056 100644 --- a/src/mailman/app/tests/test_bounces.py +++ b/src/mailman/app/tests/test_bounces.py @@ -27,7 +27,7 @@ __all__ = [ import unittest -from mailman.app.bounces import get_verp +from mailman.app.bounces import StandardVERP from mailman.app.lifecycle import create_list from mailman.testing.helpers import ( specialized_message_from_string as message_from_string) @@ -36,10 +36,13 @@ from mailman.testing.layers import ConfigLayer class TestVERP(unittest.TestCase): + """Test header VERP detection.""" + layer = ConfigLayer def setUp(self): self._mlist = create_list('test@example.com') + self._verper = StandardVERP() def test_no_verp(self): # The empty set is returned when there is no VERP headers. @@ -48,7 +51,7 @@ From: postmaster@example.com To: mailman-bounces@example.com """) - self.assertEqual(get_verp(self._mlist, msg), set()) + self.assertEqual(self._verper.get_verp(self._mlist, msg), set()) def test_verp_in_to(self): # A VERP address is found in the To header. @@ -57,7 +60,8 @@ From: postmaster@example.com To: test-bounces+anne=example.org@example.com """) - self.assertEqual(get_verp(self._mlist, msg), set(['anne@example.org'])) + self.assertEqual(self._verper.get_verp(self._mlist, msg), + set(['anne@example.org'])) def test_verp_in_delivered_to(self): # A VERP address is found in the Delivered-To header. @@ -66,7 +70,8 @@ From: postmaster@example.com Delivered-To: test-bounces+anne=example.org@example.com """) - self.assertEqual(get_verp(self._mlist, msg), set(['anne@example.org'])) + self.assertEqual(self._verper.get_verp(self._mlist, msg), + set(['anne@example.org'])) def test_verp_in_envelope_to(self): # A VERP address is found in the Envelope-To header. @@ -75,7 +80,8 @@ From: postmaster@example.com Envelope-To: test-bounces+anne=example.org@example.com """) - self.assertEqual(get_verp(self._mlist, msg), set(['anne@example.org'])) + self.assertEqual(self._verper.get_verp(self._mlist, msg), + set(['anne@example.org'])) def test_verp_in_apparently_to(self): # A VERP address is found in the Apparently-To header. @@ -84,7 +90,8 @@ From: postmaster@example.com Apparently-To: test-bounces+anne=example.org@example.com """) - self.assertEqual(get_verp(self._mlist, msg), set(['anne@example.org'])) + self.assertEqual(self._verper.get_verp(self._mlist, msg), + set(['anne@example.org'])) def test_verp_with_empty_header(self): # A VERP address is found, but there's an empty header. @@ -94,7 +101,8 @@ To: test-bounces+anne=example.org@example.com To: """) - self.assertEqual(get_verp(self._mlist, msg), set(['anne@example.org'])) + self.assertEqual(self._verper.get_verp(self._mlist, msg), + set(['anne@example.org'])) def test_no_verp_with_empty_header(self): # There's an empty header, and no VERP address is found. @@ -103,7 +111,7 @@ From: postmaster@example.com To: """) - self.assertEqual(get_verp(self._mlist, msg), set()) + self.assertEqual(self._verper.get_verp(self._mlist, msg), set()) def test_verp_with_non_match(self): # A VERP address is found, but a header had a non-matching pattern. @@ -113,7 +121,8 @@ To: test-bounces+anne=example.org@example.com To: test-bounces@example.com """) - self.assertEqual(get_verp(self._mlist, msg), set(['anne@example.org'])) + self.assertEqual(self._verper.get_verp(self._mlist, msg), + set(['anne@example.org'])) def test_no_verp_with_non_match(self): # No VERP address is found, and a header had a non-matching pattern. @@ -122,7 +131,7 @@ From: postmaster@example.com To: test-bounces@example.com """) - self.assertEqual(get_verp(self._mlist, msg), set()) + self.assertEqual(self._verper.get_verp(self._mlist, msg), set()) def test_multiple_verps(self): # More than one VERP address was found in the same header. @@ -132,7 +141,8 @@ To: test-bounces+anne=example.org@example.com To: test-bounces+anne=example.org@example.com """) - self.assertEqual(get_verp(self._mlist, msg), set(['anne@example.org'])) + self.assertEqual(self._verper.get_verp(self._mlist, msg), + set(['anne@example.org'])) def test_multiple_verps_different_values(self): # More than one VERP address was found in the same header with @@ -143,7 +153,7 @@ To: test-bounces+anne=example.org@example.com To: test-bounces+bart=example.org@example.com """) - self.assertEqual(get_verp(self._mlist, msg), + self.assertEqual(self._verper.get_verp(self._mlist, msg), set(['anne@example.org', 'bart@example.org'])) def test_multiple_verps_different_values_different_headers(self): @@ -155,12 +165,25 @@ To: test-bounces+anne=example.org@example.com Apparently-To: test-bounces+bart=example.org@example.com """) - self.assertEqual(get_verp(self._mlist, msg), + self.assertEqual(self._verper.get_verp(self._mlist, msg), set(['anne@example.org', 'bart@example.org'])) +class TestProbe(unittest.TestCase): + """Test VERP probing.""" + + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('test@example.com') + + + + + def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestVERP)) + suite.addTest(unittest.makeSuite(TestProbe)) return suite diff --git a/src/mailman/queue/bounce.py b/src/mailman/queue/bounce.py index d6b1576a2..35c27c6d6 100644 --- a/src/mailman/queue/bounce.py +++ b/src/mailman/queue/bounce.py @@ -26,7 +26,7 @@ import datetime from email.utils import parseaddr from lazr.config import as_timedelta -from mailman.app.bounces import get_verp +from mailman.app.bounces import StandardVERP from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.bounce import Stop @@ -190,7 +190,7 @@ class BounceRunner(Runner, BounceMixin): if not mlist.bounce_processing: return # Try VERP detection first, since it's quick and easy - addrs = get_verp(mlist, msg) + addrs = StandardVERP().get_verp(mlist, msg) if addrs: # We have an address, but check if the message is non-fatal. if scan_messages(mlist, msg) is Stop: diff --git a/src/mailman/utilities/uid.py b/src/mailman/utilities/uid.py index e44ed2983..7ef50ace0 100644 --- a/src/mailman/utilities/uid.py +++ b/src/mailman/utilities/uid.py @@ -31,16 +31,14 @@ __all__ = [ import os -import time import errno -import hashlib from flufl.lock import Lock +from uuid import uuid4 from mailman.config import config from mailman.model.uid import UID from mailman.testing import layers -from mailman.utilities.passwords import SALT_LENGTH @@ -69,7 +67,12 @@ class UniqueIDFactory: self._lockobj = Lock(self._lock_file) return self._lockobj - def new_uid(self, bytes=None): + def new_uid(self): + """Return a new UID. + + :return: The new uid + :rtype: unicode + """ if layers.is_testing(): # When in testing mode we want to produce predictable id, but we # need to coordinate this among separate processes. We could use @@ -80,19 +83,14 @@ class UniqueIDFactory: # tests will be serialized enough (and the ids reset between # tests) that it will not be a problem. Maybe. return self._next_uid() - salt = os.urandom(SALT_LENGTH) while True: - h = hashlib.sha1(repr(time.time())) - h.update(salt) - if bytes is not None: - h.update(bytes) - uid = unicode(h.hexdigest(), 'us-ascii') - try: - UID.record(uid) - except ValueError: - pass - else: - return uid + uid = unicode(uuid4().hex, 'us-ascii') + try: + UID.record(uid) + except ValueError: + pass + else: + return uid def _next_uid(self): with self._lock: |
