diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/app/docs/chains.txt | 104 | ||||
| -rw-r--r-- | src/mailman/app/membership.py | 51 | ||||
| -rw-r--r-- | src/mailman/chains/base.py | 13 | ||||
| -rw-r--r-- | src/mailman/chains/builtin.py | 4 | ||||
| -rw-r--r-- | src/mailman/chains/docs/moderation.txt | 79 | ||||
| -rw-r--r-- | src/mailman/email/message.py | 3 | ||||
| -rw-r--r-- | src/mailman/queue/docs/incoming.txt | 37 | ||||
| -rw-r--r-- | src/mailman/rules/approved.py | 1 | ||||
| -rw-r--r-- | src/mailman/rules/docs/emergency.txt | 70 | ||||
| -rw-r--r-- | src/mailman/rules/docs/header-matching.txt | 4 | ||||
| -rw-r--r-- | src/mailman/rules/docs/moderation.txt | 140 | ||||
| -rw-r--r-- | src/mailman/rules/docs/rules.txt | 3 | ||||
| -rw-r--r-- | src/mailman/rules/moderation.py | 58 | ||||
| -rw-r--r-- | src/mailman/testing/helpers.py | 23 | ||||
| -rw-r--r-- | src/mailman/tests/test_documentation.py | 3 |
15 files changed, 363 insertions, 230 deletions
diff --git a/src/mailman/app/docs/chains.txt b/src/mailman/app/docs/chains.txt index 58f1dd2fd..8a8ac0cc2 100644 --- a/src/mailman/app/docs/chains.txt +++ b/src/mailman/app/docs/chains.txt @@ -16,20 +16,16 @@ The Discard chain The `discard` chain simply throws the message away. :: - >>> from zope.interface.verify import verifyObject - >>> from mailman.interfaces.chain import IChain >>> chain = config.chains['discard'] - >>> verifyObject(IChain, chain) - True >>> print chain.name discard >>> print chain.description Discard a message and stop processing. - >>> mlist = create_list('_xtest@example.com') + >>> mlist = create_list('test@example.com') >>> msg = message_from_string("""\ ... From: aperson@example.com - ... To: _xtest@example.com + ... To: test@example.com ... Subject: My first post ... Message-ID: <first> ... @@ -56,8 +52,6 @@ this action. :: >>> chain = config.chains['reject'] - >>> verifyObject(IChain, chain) - True >>> print chain.name reject >>> print chain.description @@ -75,7 +69,7 @@ The bounce message is now sitting in the `virgin` queue. 1 >>> print qfiles[0].msg.as_string() Subject: My first post - From: _xtest-owner@example.com + From: test-owner@example.com To: aperson@example.com ... [No bounce details are available] @@ -84,7 +78,7 @@ The bounce message is now sitting in the `virgin` queue. MIME-Version: 1.0 <BLANKLINE> From: aperson@example.com - To: _xtest@example.com + To: test@example.com Subject: My first post Message-ID: <first> <BLANKLINE> @@ -96,14 +90,11 @@ The bounce message is now sitting in the `virgin` queue. The Hold Chain ============== -The `hold` chain places the message into the admin request database and -depending on the list's settings, sends a notification to both the original -sender and the list moderators. -:: +The `hold` chain places the message into the administrative request database +and depending on the list's settings, sends a notification to both the +original sender and the list moderators. :: >>> chain = config.chains['hold'] - >>> verifyObject(IChain, chain) - True >>> print chain.name hold >>> print chain.description @@ -113,32 +104,39 @@ sender and the list moderators. ... process(mlist, msg, {}, 'hold') HOLD: <first> -There are now two messages in the Virgin queue, one to the list moderators and +There are now two messages in the virgin queue, one to the list moderators and one to the original author. >>> qfiles = get_queue_messages('virgin', sort_on='to') >>> len(qfiles) 2 -This message is addressed to the mailing list moderators. +One of the message is addressed to the mailing list moderators, and the other +is addressed to the original sender. - >>> print qfiles[0].msg.as_string() - Subject: _xtest@example.com post from aperson@example.com requires approval - From: _xtest-owner@example.com - To: _xtest-owner@example.com + >>> from operator import itemgetter + >>> messages = sorted((item.msg for item in qfiles), + ... key=itemgetter('to'), reverse=True) + +This one is addressed to the list moderators. + + >>> print messages[0].as_string() + Subject: test@example.com post from aperson@example.com requires approval + From: test-owner@example.com + To: test-owner@example.com MIME-Version: 1.0 ... As list administrator, your authorization is requested for the following mailing list posting: <BLANKLINE> - List: _xtest@example.com + List: test@example.com From: aperson@example.com Subject: My first post Reason: XXX <BLANKLINE> At your convenience, visit: <BLANKLINE> - http://lists.example.com/admindb/_xtest@example.com + http://lists.example.com/admindb/test@example.com <BLANKLINE> to approve or deny the request. <BLANKLINE> @@ -147,7 +145,7 @@ This message is addressed to the mailing list moderators. MIME-Version: 1.0 <BLANKLINE> From: aperson@example.com - To: _xtest@example.com + To: test@example.com Subject: My first post Message-ID: <first> X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW @@ -162,7 +160,7 @@ This message is addressed to the mailing list moderators. MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: confirm ... - From: _xtest-request@example.com + From: test-request@example.com ... <BLANKLINE> If you reply to this message, keeping the Subject: header intact, @@ -175,15 +173,15 @@ This message is addressed to the mailing list moderators. This message is addressed to the sender of the message. - >>> print qfiles[1].msg.as_string() + >>> print messages[1].as_string() MIME-Version: 1.0 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit - Subject: Your message to _xtest@example.com awaits moderator approval - From: _xtest-bounces@example.com + Subject: Your message to test@example.com awaits moderator approval + From: test-bounces@example.com To: aperson@example.com ... - Your mail to '_xtest@example.com' with the subject + Your mail to 'test@example.com' with the subject <BLANKLINE> My first post <BLANKLINE> @@ -197,7 +195,7 @@ This message is addressed to the sender of the message. notification of the moderator's decision. If you would like to cancel this posting, please visit the following URL: <BLANKLINE> - http://lists.example.com/confirm/_xtest@example.com/... + http://lists.example.com/confirm/test@example.com/... <BLANKLINE> <BLANKLINE> @@ -210,7 +208,7 @@ first item is a type code and the second item is a message id. >>> import re >>> cookie = None - >>> for line in qfiles[1].msg.get_payload().splitlines(): + >>> for line in messages[1].get_payload().splitlines(): ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line) ... if mo: ... cookie = mo.group('cookie') @@ -221,8 +219,9 @@ first item is a type code and the second item is a message id. >>> from zope.component import getUtility >>> data = getUtility(IPendings).confirm(cookie) - >>> sorted(data.items()) - [(u'id', ...), (u'type', u'held message')] + >>> dump_msgdata(data) + id : 1 + type: held message The message itself is held in the message store. :: @@ -238,7 +237,7 @@ The message itself is held in the message store. >>> print msg.as_string() From: aperson@example.com - To: _xtest@example.com + To: test@example.com Subject: My first post Message-ID: <first> X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW @@ -252,14 +251,14 @@ The Accept chain The `accept` chain sends the message on the `pipeline` queue, where it will be processed and sent on to the list membership. +:: >>> chain = config.chains['accept'] - >>> verifyObject(IChain, chain) - True >>> print chain.name accept >>> print chain.description Accept a message. + >>> with event_subscribers(print_msgid): ... process(mlist, msg, {}, 'accept') ACCEPT: <first> @@ -269,7 +268,7 @@ processed and sent on to the list membership. 1 >>> print qfiles[0].msg.as_string() From: aperson@example.com - To: _xtest@example.com + To: test@example.com Subject: My first post Message-ID: <first> X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW @@ -288,21 +287,22 @@ needed. The jump names a chain which is jumped to if the rule matches. There is one built-in run-time chain, called appropriately `built-in`. This is the default chain to use when no other input chain is defined for a mailing -list. It runs through the default rules, providing functionality similar to -the Hold handler from previous versions of Mailman. +list. It runs through the default rules. >>> chain = config.chains['built-in'] - >>> verifyObject(IChain, chain) - True >>> print chain.name built-in >>> print chain.description The built-in moderation chain. -The previously created message is innocuous enough that it should pass through -all default rules. This message will end up in the `pipeline` queue. +Once the sender is a member of the mailing list, the previously created +message is innocuous enough that it should pass through all default rules. +This message will end up in the `pipeline` queue. :: + >>> from mailman.testing.helpers import subscribe + >>> subscribe(mlist, 'Anne') + >>> with event_subscribers(print_msgid): ... process(mlist, msg, {}) ACCEPT: <first> @@ -312,13 +312,13 @@ all default rules. This message will end up in the `pipeline` queue. 1 >>> print qfiles[0].msg.as_string() From: aperson@example.com - To: _xtest@example.com + To: test@example.com Subject: My first post Message-ID: <first> X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - X-Mailman-Rule-Misses: approved; emergency; loop; moderation; + X-Mailman-Rule-Misses: approved; emergency; loop; member-moderation; administrivia; implicit-dest; max-recipients; max-size; - news-moderation; no-subject; suspicious-header + news-moderation; no-subject; suspicious-header; nonmember-moderation <BLANKLINE> An important message. <BLANKLINE> @@ -326,10 +326,9 @@ all default rules. This message will end up in the `pipeline` queue. In addition, the message metadata now contains lists of all rules that have hit and all rules that have missed. - >>> sorted(qfiles[0].msgdata['rule_hits']) - [] - >>> for rule_name in sorted(qfiles[0].msgdata['rule_misses']): - ... print rule_name + >>> dump_list(qfiles[0].msgdata['rule_hits']) + *Empty* + >>> dump_list(qfiles[0].msgdata['rule_misses']) administrivia approved emergency @@ -337,7 +336,8 @@ hit and all rules that have missed. loop max-recipients max-size - moderation + member-moderation news-moderation no-subject + nonmember-moderation suspicious-header diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py index 60b0586b7..8ea8769a6 100644 --- a/src/mailman/app/membership.py +++ b/src/mailman/app/membership.py @@ -41,7 +41,7 @@ from mailman.interfaces.usermanager import IUserManager -def add_member(mlist, address, realname, password, delivery_mode, language): +def add_member(mlist, email, realname, password, delivery_mode, language): """Add a member right now. The member's subscription must be approved by whatever policy the list @@ -49,16 +49,16 @@ def add_member(mlist, address, realname, password, delivery_mode, language): :param mlist: The mailing list to add the member to. :type mlist: `IMailingList` - :param address: The address to subscribe. - :type address: string + :param email: The email address to subscribe. + :type email: str :param realname: The subscriber's full name. - :type realname: string + :type realname: str :param password: The subscriber's password. - :type password: string + :type password: str :param delivery_mode: The delivery mode the subscriber has chosen. :type delivery_mode: DeliveryMode :param language: The language that the subscriber is going to use. - :type language: string + :type language: str :return: The just created member. :rtype: `IMember` :raises AlreadySubscribedError: if the user is already subscribed to @@ -67,52 +67,51 @@ def add_member(mlist, address, realname, password, delivery_mode, language): :raises MembershipIsBannedError: if the membership is not allowed. """ # Let's be extra cautious. - validate(address) - if mlist.members.get_member(address) is not None: + validate(email) + if mlist.members.get_member(email) is not None: raise AlreadySubscribedError( - mlist.fqdn_listname, address, MemberRole.member) - # Check for banned address here too for admin mass subscribes and - # confirmations. - pattern = Utils.get_pattern(address, mlist.ban_list) + mlist.fqdn_listname, email, MemberRole.member) + # Check for banned email addresses here too for administrative mass + # subscribes and confirmations. + pattern = Utils.get_pattern(email, mlist.ban_list) if pattern: - raise MembershipIsBannedError(mlist, address) - # Do the actual addition. First, see if there's already a user linked - # with the given address. + raise MembershipIsBannedError(mlist, email) + # See if there's already a user linked with the given address. user_manager = getUtility(IUserManager) - user = user_manager.get_user(address) + user = user_manager.get_user(email) if user is None: # A user linked to this address does not yet exist. Is the address # itself known but just not linked to a user? - address_obj = user_manager.get_address(address) - if address_obj is None: + address = user_manager.get_address(email) + if address is None: # Nope, we don't even know about this address, so create both the # user and address now. - user = user_manager.create_user(address, realname) + user = user_manager.create_user(email, realname) # Do it this way so we don't have to flush the previous change. - address_obj = list(user.addresses)[0] + address = list(user.addresses)[0] else: # The address object exists, but it's not linked to a user. # Create the user and link it now. user = user_manager.create_user() - user.real_name = (realname if realname else address_obj.real_name) - user.link(address_obj) + user.real_name = (realname if realname else address.real_name) + user.link(address) # Since created the user, then the member, and set preferences on the # appropriate object. user.password = password user.preferences.preferred_language = language - member = address_obj.subscribe(mlist, MemberRole.member) + member = address.subscribe(mlist, MemberRole.member) member.preferences.delivery_mode = delivery_mode else: # The user exists and is linked to the address. - for address_obj in user.addresses: - if address_obj.address == address: + for address in user.addresses: + if address.email == address: break else: raise AssertionError( 'User should have had linked address: {0}'.format(address)) # Create the member and set the appropriate preferences. # pylint: disable-msg=W0631 - member = address_obj.subscribe(mlist, MemberRole.member) + member = address.subscribe(mlist, MemberRole.member) member.preferences.preferred_language = language member.preferences.delivery_mode = delivery_mode return member diff --git a/src/mailman/chains/base.py b/src/mailman/chains/base.py index 26f8a11e2..d42eced3e 100644 --- a/src/mailman/chains/base.py +++ b/src/mailman/chains/base.py @@ -46,6 +46,19 @@ class Link: self.chain = chain self.function = function + def __repr__(self): + message = '<Link "if {0.rule.name} then {0.action} ' + if self.chain is None and self.function is not None: + message += '{0.function}()' + elif self.chain is not None and self.function is None: + message += '{0.chain.name}' + elif self.chain is None and self.function is None: + pass + else: + message += '{0.chain.name} {0.function}()' + message += '">' + return message.format(self) + class TerminalChainBase: diff --git a/src/mailman/chains/builtin.py b/src/mailman/chains/builtin.py index 7725b9359..8bc2567e1 100644 --- a/src/mailman/chains/builtin.py +++ b/src/mailman/chains/builtin.py @@ -52,7 +52,7 @@ class BuiltInChain: ('emergency', LinkAction.jump, 'hold'), ('loop', LinkAction.jump, 'discard'), # Determine whether the member or nonmember has an action shortcut. - ('moderation', LinkAction.jump, 'moderation'), + ('member-moderation', LinkAction.jump, 'moderation'), # Do all of the following before deciding whether to hold the message. ('administrivia', LinkAction.defer, None), ('implicit-dest', LinkAction.defer, None), @@ -66,6 +66,8 @@ class BuiltInChain: # Take a detour through the header matching chain, which we'll create # later. ('truth', LinkAction.detour, 'header-match'), + # Check for nonmember moderation. + ('nonmember-moderation', LinkAction.jump, 'moderation'), # Finally, the builtin chain jumps to acceptance. ('truth', LinkAction.jump, 'accept'), ) diff --git a/src/mailman/chains/docs/moderation.txt b/src/mailman/chains/docs/moderation.txt index 33bf63df9..ce16d808d 100644 --- a/src/mailman/chains/docs/moderation.txt +++ b/src/mailman/chains/docs/moderation.txt @@ -2,7 +2,7 @@ Moderation ========== -Posts by members and non-members are subject to moderation checks during +Posts by members and nonmembers are subject to moderation checks during incoming processing. Different situations can cause such posts to be held for moderator approval. @@ -23,9 +23,6 @@ Nonmembers almost always have a `hold` action, though some mailing lists may choose to set this default action to `discard`, meaning their posts would be immediately thrown away. -XXX What about default nonmember actions when the poster has not been -registered as a nonmember? - Member moderation ================= @@ -61,9 +58,6 @@ to Zope events that are triggered on each case. ... for miss in event.msgdata.get('rule_misses', []): ... print ' ', miss - >>> import zope.event - >>> zope.event.subscribers.append(on_chain) - Anne's post to the mailing list runs through the incoming runner's default built-in chain. No rules hit and so the message is accepted. :: @@ -77,7 +71,9 @@ built-in chain. No rules hit and so the message is accepted. ... """) >>> from mailman.core.chains import process - >>> process(mlist, msg, {}, 'built-in') + >>> from mailman.testing.helpers import event_subscribers + >>> with event_subscribers(on_chain): + ... process(mlist, msg, {}, 'built-in') <mailman.chains.accept.AcceptNotification ...> <mailman.chains.accept.AcceptChain ...> Subject: aardvark @@ -86,7 +82,7 @@ built-in chain. No rules hit and so the message is accepted. approved emergency loop - moderation + member-moderation administrivia implicit-dest max-recipients @@ -94,6 +90,7 @@ built-in chain. No rules hit and so the message is accepted. news-moderation no-subject suspicious-header + nonmember-moderation However, when Anne's moderation action is set to `hold`, her post is held for moderator approval. @@ -110,12 +107,13 @@ moderator approval. ... This is a test. ... """) - >>> process(mlist, msg, {}, 'built-in') + >>> with event_subscribers(on_chain): + ... process(mlist, msg, {}, 'built-in') <mailman.chains.hold.HoldNotification ...> <mailman.chains.hold.HoldChain ...> Subject: badger Hits: - moderation + member-moderation Misses: approved emergency @@ -134,12 +132,13 @@ The list's member moderation action can also be set to `discard`... ... This is a test. ... """) - >>> process(mlist, msg, {}, 'built-in') + >>> with event_subscribers(on_chain): + ... process(mlist, msg, {}, 'built-in') <mailman.chains.discard.DiscardNotification ...> <mailman.chains.discard.DiscardChain ...> Subject: cougar Hits: - moderation + member-moderation Misses: approved emergency @@ -157,12 +156,13 @@ The list's member moderation action can also be set to `discard`... ... This is a test. ... """) - >>> process(mlist, msg, {}, 'built-in') + >>> with event_subscribers(on_chain): + ... process(mlist, msg, {}, 'built-in') <mailman.chains.reject.RejectNotification ...> <mailman.chains.reject.RejectChain ...> Subject: dingo Hits: - moderation + member-moderation Misses: approved emergency @@ -172,8 +172,51 @@ The list's member moderation action can also be set to `discard`... Nonmembers ========== -XXX +Registered nonmembers are handled very similarly to members, the main +difference being that they usually have a default moderation action. This is +how the incoming queue runner adds sender addresses as nonmembers. + >>> from zope.component import getUtility + >>> from mailman.interfaces.usermanager import IUserManager + >>> user_manager = getUtility(IUserManager) + >>> address = user_manager.create_address('bart@example.com') + >>> address + <Address: bart@example.com [not verified] at ...> + +When the moderation rule runs on a message from this sender, this address will +be registered as a nonmember of the mailing list, and it will be held for +moderator approval. +:: + + >>> msg = message_from_string("""\ + ... From: bart@example.com + ... To: test@example.com + ... Subject: elephant + ... + ... """) + + >>> with event_subscribers(on_chain): + ... process(mlist, msg, {}, 'built-in') + <mailman.chains.hold.HoldNotification ...> + <mailman.chains.hold.HoldChain ...> + Subject: elephant + Hits: + nonmember-moderation + Misses: + approved + emergency + loop + member-moderation + administrivia + implicit-dest + max-recipients + max-size + news-moderation + no-subject + suspicious-header -.. Clean up - >>> zope.event.subscribers.remove(on_chain) + >>> nonmember = mlist.nonmembers.get_member('bart@example.com') + >>> nonmember + <Member: bart@example.com on test@example.com as MemberRole.nonmember> + >>> print nonmember.moderation_action + Action.hold diff --git a/src/mailman/email/message.py b/src/mailman/email/message.py index 7b362a6a2..4eb049f17 100644 --- a/src/mailman/email/message.py +++ b/src/mailman/email/message.py @@ -144,7 +144,8 @@ class Message(email.message.Message): field_values = self.get_all(header, []) senders.extend(address.lower() for (real_name, address) in email.utils.getaddresses(field_values)) - return senders + # Filter out None and the empty string. + return [sender for sender in senders if sender] def get_filename(self, failobj=None): """Some MUA have bugs in RFC2231 filename encoding and cause diff --git a/src/mailman/queue/docs/incoming.txt b/src/mailman/queue/docs/incoming.txt index 75130c4cc..6455db20b 100644 --- a/src/mailman/queue/docs/incoming.txt +++ b/src/mailman/queue/docs/incoming.txt @@ -87,10 +87,12 @@ not linked to a user and are unverified. Accepted messages ================= -We have a message that is going to be sent to the mailing list. This message -is so perfectly fine for posting that it will be accepted and forward to the -pipeline queue. +We have a message that is going to be sent to the mailing list. Once Anne is +a member of the mailing list, this message is so perfectly fine for posting +that it will be accepted and forward to the pipeline queue. + >>> from mailman.testing.helpers import subscribe + >>> subscribe(mlist, 'Anne') >>> msg = message_from_string("""\ ... From: aperson@example.com ... To: test@example.com @@ -120,9 +122,9 @@ Now the message is in the pipeline queue. Subject: My first post Message-ID: <first> Date: ... - X-Mailman-Rule-Misses: approved; emergency; loop; moderation; + X-Mailman-Rule-Misses: approved; emergency; loop; member-moderation; administrivia; implicit-dest; max-recipients; max-size; - news-moderation; no-subject; suspicious-header + news-moderation; no-subject; suspicious-header; nonmember-moderation <BLANKLINE> First post! <BLANKLINE> @@ -149,12 +151,12 @@ pipeline queue. ... event.msg['from'], event.msg['to'], ... event.msg['message-id']) - >>> import zope.event - >>> zope.event.subscribers.append(on_chain) - >>> mlist.emergency = True - >>> inject_message(mlist, msg) - >>> incoming.run() + + >>> from mailman.testing.helpers import event_subscribers + >>> with event_subscribers(on_chain): + ... inject_message(mlist, msg) + ... incoming.run() <mailman.chains.hold.HoldNotification ...> <mailman.chains.hold.HoldChain ...> From: aperson@example.com @@ -187,8 +189,9 @@ new chain and set it as the mailing list's start chain. >>> mlist.start_chain = test_chain.name >>> msg.replace_header('message-id', '<second>') - >>> inject_message(mlist, msg) - >>> incoming.run() + >>> with event_subscribers(on_chain): + ... inject_message(mlist, msg) + ... incoming.run() <mailman.chains.discard.DiscardNotification ...> <mailman.chains.discard.DiscardChain ...> From: aperson@example.com @@ -216,8 +219,9 @@ just create a new chain that does. >>> mlist.start_chain = test_chain.name >>> msg.replace_header('message-id', '<third>') - >>> inject_message(mlist, msg) - >>> incoming.run() + >>> with event_subscribers(on_chain): + ... inject_message(mlist, msg) + ... incoming.run() <mailman.chains.reject.RejectNotification ...> <mailman.chains.reject.RejectChain ...> From: aperson@example.com @@ -257,8 +261,3 @@ to the original sender. --===============... >>> del config.chains['always-reject'] - -.. - Clean up. - - >>> zope.event.subscribers.remove(on_chain) diff --git a/src/mailman/rules/approved.py b/src/mailman/rules/approved.py index 9f9dc1614..51314cc02 100644 --- a/src/mailman/rules/approved.py +++ b/src/mailman/rules/approved.py @@ -73,6 +73,7 @@ class Approved: break payload = part.get_payload(decode=True) if payload is not None: + line = '' lines = payload.splitlines(True) for lineno, line in enumerate(lines): if line.strip() <> '': diff --git a/src/mailman/rules/docs/emergency.txt b/src/mailman/rules/docs/emergency.txt index 2a94234c6..f28f9eed9 100644 --- a/src/mailman/rules/docs/emergency.txt +++ b/src/mailman/rules/docs/emergency.txt @@ -5,75 +5,33 @@ Emergency When the mailing list has its emergency flag set, all messages posted to the list are held for moderator approval. - >>> mlist = create_list('_xtest@example.com') + >>> mlist = create_list('test@example.com') + >>> rule = config.rules['emergency'] >>> msg = message_from_string("""\ ... From: aperson@example.com - ... To: _xtest@example.com + ... To: test@example.com ... Subject: My first post ... Message-ID: <first> ... ... An important message. ... """) -The emergency rule is matched as part of the built-in chain. The emergency -rule matches if the flag is set on the mailing list. +By default, the mailing list does not have its emergency flag set. - >>> from mailman.core.chains import process - >>> mlist.emergency = True - >>> process(mlist, msg, {}, 'built-in') - -There are two messages in the virgin queue. The one addressed to the original -sender will contain a token we can use to grab the held message out of the -pending requests. -:: - - >>> virginq = config.switchboards['virgin'] + >>> mlist.emergency + False + >>> rule.check(mlist, msg, {}) + False - >>> from mailman.interfaces.messages import IMessageStore - >>> from mailman.interfaces.pending import IPendings - >>> from mailman.interfaces.requests import IRequests - >>> from zope.component import getUtility - >>> message_store = getUtility(IMessageStore) +The emergency rule matches if the flag is set on the mailing list. - >>> def get_held_message(): - ... import re - ... qfiles = [] - ... for filebase in virginq.files: - ... qmsg, qdata = virginq.dequeue(filebase) - ... virginq.finish(filebase) - ... qfiles.append(qmsg) - ... from operator import itemgetter - ... qfiles.sort(key=itemgetter('to')) - ... cookie = None - ... for line in qfiles[1].get_payload().splitlines(): - ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line) - ... if mo: - ... cookie = mo.group('cookie') - ... break - ... assert cookie is not None, 'No confirmation token found' - ... data = getUtility(IPendings).confirm(cookie) - ... requestdb = getUtility(IRequests).get_list_requests(mlist) - ... rkey, rdata = requestdb.get_request(data['id']) - ... return message_store.get_message_by_id( - ... rdata['_mod_message_id']) - - >>> msg = get_held_message() - >>> print msg.as_string() - From: aperson@example.com - To: _xtest@example.com - Subject: My first post - Message-ID: <first> - X-Mailman-Rule-Hits: emergency - X-Mailman-Rule-Misses: approved - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - <BLANKLINE> - An important message. - <BLANKLINE> + >>> mlist.emergency = True + >>> rule.check(mlist, msg, {}) + True However, if the message metadata has a ``moderator_approved`` key set, then even if the mailing list has its emergency flag set, the message still goes through to the membership. - >>> process(mlist, msg, dict(moderator_approved=True), 'built-in') - >>> len(virginq.files) - 0 + >>> rule.check(mlist, msg, dict(moderator_approved=True)) + False diff --git a/src/mailman/rules/docs/header-matching.txt b/src/mailman/rules/docs/header-matching.txt index 663fcbc8a..b07118e11 100644 --- a/src/mailman/rules/docs/header-matching.txt +++ b/src/mailman/rules/docs/header-matching.txt @@ -6,7 +6,7 @@ Mailman can do pattern based header matching during its normal rule processing. There is a set of site-wide default header matches specified in the configuration file under the ``[spam.headers]`` section. - >>> mlist = create_list('_xtest@example.com') + >>> mlist = create_list('test@example.com') Because the default ``[spam.headers]`` section is empty, we'll just extend the current header matching chain with a pattern that matches 4 or more stars, @@ -21,7 +21,7 @@ through the chain untouched (i.e. no disposition). >>> msg = message_from_string("""\ ... From: aperson@example.com - ... To: _xtest@example.com + ... To: test@example.com ... Subject: Not spam ... Message-ID: <one> ... diff --git a/src/mailman/rules/docs/moderation.txt b/src/mailman/rules/docs/moderation.txt index ce88c8576..fdca04599 100644 --- a/src/mailman/rules/docs/moderation.txt +++ b/src/mailman/rules/docs/moderation.txt @@ -1,41 +1,44 @@ -================= -Member moderation -================= +========== +Moderation +========== All members and nonmembers have a moderation action. When the action is not -`defer`, the `moderation` rule flags the message as needing a moderation -shortcut. This might be to automatically accept, discard, reject, or hold the -message. +`defer`, the `moderation` rule flags the message as needing moderation. This +might be to automatically accept, discard, reject, or hold the message. + +Two separate rules check for member and nonmember moderation. Member +moderation happens early in the built-in chain, while nonmember moderation +happens later in the chain, after normal moderation checks. >>> mlist = create_list('test@example.com') - >>> rule = config.rules['moderation'] - >>> print rule.name - moderation -Let's add the message author as a non-moderated member. -:: - >>> from mailman.interfaces.member import MemberRole - >>> from mailman.interfaces.usermanager import IUserManager +Member moderation +================= - >>> from zope.component import getUtility - >>> user = getUtility(IUserManager).create_user( - ... 'aperson@example.org', 'Anne Person') + >>> member_rule = config.rules['member-moderation'] + >>> print member_rule.name + member-moderation - >>> address = list(user.addresses)[0] - >>> member = address.subscribe(mlist, MemberRole.member) +Anne, a mailing list member, sends a message to the mailing list. Her +postings are not moderated. +:: + + >>> from mailman.testing.helpers import subscribe + >>> subscribe(mlist, 'Anne') + >>> member = mlist.members.get_member('aperson@example.com') >>> print member.moderation_action Action.defer -Because the member is not moderated, the rule does not match. +Because Anne is not moderated, the member moderation rule does not match. - >>> msg = message_from_string("""\ - ... From: aperson@example.org + >>> member_msg = message_from_string("""\ + ... From: aperson@example.com ... To: test@example.com ... Subject: A posted message ... ... """) - >>> rule.check(mlist, msg, {}) + >>> member_rule.check(mlist, member_msg, {}) False Once the member's moderation action is set to something other than `defer`, @@ -45,11 +48,11 @@ information for the eventual moderation chain. >>> from mailman.interfaces.action import Action >>> member.moderation_action = Action.hold >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> member_rule.check(mlist, member_msg, msgdata) True >>> dump_msgdata(msgdata) moderation_action: hold - moderation_sender: aperson@example.org + moderation_sender: aperson@example.com Nonmembers @@ -58,39 +61,104 @@ Nonmembers Nonmembers are handled in a similar way, although by default, nonmember postings are held for moderator approval. - >>> user = getUtility(IUserManager).create_user( - ... 'bperson@example.org', 'Bart Person') + >>> nonmember_rule = config.rules['nonmember-moderation'] + >>> print nonmember_rule.name + nonmember-moderation - >>> address = list(user.addresses)[0] - >>> nonmember = address.subscribe(mlist, MemberRole.nonmember) +Bart, who is not a member of the mailing list, sends a message to the list. + + >>> from mailman.interfaces.member import MemberRole + >>> subscribe(mlist, 'Bart', MemberRole.nonmember) + >>> nonmember = mlist.nonmembers.get_member('bperson@example.com') >>> print nonmember.moderation_action Action.hold -Because the sender's moderation action is to hold by default, the rule -matches. Again, the message metadata carries some useful information. +When Bart is registered as a nonmember of the list, his moderation action is +set to hold by default. Thus the rule matches and the message metadata again +carries some useful information. - >>> msg = message_from_string("""\ - ... From: bperson@example.org + >>> nonmember_msg = message_from_string("""\ + ... From: bperson@example.com ... To: test@example.com ... Subject: A posted message ... ... """) >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> nonmember_rule.check(mlist, nonmember_msg, msgdata) True >>> dump_msgdata(msgdata) moderation_action: hold - moderation_sender: bperson@example.org + moderation_sender: bperson@example.com Of course, the nonmember action can be set to defer the decision, in which case the rule does not match. >>> nonmember.moderation_action = Action.defer - >>> rule.check(mlist, msg, {}) + >>> nonmember_rule.check(mlist, nonmember_msg, {}) False Unregistered nonmembers ======================= -XXX +The incoming queue runner ensures that all sender addresses are registered in +the system, but it is the moderation rule that subscribes nonmember addresses +to the mailing list if they are not already subscribed. +:: + + >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility + >>> address = getUtility(IUserManager).create_address( + ... 'cperson@example.com') + >>> address + <Address: cperson@example.com [not verified] at ...> + + >>> msg = message_from_string("""\ + ... From: cperson@example.com + ... To: test@example.com + ... Subject: A posted message + ... + ... """) + +cperson is neither a member, nor a nonmember of the mailing list. +:: + + >>> def memberkey(member): + ... return member.mailing_list, member.address.email, int(member.role) + + >>> dump_list(mlist.members.members, key=memberkey) + <Member: Anne Person <aperson@example.com> + on test@example.com as MemberRole.member> + >>> dump_list(mlist.nonmembers.members, key=memberkey) + <Member: Bart Person <bperson@example.com> + on test@example.com as MemberRole.nonmember> + +However, when the nonmember moderation rule runs, it adds the cperson as a +nonmember of the list. The rule also matches. + + >>> msgdata = {} + >>> nonmember_rule.check(mlist, msg, msgdata) + True + >>> dump_msgdata(msgdata) + moderation_action: hold + moderation_sender: cperson@example.com + + >>> dump_list(mlist.members.members, key=memberkey) + <Member: Anne Person <aperson@example.com> + on test@example.com as MemberRole.member> + >>> dump_list(mlist.nonmembers.members, key=memberkey) + <Member: Bart Person <bperson@example.com> + on test@example.com as MemberRole.nonmember> + <Member: cperson@example.com + on test@example.com as MemberRole.nonmember> + + +Cross-membership checks +======================= + +Of course, the member moderation rule does not match for nonmembers... + + >>> member_rule.check(mlist, nonmember_msg, {}) + False + >>> nonmember_rule.check(mlist, member_msg, {}) + False diff --git a/src/mailman/rules/docs/rules.txt b/src/mailman/rules/docs/rules.txt index 321f1b277..3c2eab04d 100644 --- a/src/mailman/rules/docs/rules.txt +++ b/src/mailman/rules/docs/rules.txt @@ -26,9 +26,10 @@ names to rule objects. loop True max-recipients True max-size True - moderation True + member-moderation True news-moderation True no-subject True + nonmember-moderation True suspicious-header True truth True diff --git a/src/mailman/rules/moderation.py b/src/mailman/rules/moderation.py index 034458553..733edd70c 100644 --- a/src/mailman/rules/moderation.py +++ b/src/mailman/rules/moderation.py @@ -21,49 +21,85 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ - 'Moderation', + 'MemberModeration', + 'NonmemberModeration', ] +from zope.component import getUtility from zope.interface import implements from mailman.core.i18n import _ from mailman.interfaces.action import Action +from mailman.interfaces.member import MemberRole from mailman.interfaces.rules import IRule +from mailman.interfaces.usermanager import IUserManager -class Moderation: +class MemberModeration: """The member moderation rule.""" implements(IRule) - name = 'moderation' - description = _('Match messages sent by moderated members and nonmembers.') + name = 'member-moderation' + description = _('Match messages sent by moderated members.') record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" for sender in msg.senders: member = mlist.members.get_member(sender) - action = (Action.defer if member is None + action = (None if member is None else member.moderation_action) - if action is not Action.defer: + if action is Action.defer: + # The regular moderation rules apply. + return False + elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. msgdata['moderation_action'] = action.enumname msgdata['moderation_sender'] = sender return True + # The sender is not a member so this rule does not match. + return False + + + +class NonmemberModeration: + """The nonmember moderation rule.""" + implements(IRule) + + name = 'nonmember-moderation' + description = _('Match messages sent by nonmembers.') + record = True + + def check(self, mlist, msg, msgdata): + """See `IRule`.""" + user_manager = getUtility(IUserManager) + # First ensure that all senders are already either members or + # nonmembers. If they are not subscribed in some role to the mailing + # list, make them nonmembers. + for sender in msg.senders: + if (mlist.members.get_member(sender) is None and + mlist.nonmembers.get_member(sender) is None): + # The address is neither a member nor nonmember. + address = user_manager.get_address(sender) + assert address is not None, ( + 'Posting address is not registered: {0}'.format(sender)) + address.subscribe(mlist, MemberRole.nonmember) + # Do nonmember moderation check. for sender in msg.senders: nonmember = mlist.nonmembers.get_member(sender) - action = (Action.defer if nonmember is None + action = (None if nonmember is None else nonmember.moderation_action) - if action is not Action.defer: + if action is Action.defer: + # The regular moderation rules apply. + return False + elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. msgdata['moderation_action'] = action.enumname msgdata['moderation_sender'] = sender return True - # XXX This is not correct. If the sender is neither a member nor a - # nonmember, we need to register them as a nonmember and give them the - # default action. + # The sender must be a member, so this rule does not match. return False diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py index adcc20aef..fd2b9ffb3 100644 --- a/src/mailman/testing/helpers.py +++ b/src/mailman/testing/helpers.py @@ -252,19 +252,28 @@ def event_subscribers(*subscribers): """ old_subscribers = event.subscribers[:] event.subscribers = list(subscribers) - yield - event.subscribers[:] = old_subscribers + try: + yield + finally: + event.subscribers[:] = old_subscribers def subscribe(mlist, first_name, role=MemberRole.member): """Helper for subscribing a sample person to a mailing list.""" user_manager = getUtility(IUserManager) - address = '{0}person@example.com'.format(first_name[0].lower()) + email = '{0}person@example.com'.format(first_name[0].lower()) full_name = '{0} Person'.format(first_name) - person = user_manager.get_user(address) + person = user_manager.get_user(email) if person is None: - person = user_manager.create_user(address, full_name) - preferred_address = list(person.addresses)[0] - preferred_address.subscribe(mlist, role) + address = user_manager.get_address(email) + if address is None: + person = user_manager.create_user(email, full_name) + preferred_address = list(person.addresses)[0] + preferred_address.subscribe(mlist, role) + else: + address.subscribe(mlist, role) + else: + preferred_address = list(person.addresses)[0] + preferred_address.subscribe(mlist, role) config.db.commit() diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py index eac06e1cf..30bcdccc3 100644 --- a/src/mailman/tests/test_documentation.py +++ b/src/mailman/tests/test_documentation.py @@ -99,6 +99,9 @@ def stop(): def dump_msgdata(msgdata, *additional_skips): """Dump in a more readable way a message metadata dictionary.""" + if len(msgdata) == 0: + print '*Empty*' + return skips = set(additional_skips) # Some stuff we always want to skip, because their values will always be # variable data. |
