summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2011-01-02 17:09:11 -0500
committerBarry Warsaw2011-01-02 17:09:11 -0500
commit00e2ef1c318e00cbf0f862ed839f6c7e44b1c0a9 (patch)
tree158da3cac60ec65e8a153c5fd46bee3934eb7b28
parent0fd3cd5393d319da4111b3e196c03ec67b0b9c66 (diff)
downloadmailman-00e2ef1c318e00cbf0f862ed839f6c7e44b1c0a9.tar.gz
mailman-00e2ef1c318e00cbf0f862ed839f6c7e44b1c0a9.tar.zst
mailman-00e2ef1c318e00cbf0f862ed839f6c7e44b1c0a9.zip
-rw-r--r--src/mailman/app/docs/chains.txt104
-rw-r--r--src/mailman/app/membership.py51
-rw-r--r--src/mailman/chains/base.py13
-rw-r--r--src/mailman/chains/builtin.py4
-rw-r--r--src/mailman/chains/docs/moderation.txt79
-rw-r--r--src/mailman/email/message.py3
-rw-r--r--src/mailman/queue/docs/incoming.txt37
-rw-r--r--src/mailman/rules/approved.py1
-rw-r--r--src/mailman/rules/docs/emergency.txt70
-rw-r--r--src/mailman/rules/docs/header-matching.txt4
-rw-r--r--src/mailman/rules/docs/moderation.txt140
-rw-r--r--src/mailman/rules/docs/rules.txt3
-rw-r--r--src/mailman/rules/moderation.py58
-rw-r--r--src/mailman/testing/helpers.py23
-rw-r--r--src/mailman/tests/test_documentation.py3
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.