diff options
Diffstat (limited to 'src/mailman/model')
| -rw-r--r-- | src/mailman/model/bounce.py | 82 | ||||
| -rw-r--r-- | src/mailman/model/docs/bounce.rst | 84 | ||||
| -rw-r--r-- | src/mailman/model/docs/registration.txt | 2 | ||||
| -rw-r--r-- | src/mailman/model/docs/requests.txt | 20 | ||||
| -rw-r--r-- | src/mailman/model/mailinglist.py | 7 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_bounce.py | 105 |
6 files changed, 286 insertions, 14 deletions
diff --git a/src/mailman/model/bounce.py b/src/mailman/model/bounce.py new file mode 100644 index 000000000..20953b0ff --- /dev/null +++ b/src/mailman/model/bounce.py @@ -0,0 +1,82 @@ +# Copyright (C) 2011 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/>. + +"""Bounce support.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'BounceEvent', + 'BounceProcessor', + ] + + +from storm.locals import Bool, Int, DateTime, Unicode +from zope.interface import implements + +from mailman.config import config +from mailman.database.model import Model +from mailman.database.types import Enum +from mailman.interfaces.bounce import ( + BounceContext, IBounceEvent, IBounceProcessor) +from mailman.utilities.datetime import now + + + +class BounceEvent(Model): + implements(IBounceEvent) + + id = Int(primary=True) + list_name = Unicode() + email = Unicode() + timestamp = DateTime() + message_id = Unicode() + context = Enum() + processed = Bool() + + def __init__(self, list_name, email, msg, context=None): + self.list_name = list_name + self.email = email + self.timestamp = now() + self.message_id = msg['message-id'] + self.context = (BounceContext.normal if context is None else context) + self.processed = False + + + +class BounceProcessor: + implements(IBounceProcessor) + + def register(self, mlist, email, msg, where=None): + """See `IBounceProcessor`.""" + event = BounceEvent(mlist.fqdn_listname, email, msg, where) + config.db.store.add(event) + return event + + @property + def events(self): + """See `IBounceProcessor`.""" + for event in config.db.store.find(BounceEvent): + yield event + + @property + def unprocessed(self): + """See `IBounceProcessor`.""" + for event in config.db.store.find(BounceEvent, + BounceEvent.processed == False): + yield event diff --git a/src/mailman/model/docs/bounce.rst b/src/mailman/model/docs/bounce.rst new file mode 100644 index 000000000..41784cd9c --- /dev/null +++ b/src/mailman/model/docs/bounce.rst @@ -0,0 +1,84 @@ +======= +Bounces +======= + +When a message to an email address bounces, Mailman's bounce runner will +register a bounce event. This registration is done through a utility. + + >>> from zope.component import getUtility + >>> from zope.interface.verify import verifyObject + >>> from mailman.interfaces.bounce import IBounceProcessor + >>> processor = getUtility(IBounceProcessor) + >>> verifyObject(IBounceProcessor, processor) + True + + +Registration +============ + +When a bounce occurs, it's always within the context of a specific mailing +list. + + >>> mlist = create_list('test@example.com') + +The bouncing email contains useful information that will be registered as +well. In particular, the Message-ID is a key piece of data that needs to be +recorded. + + >>> msg = message_from_string("""\ + ... From: mail-daemon@example.org + ... To: test-bounces@example.com + ... Message-ID: <first> + ... + ... """) + +There is a suite of bounce detectors that are used to heuristically extract +the bouncing email addresses. Various techniques are employed including VERP, +DSN, and magic. It is the bounce queue's responsibility to extract the set of +bouncing email addrsses. These are passed one-by-one to the registration +interface. + + >>> event = processor.register(mlist, 'anne@example.com', msg) + >>> print event.list_name + test@example.com + >>> print event.email + anne@example.com + >>> print event.message_id + <first> + +Bounce events have a timestamp. + + >>> print event.timestamp + 2005-08-01 07:49:23 + +Bounce events have a flag indicating whether they've been processed or not. + + >>> event.processed + False + +When a bounce is registered, you can indicate the bounce context. + + >>> msg = message_from_string("""\ + ... From: mail-daemon@example.org + ... To: test-bounces@example.com + ... Message-ID: <second> + ... + ... """) + +If no context is given, then a default one is used. + + >>> event = processor.register(mlist, 'bart@example.com', msg) + >>> print event.message_id + <second> + >>> print event.context + BounceContext.normal + +A probe bounce carries more weight than just a normal bounce. + + >>> from mailman.interfaces.bounce import BounceContext + >>> event = processor.register( + ... mlist, 'bart@example.com', msg, BounceContext.probe) + >>> print event.message_id + <second> + >>> print event.context + BounceContext.probe diff --git a/src/mailman/model/docs/registration.txt b/src/mailman/model/docs/registration.txt index d0827d37b..0e80bfa14 100644 --- a/src/mailman/model/docs/registration.txt +++ b/src/mailman/model/docs/registration.txt @@ -149,7 +149,7 @@ message is sent to the user in order to verify the registered address. _parsemsg : False listname : alpha@example.com nodecorate : True - recipients : [u'aperson@example.com'] + recipients : set([u'aperson@example.com']) reduced_list_headers: True version : 3 diff --git a/src/mailman/model/docs/requests.txt b/src/mailman/model/docs/requests.txt index bebb61259..812d25a43 100644 --- a/src/mailman/model/docs/requests.txt +++ b/src/mailman/model/docs/requests.txt @@ -302,7 +302,7 @@ The message can be rejected, meaning it is bounced back to the sender. _parsemsg : False listname : alist@example.com nodecorate : True - recipients : [u'aperson@example.org'] + recipients : set([u'aperson@example.org']) reduced_list_headers: True version : 3 @@ -479,7 +479,7 @@ queue when the message is held. _parsemsg : False listname : alist@example.com nodecorate : True - recipients : [u'alist-owner@example.com'] + recipients : set([u'alist-owner@example.com']) reduced_list_headers: True tomoderators : True version : 3 @@ -534,7 +534,7 @@ subscriber. _parsemsg : False listname : alist@example.com nodecorate : True - recipients : [u'cperson@example.org'] + recipients : set([u'cperson@example.org']) reduced_list_headers: True version : 3 @@ -578,7 +578,7 @@ subscription and the fact that they may need to approve it. _parsemsg : False listname : alist@example.com nodecorate : True - recipients : [u'alist-owner@example.com'] + recipients : set([u'alist-owner@example.com']) reduced_list_headers: True tomoderators : True version : 3 @@ -651,7 +651,7 @@ The welcome message is sent to the person who just subscribed. _parsemsg : False listname : alist@example.com nodecorate : True - recipients : [u'fperson@example.org'] + recipients : set([u'fperson@example.org']) reduced_list_headers: True verp : False version : 3 @@ -677,7 +677,7 @@ The admin message is sent to the moderators. envsender : changeme@example.com listname : alist@example.com nodecorate : True - recipients : [] + recipients : set([]) reduced_list_headers: True version : 3 @@ -759,7 +759,7 @@ notification. _parsemsg : False listname : alist@example.com nodecorate : True - recipients : [u'alist-owner@example.com'] + recipients : set([u'alist-owner@example.com']) reduced_list_headers: True tomoderators : True version : 3 @@ -818,7 +818,7 @@ and the person remains a member of the mailing list. _parsemsg : False listname : alist@example.com nodecorate : True - recipients : [u'hperson@example.com'] + recipients : set([u'hperson@example.com']) reduced_list_headers: True version : 3 @@ -873,7 +873,7 @@ The goodbye message... _parsemsg : False listname : alist@example.com nodecorate : True - recipients : [u'gperson@example.com'] + recipients : set([u'gperson@example.com']) reduced_list_headers: True verp : False version : 3 @@ -898,6 +898,6 @@ The goodbye message... envsender : changeme@example.com listname : alist@example.com nodecorate : True - recipients : [] + recipients : set([]) reduced_list_headers: True version : 3 diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 9294fe7cc..2d972a24a 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -118,11 +118,12 @@ class MailingList(Model): bounce_matching_headers = Unicode() # XXX bounce_notify_owner_on_disable = Bool() # XXX bounce_notify_owner_on_removal = Bool() # XXX - bounce_processing = Bool() # XXX bounce_score_threshold = Int() # XXX - bounce_unrecognized_goes_to_list_owner = Bool() # XXX bounce_you_are_disabled_warnings = Int() # XXX bounce_you_are_disabled_warnings_interval = TimeDelta() # XXX + forward_unrecognized_bounces_to = Enum() + process_bounces = Bool() + # Miscellaneous default_member_action = Enum() default_nonmember_action = Enum() description = Unicode() @@ -195,7 +196,7 @@ class MailingList(Model): # For the pending database self.next_request_id = 1 self._restore() - self.personalization = Personalization.none + self.personalize = Personalization.none self.real_name = string.capwords( SPACE.join(listname.split(UNDERSCORE))) makedirs(self.data_path) diff --git a/src/mailman/model/tests/test_bounce.py b/src/mailman/model/tests/test_bounce.py new file mode 100644 index 000000000..a232b37fd --- /dev/null +++ b/src/mailman/model/tests/test_bounce.py @@ -0,0 +1,105 @@ +# Copyright (C) 2011 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/>. + +"""Test bounce model objects.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'test_suite', + ] + + +import unittest + +from datetime import datetime +from zope.component import getUtility + +from mailman.app.lifecycle import create_list +from mailman.config import config +from mailman.interfaces.bounce import BounceContext, IBounceProcessor +from mailman.testing.helpers import ( + specialized_message_from_string as message_from_string) +from mailman.testing.layers import ConfigLayer + + + +class TestBounceEvents(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._processor = getUtility(IBounceProcessor) + self._mlist = create_list('test@example.com') + self._msg = message_from_string("""\ +From: mail-daemon@example.com +To: test-bounces@example.com +Message-Id: <first> + +""") + + def test_events_iterator(self): + self._processor.register(self._mlist, 'anne@example.com', self._msg) + config.db.commit() + events = list(self._processor.events) + self.assertEqual(len(events), 1) + event = events[0] + self.assertEqual(event.list_name, 'test@example.com') + self.assertEqual(event.email, 'anne@example.com') + self.assertEqual(event.timestamp, datetime(2005, 8, 1, 7, 49, 23)) + self.assertEqual(event.message_id, '<first>') + self.assertEqual(event.context, BounceContext.normal) + self.assertEqual(event.processed, False) + # The unprocessed list will be exactly the same right now. + unprocessed = list(self._processor.unprocessed) + self.assertEqual(len(unprocessed), 1) + event = unprocessed[0] + self.assertEqual(event.list_name, 'test@example.com') + self.assertEqual(event.email, 'anne@example.com') + self.assertEqual(event.timestamp, datetime(2005, 8, 1, 7, 49, 23)) + self.assertEqual(event.message_id, '<first>') + self.assertEqual(event.context, BounceContext.normal) + self.assertEqual(event.processed, False) + + def test_unprocessed_events_iterator(self): + self._processor.register(self._mlist, 'anne@example.com', self._msg) + self._processor.register(self._mlist, 'bart@example.com', self._msg) + config.db.commit() + events = list(self._processor.events) + self.assertEqual(len(events), 2) + unprocessed = list(self._processor.unprocessed) + # The unprocessed list will be exactly the same right now. + self.assertEqual(len(unprocessed), 2) + # Process one of the events. + events[0].processed = True + config.db.commit() + # Now there will be only one unprocessed event. + unprocessed = list(self._processor.unprocessed) + self.assertEqual(len(unprocessed), 1) + # Process the other event. + events[1].processed = True + config.db.commit() + # Now there will be no unprocessed events. + unprocessed = list(self._processor.unprocessed) + self.assertEqual(len(unprocessed), 0) + + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestBounceEvents)) + return suite |
