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.
8
There are a number of built-in chains available that act as end-points in the
9
processing of messages.
15
The Discard chain simply throws the message away.
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)
26
u'Discard a message and stop processing.'
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>
36
... An important message.
39
>>> from Mailman.app.chains import process
41
# XXX This checks the vette log file because there is no other evidence
42
# that this chain has done anything.
44
>>> fp = open(os.path.join(config.LOG_DIR, 'vette'))
45
>>> file_pos = fp.tell()
46
>>> process(mlist, msg, {}, 'discard')
48
>>> print 'LOG:', fp.read()
49
LOG: ... DISCARD: <first>
56
The Reject chain bounces the message back to the original sender, and logs
59
>>> chain = config.chains['reject']
60
>>> verifyObject(IChain, chain)
65
u'Reject/bounce a message and stop processing.'
66
>>> file_pos = fp.tell()
67
>>> process(mlist, msg, {}, 'reject')
69
>>> print 'LOG:', fp.read()
70
LOG: ... REJECT: <first>
72
The bounce message is now sitting in the Virgin queue.
74
>>> from Mailman.queue import Switchboard
75
>>> virginq = Switchboard(config.VIRGINQUEUE_DIR)
76
>>> len(virginq.files)
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
84
[No bounce details are available]
86
Content-Type: message/rfc822
89
From: aperson@example.com
90
To: _xtest@example.com
91
Subject: My first post
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.
106
>>> mlist.web_page_url = u'http://www.example.com/'
107
>>> chain = config.chains['hold']
108
>>> verifyObject(IChain, chain)
112
>>> chain.description
113
u'Hold a message and stop processing.'
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
123
There are now two messages in the Virgin queue, one to the list moderators and
124
one to the original author.
126
>>> len(virginq.files)
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'))
136
This message is addressed to the mailing list moderators.
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
144
As list administrator, your authorization is requested for the
145
following mailing list posting:
147
List: _xtest@example.com
148
From: aperson@example.com
149
Subject: My first post
152
At your convenience, visit:
154
http://www.example.com/admindb/_xtest@example.com
156
to approve or deny the request.
159
Content-Type: message/rfc822
162
From: aperson@example.com
163
To: _xtest@example.com
164
Subject: My first post
166
X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
168
An important message.
171
Content-Type: message/rfc822
174
Content-Type: text/plain; charset="us-ascii"
176
Content-Transfer-Encoding: 7bit
178
Sender: _xtest-request@example.com
179
From: _xtest-request@example.com
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.
190
This message is addressed to the sender of the message.
192
>>> print qfiles[1].as_string()
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
200
Your mail to '_xtest@example.com' with the subject
204
Is being held until the list moderator can review it for approval.
206
The reason it is being held:
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:
214
http://www.example.com/confirm/_xtest@example.com/...
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.
226
>>> for line in qfiles[1].get_payload().splitlines():
227
... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line)
229
... cookie = mo.group('cookie')
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')]
236
The message itself is held in the message store.
238
>>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request(
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
247
X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
249
An important message.
256
The Accept chain sends the message on the 'prep' queue, where it will be
257
processed and sent on to the list membership.
259
>>> chain = config.chains['accept']
260
>>> verifyObject(IChain, chain)
264
>>> chain.description
266
>>> file_pos = fp.tell()
267
>>> process(mlist, msg, {}, 'accept')
268
>>> fp.seek(file_pos)
269
>>> print 'LOG:', fp.read()
270
LOG: ... ACCEPT: <first>
272
>>> prepq = Switchboard(config.PREPQUEUE_DIR)
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
281
X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
283
An important message.
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.
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.
300
>>> chain = config.chains['built-in']
301
>>> verifyObject(IChain, chain)
305
>>> chain.description
306
u'The built-in moderation chain.'
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.
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>
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
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;
329
An important message.
332
In addition, the message metadata now contains lists of all rules that have
333
hit and all rules that have missed.
335
>>> sorted(qdata['rule_hits'])
337
>>> sorted(qdata['rule_misses'])
338
['administrivia', 'approved', 'emergency', 'implicit-dest', 'loop',
339
'max-recipients', 'max-size', 'news-moderation', 'no-subject',