Cleanup.
[mailman.git] / src / mailman / docs / chains.txt
blobf9ed156b1f94d8b32ed41cc450ba342162bf246d
1 ======
2 Chains
3 ======
5 When a new message comes into the system, Mailman uses a set of rule chains to
6 decide whether the message gets posted to the list, rejected, discarded, or
7 held for moderator approval.
9 There are a number of built-in chains available that act as end-points in the
10 processing of messages.
13 The Discard chain
14 =================
16 The Discard chain simply throws the message away.
18     >>> from zope.interface.verify import verifyObject
19     >>> from mailman.interfaces.chain import IChain
20     >>> chain = config.chains['discard']
21     >>> verifyObject(IChain, chain)
22     True
23     >>> print chain.name
24     discard
25     >>> print chain.description
26     Discard a message and stop processing.
28     >>> mlist = create_list('_xtest@example.com')
29     >>> msg = message_from_string("""\
30     ... From: aperson@example.com
31     ... To: _xtest@example.com
32     ... Subject: My first post
33     ... Message-ID: <first>
34     ...
35     ... An important message.
36     ... """)
38     >>> from mailman.core.chains import process
40     # XXX This checks the vette log file because there is no other evidence
41     # that this chain has done anything.
42     >>> import os
43     >>> fp = open(os.path.join(config.LOG_DIR, 'vette'))
44     >>> file_pos = fp.tell()
45     >>> process(mlist, msg, {}, 'discard')
46     >>> fp.seek(file_pos)
47     >>> print 'LOG:', fp.read()
48     LOG: ... DISCARD: <first>
49     <BLANKLINE>
52 The Reject chain
53 ================
55 The Reject chain bounces the message back to the original sender, and logs
56 this action.
58     >>> chain = config.chains['reject']
59     >>> verifyObject(IChain, chain)
60     True
61     >>> print chain.name
62     reject
63     >>> print chain.description
64     Reject/bounce a message and stop processing.
65     >>> file_pos = fp.tell()
66     >>> process(mlist, msg, {}, 'reject')
67     >>> fp.seek(file_pos)
68     >>> print 'LOG:', fp.read()
69     LOG: ... REJECT: <first>
71 The bounce message is now sitting in the Virgin queue.
73     >>> virginq = config.switchboards['virgin']
74     >>> len(virginq.files)
75     1
76     >>> qmsg, qdata = virginq.dequeue(virginq.files[0])
77     >>> print qmsg.as_string()
78     Subject: My first post
79     From: _xtest-owner@example.com
80     To: aperson@example.com
81     ...
82     [No bounce details are available]
83     ...
84     Content-Type: message/rfc822
85     MIME-Version: 1.0
86     <BLANKLINE>
87     From: aperson@example.com
88     To: _xtest@example.com
89     Subject: My first post
90     Message-ID: <first>
91     <BLANKLINE>
92     An important message.
93     <BLANKLINE>
94     ...
97 The Hold Chain
98 ==============
100 The Hold chain places the message into the admin request database and
101 depending on the list's settings, sends a notification to both the original
102 sender and the list moderators.
104     >>> chain = config.chains['hold']
105     >>> verifyObject(IChain, chain)
106     True
107     >>> print chain.name
108     hold
109     >>> print chain.description
110     Hold a message and stop processing.
112     >>> file_pos = fp.tell()
113     >>> process(mlist, msg, {}, 'hold')
114     >>> fp.seek(file_pos)
115     >>> print 'LOG:', fp.read()
116     LOG: ... HOLD: _xtest@example.com post from aperson@example.com held,
117         message-id=<first>: n/a
118     <BLANKLINE>
120 There are now two messages in the Virgin queue, one to the list moderators and
121 one to the original author.
123     >>> len(virginq.files)
124     2
125     >>> qfiles = []
126     >>> for filebase in virginq.files:
127     ...     qmsg, qdata = virginq.dequeue(filebase)
128     ...     virginq.finish(filebase)
129     ...     qfiles.append(qmsg)
130     >>> from operator import itemgetter
131     >>> qfiles.sort(key=itemgetter('to'))
133 This message is addressed to the mailing list moderators.
135     >>> print qfiles[0].as_string()
136     Subject: _xtest@example.com post from aperson@example.com requires approval
137     From: _xtest-owner@example.com
138     To: _xtest-owner@example.com
139     MIME-Version: 1.0
140     ...
141     As list administrator, your authorization is requested for the
142     following mailing list posting:
143     <BLANKLINE>
144         List:    _xtest@example.com
145         From:    aperson@example.com
146         Subject: My first post
147         Reason:  XXX
148     <BLANKLINE>
149     At your convenience, visit:
150     <BLANKLINE>
151         http://lists.example.com/admindb/_xtest@example.com
152     <BLANKLINE>
153     to approve or deny the request.
154     <BLANKLINE>
155     ...
156     Content-Type: message/rfc822
157     MIME-Version: 1.0
158     <BLANKLINE>
159     From: aperson@example.com
160     To: _xtest@example.com
161     Subject: My first post
162     Message-ID: <first>
163     X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
164     <BLANKLINE>
165     An important message.
166     <BLANKLINE>
167     ...
168     Content-Type: message/rfc822
169     MIME-Version: 1.0
170     <BLANKLINE>
171     Content-Type: text/plain; charset="us-ascii"
172     MIME-Version: 1.0
173     Content-Transfer-Encoding: 7bit
174     Subject: confirm ...
175     Sender: _xtest-request@example.com
176     From: _xtest-request@example.com
177     ...
178     <BLANKLINE>
179     If you reply to this message, keeping the Subject: header intact,
180     Mailman will discard the held message.  Do this if the message is
181     spam.  If you reply to this message and include an Approved: header
182     with the list password in it, the message will be approved for posting
183     to the list.  The Approved: header can also appear in the first line
184     of the body of the reply.
185     ...
187 This message is addressed to the sender of the message.
189     >>> print qfiles[1].as_string()
190     MIME-Version: 1.0
191     Content-Type: text/plain; charset="us-ascii"
192     Content-Transfer-Encoding: 7bit
193     Subject: Your message to _xtest@example.com awaits moderator approval
194     From: _xtest-bounces@example.com
195     To: aperson@example.com
196     ...
197     Your mail to '_xtest@example.com' with the subject
198     <BLANKLINE>
199         My first post
200     <BLANKLINE>
201     Is being held until the list moderator can review it for approval.
202     <BLANKLINE>
203     The reason it is being held:
204     <BLANKLINE>
205         XXX
206     <BLANKLINE>
207     Either the message will get posted to the list, or you will receive
208     notification of the moderator's decision.  If you would like to cancel
209     this posting, please visit the following URL:
210     <BLANKLINE>
211         http://lists.example.com/confirm/_xtest@example.com/...
212     <BLANKLINE>
213     <BLANKLINE>
215 In addition, the pending database is holding the original messages, waiting
216 for them to be disposed of by the original author or the list moderators.  The
217 database is essentially a dictionary, with the keys being the randomly
218 selected tokens included in the urls and the values being a 2-tuple where the
219 first item is a type code and the second item is a message id.
221     >>> import re
222     >>> cookie = None
223     >>> for line in qfiles[1].get_payload().splitlines():
224     ...     mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line)
225     ...     if mo:
226     ...         cookie = mo.group('cookie')
227     ...         break
228     >>> assert cookie is not None, 'No confirmation token found'
230     >>> from mailman.interfaces.pending import IPendings
231     >>> from zope.component import getUtility
233     >>> data = getUtility(IPendings).confirm(cookie)
234     >>> sorted(data.items())
235     [(u'id', ...), (u'type', u'held message')]
237 The message itself is held in the message store.
239     >>> from mailman.interfaces.requests import IRequests
240     >>> list_requests = getUtility(IRequests).get_list_requests(mlist)
241     >>> rkey, rdata = list_requests.get_request(data['id'])
243     >>> from mailman.interfaces.messages import IMessageStore
244     >>> from zope.component import getUtility
245     >>> msg = getUtility(IMessageStore).get_message_by_id(
246     ...     rdata['_mod_message_id'])
248     >>> print msg.as_string()
249     From: aperson@example.com
250     To: _xtest@example.com
251     Subject: My first post
252     Message-ID: <first>
253     X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
254     <BLANKLINE>
255     An important message.
256     <BLANKLINE>
259 The Accept chain
260 ================
262 The Accept chain sends the message on the 'prep' queue, where it will be
263 processed and sent on to the list membership.
265     >>> chain = config.chains['accept']
266     >>> verifyObject(IChain, chain)
267     True
268     >>> print chain.name
269     accept
270     >>> print chain.description
271     Accept a message.
272     >>> file_pos = fp.tell()
273     >>> process(mlist, msg, {}, 'accept')
274     >>> fp.seek(file_pos)
275     >>> print 'LOG:', fp.read()
276     LOG: ... ACCEPT: <first>
278     >>> pipelineq = config.switchboards['pipeline']
279     >>> len(pipelineq.files)
280     1
281     >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
282     >>> print qmsg.as_string()
283     From: aperson@example.com
284     To: _xtest@example.com
285     Subject: My first post
286     Message-ID: <first>
287     X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
288     <BLANKLINE>
289     An important message.
290     <BLANKLINE>
293 Run-time chains
294 ===============
296 We can also define chains at run time, and these chains can be mutated.
297 Run-time chains are made up of links where each link associates both a rule
298 and a 'jump'.  The rule is really a rule name, which is looked up when
299 needed.  The jump names a chain which is jumped to if the rule matches.
301 There is one built-in run-time chain, called appropriately 'built-in'.  This
302 is the default chain to use when no other input chain is defined for a mailing
303 list.  It runs through the default rules, providing functionality similar to
304 the Hold handler from previous versions of Mailman.
306     >>> chain = config.chains['built-in']
307     >>> verifyObject(IChain, chain)
308     True
309     >>> print chain.name
310     built-in
311     >>> print chain.description
312     The built-in moderation chain.
314 The previously created message is innocuous enough that it should pass through
315 all default rules.  This message will end up in the pipeline queue.
317     >>> file_pos = fp.tell()
318     >>> process(mlist, msg, {})
319     >>> fp.seek(file_pos)
320     >>> print 'LOG:', fp.read()
321     LOG: ... ACCEPT: <first>
323     >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
324     >>> print qmsg.as_string()
325     From: aperson@example.com
326     To: _xtest@example.com
327     Subject: My first post
328     Message-ID: <first>
329     X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
330     X-Mailman-Rule-Misses: approved; emergency; loop; administrivia;
331         implicit-dest;
332         max-recipients; max-size; news-moderation; no-subject;
333         suspicious-header
334     <BLANKLINE>
335     An important message.
336     <BLANKLINE>
338 In addition, the message metadata now contains lists of all rules that have
339 hit and all rules that have missed.
341     >>> sorted(qdata['rule_hits'])
342     []
343     >>> for rule_name in sorted(qdata['rule_misses']):
344     ...     print rule_name
345     administrivia
346     approved
347     emergency
348     implicit-dest
349     loop
350     max-recipients
351     max-size
352     news-moderation
353     no-subject
354     suspicious-header