diff options
| -rw-r--r-- | Mailman/Handlers/CalcRecips.py | 30 | ||||
| -rw-r--r-- | Mailman/constants.py | 1 | ||||
| -rw-r--r-- | Mailman/database/model/member.py | 26 | ||||
| -rw-r--r-- | Mailman/database/model/preferences.py | 1 | ||||
| -rw-r--r-- | Mailman/docs/calc-recips.txt | 130 | ||||
| -rw-r--r-- | Mailman/interfaces/member.py | 47 | ||||
| -rw-r--r-- | Mailman/interfaces/preferences.py | 8 | ||||
| -rw-r--r-- | Mailman/testing/test_after_delivery.py | 2 | ||||
| -rw-r--r-- | Mailman/testing/test_calc_recips.py | 32 | ||||
| -rw-r--r-- | Mailman/testing/test_handlers.py | 98 |
10 files changed, 245 insertions, 130 deletions
diff --git a/Mailman/Handlers/CalcRecips.py b/Mailman/Handlers/CalcRecips.py index 5db860a79..29135999c 100644 --- a/Mailman/Handlers/CalcRecips.py +++ b/Mailman/Handlers/CalcRecips.py @@ -26,8 +26,8 @@ SendmailDeliver and BulkDeliver modules. from Mailman import Errors from Mailman import Message from Mailman import Utils -from Mailman.MemberAdaptor import ENABLED from Mailman.configuration import config +from Mailman.constants import DeliveryStatus from Mailman.i18n import _ @@ -35,16 +35,14 @@ from Mailman.i18n import _ def process(mlist, msg, msgdata): # Short circuit if we've already calculated the recipients list, # regardless of whether the list is empty or not. - if msgdata.has_key('recips'): + if 'recips' in msgdata: return # Should the original sender should be included in the recipients list? - include_sender = 1 + include_sender = True sender = msg.get_sender() - try: - if mlist.getMemberOption(sender, config.DontReceiveOwnPosts): - include_sender = 0 - except Errors.NotAMemberError: - pass + member = mlist.members.get_member(sender) + if member and not member.receive_own_postings: + include_sender = False # Support for urgent messages, which bypasses digests and disabled # delivery and forces an immediate delivery to all members Right Now. We # are specifically /not/ allowing the site admins password to work here @@ -71,18 +69,12 @@ delivery. The original message as received by Mailman is attached. """) raise Errors.RejectMessage, Utils.wrap(text) # Calculate the regular recipients of the message - recips = [mlist.getMemberCPAddress(m) - for m in mlist.getRegularMemberKeys() - if mlist.getDeliveryStatus(m) == ENABLED] + recips = set(member.address.address + for member in mlist.regular_members.members + if member.delivery_status == DeliveryStatus.enabled) # Remove the sender if they don't want to receive their own posts - if not include_sender: - try: - recips.remove(mlist.getMemberCPAddress(sender)) - except (Errors.NotAMemberError, ValueError): - # Sender does not want to get copies of their own messages (not - # metoo), but delivery to their address is disabled (nomail). Or - # the sender is not a member of the mailing list. - pass + if not include_sender and member.address.address in recips: + recips.remove(member.address.address) # Handle topic classifications do_topic_filters(mlist, msg, msgdata, recips) # Bookkeeping diff --git a/Mailman/constants.py b/Mailman/constants.py index 7b3876a2a..0933d7713 100644 --- a/Mailman/constants.py +++ b/Mailman/constants.py @@ -64,3 +64,4 @@ class SystemDefaultPreferences(object): receive_list_copy = True receive_own_postings = True delivery_mode = DeliveryMode.regular + delivery_status = DeliveryStatus.enabled diff --git a/Mailman/database/model/member.py b/Mailman/database/model/member.py index efb72ee11..1dc942323 100644 --- a/Mailman/database/model/member.py +++ b/Mailman/database/model/member.py @@ -58,10 +58,6 @@ class Member(Entity): return getattr(SystemDefaultPreferences, preference) @property - def delivery_mode(self): - return self._lookup('delivery_mode') - - @property def acknowledge_posts(self): return self._lookup('acknowledge_posts') @@ -69,11 +65,27 @@ class Member(Entity): def preferred_language(self): return self._lookup('preferred_language') - def unsubscribe(self): - self.preferences.delete() - self.delete() + @property + def receive_list_copy(self): + return self._lookup('receive_list_copy') + + @property + def receive_own_postings(self): + return self._lookup('receive_own_postings') + + @property + def delivery_mode(self): + return self._lookup('delivery_mode') + + @property + def delivery_status(self): + return self._lookup('delivery_status') @property def options_url(self): # XXX Um, this is definitely wrong return 'http://example.com/' + self.address.address + + def unsubscribe(self): + self.preferences.delete() + self.delete() diff --git a/Mailman/database/model/preferences.py b/Mailman/database/model/preferences.py index 33511f54b..07d4d84e2 100644 --- a/Mailman/database/model/preferences.py +++ b/Mailman/database/model/preferences.py @@ -37,6 +37,7 @@ class Preferences(Entity): has_field('receive_list_copy', Boolean) has_field('receive_own_postings', Boolean) has_field('delivery_mode', EnumType) + has_field('delivery_status', EnumType) # Options using_options(shortnames=True) diff --git a/Mailman/docs/calc-recips.txt b/Mailman/docs/calc-recips.txt new file mode 100644 index 000000000..9646778d6 --- /dev/null +++ b/Mailman/docs/calc-recips.txt @@ -0,0 +1,130 @@ +Calculating recipients +====================== + +Every message that makes it through to the list membership gets sent to a set +of recipient addresses. These addresses are calculated by one of the handler +modules and depends on a host of factors. + + >>> from email import message_from_string + >>> from Mailman.Message import Message + >>> from Mailman.Handlers.CalcRecips import process + >>> from Mailman.configuration import config + >>> from Mailman.database import flush + >>> mlist = config.list_manager.create('_xtest@example.com') + >>> flush() + + +Recipients are calculate from the list members, so add a bunch of members to +start out with. First, create a bunch of addresses... + + >>> address_a = config.user_manager.create_address('aperson@example.com') + >>> address_b = config.user_manager.create_address('bperson@example.com') + >>> address_c = config.user_manager.create_address('cperson@example.com') + >>> address_d = config.user_manager.create_address('dperson@example.com') + >>> address_e = config.user_manager.create_address('eperson@example.com') + >>> address_f = config.user_manager.create_address('fperson@example.com') + +...then subscribe these addresses to the mailing list as members... + + >>> from Mailman.constants import MemberRole + >>> member_a = address_a.subscribe(mlist, MemberRole.member) + >>> member_b = address_b.subscribe(mlist, MemberRole.member) + >>> member_c = address_c.subscribe(mlist, MemberRole.member) + >>> member_d = address_d.subscribe(mlist, MemberRole.member) + >>> member_e = address_e.subscribe(mlist, MemberRole.member) + >>> member_f = address_f.subscribe(mlist, MemberRole.member) + +...then make some of the members digest members. + + >>> from Mailman.constants import DeliveryMode + >>> member_d.preferences.delivery_mode = DeliveryMode.plaintext_digests + >>> member_e.preferences.delivery_mode = DeliveryMode.mime_digests + >>> member_f.preferences.delivery_mode = DeliveryMode.summary_digests + >>> flush() + + +Short-circuiting +---------------- + +Sometimes, the list of recipients already exists in the message metadata. +This can happen for example, when a message was previously delivered to some +but not all of the recipients. + + >>> msg = message_from_string("""\ + ... From: Xavier Person <xperson@example.com> + ... + ... Something of great import. + ... """, Message) + >>> recips = set(('qperson@example.com', 'zperson@example.com')) + >>> msgdata = dict(recips=recips) + >>> process(mlist, msg, msgdata) + >>> sorted(msgdata['recips']) + ['qperson@example.com', 'zperson@example.com'] + + +Regular delivery recipients +--------------------------- + +Regular delivery recipients are those people who get messages from the list as +soon as they are posted. In other words, these folks are not digest members. + + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> sorted(msgdata['recips']) + ['aperson@example.com', 'bperson@example.com', 'cperson@example.com'] + +Members can elect not to receive a list copy of their own postings. + + >>> member_c.preferences.receive_own_postings = False + >>> flush() + >>> msg = message_from_string("""\ + ... From: Claire Person <cperson@example.com> + ... + ... Something of great import. + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> sorted(msgdata['recips']) + ['aperson@example.com', 'bperson@example.com'] + +Members can also elect not to receive a list copy of any message on which they +are explicitly named as a recipient. However, see the AvoidDuplicates handler +for details. + + +Digest recipients +----------------- + +XXX Test various digest deliveries. + + +Urgent messages +--------------- + +XXX Test various urgent deliveries: + * test_urgent_moderator() + * test_urgent_admin() + * test_urgent_reject() + + +Clean up +-------- + + >>> for member in mlist.members.members: + ... member.unsubscribe() + >>> flush() + >>> list(mlist.members.members) + [] + >>> for user in config.user_manager.users: + ... config.user_manager.delete_user(user) + >>> for address in config.user_manager.addresses: + ... config.user_manager.delete_address(address) + >>> for mlist in config.list_manager.mailing_lists: + ... config.list_manager.delete(mlist) + >>> flush() + >>> list(config.user_manager.users) + [] + >>> list(config.user_manager.addresses) + [] + >>> list(config.list_manager.mailing_lists) + [] diff --git a/Mailman/interfaces/member.py b/Mailman/interfaces/member.py index 4bb03f41d..9921f7dab 100644 --- a/Mailman/interfaces/member.py +++ b/Mailman/interfaces/member.py @@ -41,7 +41,7 @@ class IMember(Interface): """Unsubscribe (and delete) this member from the mailing list.""" acknowledge_posts = Attribute( - """This is the actual acknowledgment setting for this member. + """Send an acknowledgment for every posting? Unlike going through the preferences, this attribute return the preference value based on the following lookup order: @@ -52,8 +52,8 @@ class IMember(Interface): 4. System default """) - delivery_mode = Attribute( - """This is the actual delivery mode for this member. + preferred_language = Attribute( + """The preferred language for interacting with a mailing list. Unlike going through the preferences, this attribute return the preference value based on the following lookup order: @@ -64,8 +64,32 @@ class IMember(Interface): 4. System default """) - preferred_language = Attribute( - """This is the actual preferred language for this member. + receive_list_copy = Attribute( + """Should an explicit recipient receive a list copy? + + Unlike going through the preferences, this attribute return the + preference value based on the following lookup order: + + 1. The member + 2. The address + 3. The user + 4. System default + """) + + receive_own_postings = Attribute( + """Should the poster get a list copy of their own messages? + + Unlike going through the preferences, this attribute return the + preference value based on the following lookup order: + + 1. The member + 2. The address + 3. The user + 4. System default + """) + + delivery_mode = Attribute( + """The preferred delivery mode. Unlike going through the preferences, this attribute return the preference value based on the following lookup order: @@ -76,6 +100,19 @@ class IMember(Interface): 4. System default """) + delivery_status = Attribute( + """The delivery status. + + Unlike going through the preferences, this attribute return the + preference value based on the following lookup order: + + 1. The member + 2. The address + 3. The user + 4. System default + + XXX I'm not sure this is the right place to put this.""") + options_url = Attribute( """Return the url for the given member's option page. diff --git a/Mailman/interfaces/preferences.py b/Mailman/interfaces/preferences.py index 17cfebae6..0809874e2 100644 --- a/Mailman/interfaces/preferences.py +++ b/Mailman/interfaces/preferences.py @@ -59,3 +59,11 @@ class IPreferences(Interface): This is an enum constant of the type DeliveryMode. It may also be None which means that no preference is specified.""") + + delivery_status = Attribute( + """The delivery status. + + This is an enum constant of type DeliveryStatus. It may also be None + which means that no preference is specified. + + XXX I'm not sure this is the right place to put this.""") diff --git a/Mailman/testing/test_after_delivery.py b/Mailman/testing/test_after_delivery.py index 0abad5d6e..ea4801b39 100644 --- a/Mailman/testing/test_after_delivery.py +++ b/Mailman/testing/test_after_delivery.py @@ -15,7 +15,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -"""Doctest harness for testing booking done after message delivery.""" +"""Doctest harness for testing bookkeeping done after message delivery.""" import doctest import unittest diff --git a/Mailman/testing/test_calc_recips.py b/Mailman/testing/test_calc_recips.py new file mode 100644 index 000000000..7e876d428 --- /dev/null +++ b/Mailman/testing/test_calc_recips.py @@ -0,0 +1,32 @@ +# Copyright (C) 2007 by the Free Software Foundation, Inc. +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. + +"""Doctest harness for testing the recipient calculation handler.""" + +import doctest +import unittest + +options = (doctest.ELLIPSIS + | doctest.NORMALIZE_WHITESPACE + | doctest.REPORT_NDIFF) + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocFileSuite('../docs/calc-recips.txt', + optionflags=options)) + return suite diff --git a/Mailman/testing/test_handlers.py b/Mailman/testing/test_handlers.py index 9c236584f..bbc7f5ba8 100644 --- a/Mailman/testing/test_handlers.py +++ b/Mailman/testing/test_handlers.py @@ -39,7 +39,6 @@ from Mailman.testing.base import TestBase from Mailman.Handlers import Acknowledge from Mailman.Handlers import AfterDelivery from Mailman.Handlers import Approve -from Mailman.Handlers import CalcRecips from Mailman.Handlers import Cleanse from Mailman.Handlers import CookHeaders from Mailman.Handlers import FileRecips @@ -138,102 +137,6 @@ X-BeenThere: %s -class TestCalcRecips(TestBase): - def setUp(self): - TestBase.setUp(self) - # Add a bunch of regular members - mlist = self._mlist - mlist.addNewMember('aperson@example.org') - mlist.addNewMember('bperson@example.com') - mlist.addNewMember('cperson@example.com') - # And a bunch of digest members - mlist.addNewMember('dperson@example.com', digest=1) - mlist.addNewMember('eperson@example.com', digest=1) - mlist.addNewMember('fperson@example.com', digest=1) - - def test_short_circuit(self): - msgdata = {'recips': 1} - rtn = CalcRecips.process(self._mlist, None, msgdata) - # Not really a great test, but there's little else to assert - self.assertEqual(rtn, None) - - def test_simple_path(self): - msgdata = {} - msg = email.message_from_string("""\ -From: dperson@example.com - -""", Message.Message) - CalcRecips.process(self._mlist, msg, msgdata) - self.failUnless(msgdata.has_key('recips')) - recips = msgdata['recips'] - recips.sort() - self.assertEqual(recips, ['aperson@example.org', 'bperson@example.com', - 'cperson@example.com']) - - def test_exclude_sender(self): - msgdata = {} - msg = email.message_from_string("""\ -From: cperson@example.com - -""", Message.Message) - self._mlist.setMemberOption('cperson@example.com', - config.DontReceiveOwnPosts, 1) - CalcRecips.process(self._mlist, msg, msgdata) - self.failUnless(msgdata.has_key('recips')) - recips = msgdata['recips'] - recips.sort() - self.assertEqual(recips, ['aperson@example.org', 'bperson@example.com']) - - def test_urgent_moderator(self): - self._mlist.mod_password = password('xxXXxx') - msgdata = {} - msg = email.message_from_string("""\ -From: dperson@example.com -Urgent: xxXXxx - -""", Message.Message) - CalcRecips.process(self._mlist, msg, msgdata) - self.failUnless(msgdata.has_key('recips')) - recips = msgdata['recips'] - recips.sort() - self.assertEqual(recips, ['aperson@example.org', 'bperson@example.com', - 'cperson@example.com', 'dperson@example.com', - 'eperson@example.com', 'fperson@example.com']) - - def test_urgent_admin(self): - self._mlist.mod_password = password('yyYYyy') - self._mlist.password = password('xxXXxx') - msgdata = {} - msg = email.message_from_string("""\ -From: dperson@example.com -Urgent: xxXXxx - -""", Message.Message) - CalcRecips.process(self._mlist, msg, msgdata) - self.failUnless(msgdata.has_key('recips')) - recips = msgdata['recips'] - recips.sort() - self.assertEqual(recips, ['aperson@example.org', 'bperson@example.com', - 'cperson@example.com', 'dperson@example.com', - 'eperson@example.com', 'fperson@example.com']) - - def test_urgent_reject(self): - self._mlist.mod_password = password('yyYYyy') - self._mlist.password = password('xxXXxx') - msgdata = {} - msg = email.message_from_string("""\ -From: dperson@example.com -Urgent: zzZZzz - -""", Message.Message) - self.assertRaises(Errors.RejectMessage, - CalcRecips.process, - self._mlist, msg, msgdata) - - # BAW: must test the do_topic_filters() path... - - - class TestCleanse(TestBase): def setUp(self): TestBase.setUp(self) @@ -1560,7 +1463,6 @@ Mailman rocks! def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestApprove)) - suite.addTest(unittest.makeSuite(TestCalcRecips)) suite.addTest(unittest.makeSuite(TestCleanse)) suite.addTest(unittest.makeSuite(TestCookHeaders)) suite.addTest(unittest.makeSuite(TestFileRecips)) |
