summaryrefslogtreecommitdiff
path: root/src/mailman/core/docs/chains.rst
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/core/docs/chains.rst')
-rw-r--r--src/mailman/core/docs/chains.rst344
1 files changed, 344 insertions, 0 deletions
diff --git a/src/mailman/core/docs/chains.rst b/src/mailman/core/docs/chains.rst
new file mode 100644
index 000000000..a79999de0
--- /dev/null
+++ b/src/mailman/core/docs/chains.rst
@@ -0,0 +1,344 @@
+======
+Chains
+======
+
+When a new message is posted to a mailing list, Mailman uses a set of rule
+chains to decide whether the message gets accepted for posting, rejected,
+discarded, or held for moderator approval.
+
+There are a number of built-in chains available that act as end-points in the
+processing of messages.
+
+
+The Discard chain
+=================
+
+The `discard` chain simply throws the message away.
+::
+
+ >>> chain = config.chains['discard']
+ >>> print(chain.name)
+ discard
+ >>> print(chain.description)
+ Discard a message and stop processing.
+
+ >>> mlist = create_list('test@example.com')
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: test@example.com
+ ... Subject: My first post
+ ... Message-ID: <first>
+ ...
+ ... An important message.
+ ... """)
+
+ >>> def print_msgid(event):
+ ... print('{0}: {1}'.format(
+ ... event.chain.name.upper(), event.msg.get('message-id', 'n/a')))
+
+ >>> from mailman.core.chains import process
+ >>> from mailman.testing.helpers import event_subscribers
+
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {}, 'discard')
+ DISCARD: <first>
+
+
+The Reject chain
+================
+
+The `reject` chain bounces the message back to the original sender, and logs
+this action.
+::
+
+ >>> chain = config.chains['reject']
+ >>> print(chain.name)
+ reject
+ >>> print(chain.description)
+ Reject/bounce a message and stop processing.
+
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {}, 'reject')
+ REJECT: <first>
+
+The bounce message is now sitting in the `virgin` queue.
+
+ >>> from mailman.testing.helpers import get_queue_messages
+ >>> qfiles = get_queue_messages('virgin')
+ >>> len(qfiles)
+ 1
+ >>> print(qfiles[0].msg.as_string())
+ Subject: My first post
+ From: test-owner@example.com
+ To: aperson@example.com
+ ...
+ [No bounce details are available]
+ ...
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ From: aperson@example.com
+ To: test@example.com
+ Subject: My first post
+ Message-ID: <first>
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+ ...
+
+
+The Hold Chain
+==============
+
+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']
+ >>> print(chain.name)
+ hold
+ >>> print(chain.description)
+ Hold a message and stop processing.
+
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {}, 'hold')
+ HOLD: <first>
+
+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
+
+One of the message is addressed to the mailing list moderators, and the other
+is addressed to the original sender.
+
+ >>> 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: test@example.com
+ From: aperson@example.com
+ Subject: My first post
+ Reason: XXX
+ <BLANKLINE>
+ At your convenience, visit:
+ <BLANKLINE>
+ http://lists.example.com/admindb/test@example.com
+ <BLANKLINE>
+ to approve or deny the request.
+ <BLANKLINE>
+ ...
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ From: aperson@example.com
+ To: test@example.com
+ Subject: My first post
+ Message-ID: <first>
+ X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+ ...
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Subject: confirm ...
+ From: test-request@example.com
+ ...
+ <BLANKLINE>
+ If you reply to this message, keeping the Subject: header intact,
+ Mailman will discard the held message. Do this if the message is
+ spam. If you reply to this message and include an Approved: header
+ with the list password in it, the message will be approved for posting
+ to the list. The Approved: header can also appear in the first line
+ of the body of the reply.
+ ...
+
+This message is addressed to the sender of the message.
+
+ >>> print(messages[1].as_string())
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Your message to test@example.com awaits moderator approval
+ From: test-bounces@example.com
+ To: aperson@example.com
+ ...
+ Your mail to 'test@example.com' with the subject
+ <BLANKLINE>
+ My first post
+ <BLANKLINE>
+ Is being held until the list moderator can review it for approval.
+ <BLANKLINE>
+ The reason it is being held:
+ <BLANKLINE>
+ XXX
+ <BLANKLINE>
+ Either the message will get posted to the list, or you will receive
+ 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/test@example.com/...
+ <BLANKLINE>
+ <BLANKLINE>
+
+In addition, the pending database is holding the original messages, waiting
+for them to be disposed of by the original author or the list moderators. The
+database is essentially a dictionary, with the keys being the randomly
+selected tokens included in the urls and the values being a 2-tuple where the
+first item is a type code and the second item is a message id.
+::
+
+ >>> import re
+ >>> cookie = None
+ >>> for line in messages[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'
+
+ >>> from mailman.interfaces.pending import IPendings
+ >>> from zope.component import getUtility
+
+ >>> data = getUtility(IPendings).confirm(cookie)
+ >>> dump_msgdata(data)
+ id : 1
+ type: held message
+
+The message itself is held in the message store.
+::
+
+ >>> from mailman.interfaces.requests import IListRequests
+ >>> list_requests = IListRequests(mlist)
+ >>> rkey, rdata = list_requests.get_request(data['id'])
+
+ >>> from mailman.interfaces.messages import IMessageStore
+ >>> from zope.component import getUtility
+ >>> msg = getUtility(IMessageStore).get_message_by_id(
+ ... rdata['_mod_message_id'])
+
+ >>> print(msg.as_string())
+ From: aperson@example.com
+ To: test@example.com
+ Subject: My first post
+ Message-ID: <first>
+ X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+
+
+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']
+ >>> print(chain.name)
+ accept
+ >>> print(chain.description)
+ Accept a message.
+
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {}, 'accept')
+ ACCEPT: <first>
+
+ >>> qfiles = get_queue_messages('pipeline')
+ >>> len(qfiles)
+ 1
+ >>> print(qfiles[0].msg.as_string())
+ From: aperson@example.com
+ To: test@example.com
+ Subject: My first post
+ Message-ID: <first>
+ X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+
+
+Run-time chains
+===============
+
+We can also define chains at run time, and these chains can be mutated.
+Run-time chains are made up of links where each link associates both a rule
+and a `jump`. The rule is really a rule name, which is looked up when
+needed. The jump names a chain which is jumped to if the rule matches.
+
+There is one built-in posting chain. This is the default chain to use when no
+other input chain is defined for a mailing list. It runs through the default
+rules.
+
+ >>> chain = config.chains['default-posting-chain']
+ >>> print(chain.name)
+ default-posting-chain
+ >>> print(chain.description)
+ The built-in moderation chain.
+
+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')
+ <Member: aperson@example.com on test@example.com as MemberRole.member>
+
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {})
+ ACCEPT: <first>
+
+ >>> qfiles = get_queue_messages('pipeline')
+ >>> len(qfiles)
+ 1
+ >>> print(qfiles[0].msg.as_string())
+ From: aperson@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; member-moderation;
+ administrivia; implicit-dest; max-recipients; max-size;
+ news-moderation; no-subject; suspicious-header; nonmember-moderation
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+
+In addition, the message metadata now contains lists of all rules that have
+hit and all rules that have missed.
+
+ >>> dump_list(qfiles[0].msgdata['rule_hits'])
+ *Empty*
+ >>> dump_list(qfiles[0].msgdata['rule_misses'])
+ administrivia
+ approved
+ emergency
+ implicit-dest
+ loop
+ max-recipients
+ max-size
+ member-moderation
+ news-moderation
+ no-subject
+ nonmember-moderation
+ suspicious-header