~barry/mailman/events-and-web

« back to all changes in this revision

Viewing changes to Mailman/docs/chains.txt

  • Committer: Barry Warsaw
  • Date: 2008-02-03 04:03:19 UTC
  • mfrom: (6581.1.27 rules)
  • Revision ID: barry@python.org-20080203040319-mnb1sar9bumaih01
Merge the 'rules' branch.

Give the first alpha a code name.

This branch mostly gets rid of all the approval oriented handlers in favor of
a chain-of-rules based approach.  This will be much more powerful and
extensible, allowing rule definition by plugin and chain creation via web
page.

When a message is processed by the incoming queue, it gets sent through a
chain of rules.  The starting chain is defined on the mailing list object, and
there is a built-in default starting chain, called 'built-in'.  Each chain is
made up of links, which describe a rule and an action, along with possibly
some other information.  Actions allow processing to take a detour through
another chain, jump to another chain, stop processing, run a function, etc.

The built-in chain essentially implements the original early part of the
handler pipeline.  If a message makes it through the built-in chain, it gets
sent to the prep queue, where the message is decorated and such before sending
out to the list membership.  The 'accept' chain is what moves the message into
the prep queue.

There are also 'hold', 'discard', and 'reject' chains, which do what you would
expect them to.  There are lots of built-in rules, implementing everything
from the old emergency handler to new handlers such as one not allowing empty
subject headers.

IMember grows an is_moderated attribute.

The 'adminapproved' metadata key is renamed 'moderator_approved'.

Fix some bogus uses of noreply_address to no_reply_address.

Stash an 'original_size' attribute on the message after parsing its plain
text.  This can be used later to ensure the original message does not exceed a
specified size without have to flatten the message again.

The KNOWN_SPAMMERS global variable is replaced with HEADER_MATCHES.  The
mailing list's header_filter_rules variable is replaced with header_matches
which has the same semantics as HEADER_MATCHES, but is list-specific.

DEFAULT_MAIL_COMMANDS_MAX_LINES -> EMAIL_COMMANDS_MAX_LINES.

Update smtplistener.py to be much better, to use maildir format instead of
mbox format, to respond to RSET commands by clearing the maildir, and by
silencing annoying asyncore error messages.

Extend the doctest runner so that it will run .txt files in any docs
subdirectory in the code tree.

Add plugable keys 'mailman.mta' and 'mailman.rules'.  The latter may have only
one setting while the former is extensible.

There are lots of doctests which should give all the gory details.

Mailman/Post.py -> Mailman/inject.py and the command line usage of this module
is removed.

SQLALCHEMY_ECHO, which was unused, is removed.

Backport the ability to specify additional footer interpolation variables by
the message metadata 'decoration-data' key.

can_acknowledge() defines whether a message can be responded to by the email
robot.

Simplify the implementation of _reset() based on Storm fixes.  Be able to
handle lists in Storm values.

Do some reorganization.

Show diffs side-by-side

added added

removed removed

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