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.
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)
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>
35 ... An important message.
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.
43 >>> fp = open(os.path.join(config.LOG_DIR, 'vette'))
44 >>> file_pos = fp.tell()
45 >>> process(mlist, msg, {}, 'discard')
47 >>> print 'LOG:', fp.read()
48 LOG: ... DISCARD: <first>
55 The Reject chain bounces the message back to the original sender, and logs
58 >>> chain = config.chains['reject']
59 >>> verifyObject(IChain, chain)
63 >>> print chain.description
64 Reject/bounce a message and stop processing.
65 >>> file_pos = fp.tell()
66 >>> process(mlist, msg, {}, 'reject')
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)
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
82 [No bounce details are available]
84 Content-Type: message/rfc822
87 From: aperson@example.com
88 To: _xtest@example.com
89 Subject: My first post
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)
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
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)
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
141 As list administrator, your authorization is requested for the
142 following mailing list posting:
144 List: _xtest@example.com
145 From: aperson@example.com
146 Subject: My first post
149 At your convenience, visit:
151 http://lists.example.com/admindb/_xtest@example.com
153 to approve or deny the request.
156 Content-Type: message/rfc822
159 From: aperson@example.com
160 To: _xtest@example.com
161 Subject: My first post
163 X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
165 An important message.
168 Content-Type: message/rfc822
171 Content-Type: text/plain; charset="us-ascii"
173 Content-Transfer-Encoding: 7bit
175 Sender: _xtest-request@example.com
176 From: _xtest-request@example.com
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.
187 This message is addressed to the sender of the message.
189 >>> print qfiles[1].as_string()
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
197 Your mail to '_xtest@example.com' with the subject
201 Is being held until the list moderator can review it for approval.
203 The reason it is being held:
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:
211 http://lists.example.com/confirm/_xtest@example.com/...
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.
223 >>> for line in qfiles[1].get_payload().splitlines():
224 ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line)
226 ... cookie = mo.group('cookie')
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
253 X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
255 An important message.
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)
270 >>> print chain.description
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)
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
287 X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
289 An important message.
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)
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
329 X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
330 X-Mailman-Rule-Misses: approved; emergency; loop; administrivia;
332 max-recipients; max-size; news-moderation; no-subject;
335 An important message.
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'])
343 >>> for rule_name in sorted(qdata['rule_misses']):