~barry/mailman/events-and-web

« back to all changes in this revision

Viewing changes to src/mailman/rules/docs/approved.rst

  • Committer: Barry Warsaw
  • Date: 2012-04-05 05:43:40 UTC
  • Revision ID: barry@list.org-20120405054340-tcpcksw951pr76nr
 * A mailing list's *moderator password* is no longer stored in the clear; it
   is hashed with the currently selected scheme.

Also:

 - Simplify and rewrite the approved.rst doctest.  Now just document the good
   path, and only describe its functionality using the Approved: header, which
   is the recommended header.
 - Greatly expand the unittests for the approved rule.

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
Pre-approved postings
3
3
=====================
4
4
 
5
 
Messages can contain a pre-approval, which is used to bypass the message
6
 
approval queue.  This has several use cases:
7
 
 
8
 
- A list administrator can send an emergency message to the mailing list from
9
 
  an unregistered address, say if they are away from their normal email.
10
 
 
11
 
- An automated script can be programmed to send a message to an otherwise
12
 
  moderated list.
 
5
Messages can contain a pre-approval, which is used to bypass the normal
 
6
message approval queue.  This has several use cases:
 
7
 
 
8
  - A list administrator can send an emergency message to the mailing list
 
9
    from an unregistered address, for example if they are away from their
 
10
    normal email.
 
11
 
 
12
  - An automated script can be programmed to send a message to an otherwise
 
13
    moderated list.
13
14
 
14
15
In order to support this, a mailing list can be given a *moderator password*
15
16
which is shared among all the administrators.
16
17
 
17
 
    >>> mlist = create_list('_xtest@example.com')
18
 
    >>> mlist.moderator_password = 'abcxyz'
 
18
    >>> mlist = create_list('test@example.com')
 
19
 
 
20
This password will not be stored in clear text, so it must be hashed using the
 
21
configured hash protocol.
 
22
 
 
23
    >>> from flufl.password import lookup, make_secret
 
24
    >>> scheme = lookup(config.passwords.password_scheme.upper())
 
25
    >>> mlist.moderator_password = make_secret('super secret', scheme)
19
26
 
20
27
The ``approved`` rule determines whether the message contains the proper
21
28
approval or not.
28
35
No approval
29
36
===========
30
37
 
31
 
If the message has no ``Approve:`` or ``Approved:`` header (or their ``X-``
32
 
equivalents), then the rule does not match.
 
38
The preferred header to check for approval is ``Approved:``.  If the message
 
39
does not have this header, the rule will not match.
33
40
 
34
41
    >>> msg = message_from_string("""\
35
42
    ... From: aperson@example.com
39
46
    >>> rule.check(mlist, msg, {})
40
47
    False
41
48
 
42
 
If the message has an ``Approve:``, ``Approved:``, ``X-Approve:``, or
43
 
``X-Approved:`` header with a value that does not match the moderator
44
 
password, then the rule does not match.  However, the header is still removed.
45
 
::
46
 
 
47
 
    >>> msg['Approve'] = '12345'
48
 
    >>> rule.check(mlist, msg, {})
49
 
    False
50
 
    >>> print msg['approve']
51
 
    None
52
 
 
53
 
    >>> del msg['approve']
54
 
    >>> msg['Approved'] = '12345'
55
 
    >>> rule.check(mlist, msg, {})
56
 
    False
57
 
    >>> print msg['approved']
58
 
    None
59
 
 
60
 
    >>> del msg['approved']
61
 
    >>> msg['X-Approve'] = '12345'
62
 
    >>> rule.check(mlist, msg, {})
63
 
    False
64
 
    >>> print msg['x-approve']
65
 
    None
66
 
 
67
 
    >>> del msg['x-approve']
68
 
    >>> msg['X-Approved'] = '12345'
69
 
    >>> rule.check(mlist, msg, {})
70
 
    False
71
 
    >>> print msg['x-approved']
72
 
    None
73
 
 
74
 
    >>> del msg['x-approved']
75
 
 
76
 
 
77
 
Using an approval header
78
 
========================
79
 
 
80
 
If the moderator password is given in an ``Approve:`` header, then the rule
81
 
matches, and the ``Approve:`` header is stripped.
82
 
 
83
 
    >>> msg['Approve'] = 'abcxyz'
84
 
    >>> rule.check(mlist, msg, {})
85
 
    True
86
 
    >>> print msg['approve']
87
 
    None
88
 
 
89
 
Similarly, for the ``Approved:`` header.
90
 
 
91
 
    >>> del msg['approve']
92
 
    >>> msg['Approved'] = 'abcxyz'
93
 
    >>> rule.check(mlist, msg, {})
94
 
    True
95
 
    >>> print msg['approved']
96
 
    None
97
 
 
98
 
The headers ``X-Approve:`` and ``X-Approved:`` are treated the same way.
99
 
::
100
 
 
101
 
    >>> del msg['approved']
102
 
    >>> msg['X-Approve'] = 'abcxyz'
103
 
    >>> rule.check(mlist, msg, {})
104
 
    True
105
 
    >>> print msg['x-approve']
106
 
    None
107
 
 
108
 
    >>> del msg['x-approve']
109
 
    >>> msg['X-Approved'] = 'abcxyz'
110
 
    >>> rule.check(mlist, msg, {})
111
 
    True
112
 
    >>> print msg['x-approved']
113
 
    None
114
 
 
115
 
    >>> del msg['x-approved']
 
49
If the rule has an ``Approved`` header, but the value of this header does not
 
50
match the moderator password, the rule will not match.  Note that the header
 
51
must contain the clear text version of the password.
 
52
 
 
53
    >>> msg['Approved'] = 'not the password'
 
54
    >>> rule.check(mlist, msg, {})
 
55
    False
 
56
 
 
57
 
 
58
The message is approved
 
59
=======================
 
60
 
 
61
By adding an ``Approved`` header with a matching password, the rule will
 
62
match.
 
63
 
 
64
    >>> del msg['approved']
 
65
    >>> msg['Approved'] = 'super secret'
 
66
    >>> rule.check(mlist, msg, {})
 
67
    True
 
68
 
 
69
 
 
70
Alternative headers
 
71
===================
 
72
 
 
73
Other headers can be used to stash the moderator password.  This rule also
 
74
checks the ``Approve`` header.
 
75
 
 
76
    >>> del msg['approved']
 
77
    >>> msg['Approve'] = 'super secret'
 
78
    >>> rule.check(mlist, msg, {})
 
79
    True
 
80
 
 
81
Similarly, an ``X-Approved`` header can be used.
 
82
 
 
83
    >>> del msg['approve']
 
84
    >>> msg['X-Approved'] = 'super secret'
 
85
    >>> rule.check(mlist, msg, {})
 
86
    True
 
87
 
 
88
And finally, an ``X-Approve`` header can be used.
 
89
 
 
90
    >>> del msg['x-approved']
 
91
    >>> msg['X-Approve'] = 'super secret'
 
92
    >>> rule.check(mlist, msg, {})
 
93
    True
 
94
 
 
95
 
 
96
Removal of header
 
97
=================
 
98
 
 
99
Technically, rules should not have side-effects, however this rule does remove
 
100
the ``Approved`` header (LP: #973790) when it matches.
 
101
 
 
102
    >>> del msg['x-approved']
 
103
    >>> msg['Approved'] = 'super secret'
 
104
    >>> rule.check(mlist, msg, {})
 
105
    True
 
106
    >>> print msg['approved']
 
107
    None
 
108
 
 
109
It also removes the header when it doesn't match.  If the rule didn't do this,
 
110
then the mailing list could be probed for its moderator password.
 
111
 
 
112
    >>> msg['Approved'] = 'not the password'
 
113
    >>> rule.check(mlist, msg, {})
 
114
    False
 
115
    >>> print msg['approved']
 
116
    None
116
117
 
117
118
 
118
119
Using a pseudo-header
119
120
=====================
120
121
 
121
 
Different mail user agents have varying degrees to which they support custom
122
 
headers like ``Approve:`` and ``Approved:``.  For this reason, Mailman also
123
 
supports using a *pseudo-header*, which is really just the first
124
 
non-whitespace line in the payload of the message.  If this pseudo-header
125
 
looks like a matching ``Approve:`` or ``Approved:`` header, the message is
126
 
similarly allowed to pass.
127
 
 
128
 
    >>> msg = message_from_string("""\
129
 
    ... From: aperson@example.com
130
 
    ...
131
 
    ... Approve: abcxyz
132
 
    ... An important message.
133
 
    ... """)
134
 
    >>> rule.check(mlist, msg, {})
135
 
    True
136
 
 
137
 
The pseudo-header is removed.
138
 
 
139
 
    >>> print msg.as_string()
140
 
    From: aperson@example.com
141
 
    Content-Transfer-Encoding: 7bit
142
 
    MIME-Version: 1.0
143
 
    Content-Type: text/plain; charset="us-ascii"
144
 
    <BLANKLINE>
145
 
    An important message.
146
 
    <BLANKLINE>
147
 
 
148
 
Similarly for the ``Approved:`` header.
149
 
::
150
 
 
151
 
    >>> msg = message_from_string("""\
152
 
    ... From: aperson@example.com
153
 
    ...
154
 
    ... Approved: abcxyz
155
 
    ... An important message.
156
 
    ... """)
157
 
    >>> rule.check(mlist, msg, {})
158
 
    True
 
122
Mail programs have varying degrees to which they support custom headers like
 
123
``Approved:``.  For this reason, Mailman also supports using a
 
124
*pseudo-header*, which is really just the first non-whitespace line in the
 
125
payload of the message.  If this pseudo-header looks like a matching
 
126
``Approved:`` header, the message is similarly allowed to pass.
 
127
 
 
128
    >>> msg = message_from_string("""\
 
129
    ... From: aperson@example.com
 
130
    ...
 
131
    ... Approved: super secret
 
132
    ... An important message.
 
133
    ... """)
 
134
    >>> rule.check(mlist, msg, {})
 
135
    True
 
136
 
 
137
The pseudo-header is always removed from the body of plain text messages.
159
138
 
160
139
    >>> print msg.as_string()
161
140
    From: aperson@example.com
173
152
    >>> msg = message_from_string("""\
174
153
    ... From: aperson@example.com
175
154
    ...
176
 
    ... Approve: 123456
177
 
    ... An important message.
178
 
    ... """)
179
 
    >>> rule.check(mlist, msg, {})
180
 
    False
181
 
 
182
 
    >>> print msg.as_string()
183
 
    From: aperson@example.com
184
 
    Content-Transfer-Encoding: 7bit
185
 
    MIME-Version: 1.0
186
 
    Content-Type: text/plain; charset="us-ascii"
187
 
    <BLANKLINE>
188
 
    An important message.
189
 
    <BLANKLINE>
190
 
 
191
 
Similarly for the ``Approved:`` header.
192
 
::
193
 
 
194
 
    >>> msg = message_from_string("""\
195
 
    ... From: aperson@example.com
196
 
    ...
197
 
    ... Approved: 123456
 
155
    ... Approved: not the password
198
156
    ... An important message.
199
157
    ... """)
200
158
    >>> rule.check(mlist, msg, {})
225
183
    ... --AAA
226
184
    ... Content-Type: application/x-ignore
227
185
    ...
228
 
    ... Approve: 123456
 
186
    ... Approved: not the password
229
187
    ... The above line will be ignored.
230
188
    ...
231
189
    ... --AAA
232
190
    ... Content-Type: text/plain
233
191
    ...
234
 
    ... Approve: abcxyz
 
192
    ... Approved: super secret
235
193
    ... An important message.
236
194
    ... --AAA--
237
195
    ... """)
248
206
    --AAA
249
207
    Content-Type: application/x-ignore
250
208
    <BLANKLINE>
251
 
    Approve: 123456
252
 
    The above line will be ignored.
253
 
    <BLANKLINE>
254
 
    --AAA
255
 
    Content-Transfer-Encoding: 7bit
256
 
    MIME-Version: 1.0
257
 
    Content-Type: text/plain; charset="us-ascii"
258
 
    <BLANKLINE>
259
 
    An important message.
260
 
    --AAA--
261
 
    <BLANKLINE>
262
 
 
263
 
The same goes for the ``Approved:`` message.
264
 
 
265
 
    >>> msg = message_from_string("""\
266
 
    ... From: aperson@example.com
267
 
    ... MIME-Version: 1.0
268
 
    ... Content-Type: multipart/mixed; boundary="AAA"
269
 
    ...
270
 
    ... --AAA
271
 
    ... Content-Type: application/x-ignore
272
 
    ...
273
 
    ... Approved: 123456
274
 
    ... The above line will be ignored.
275
 
    ...
276
 
    ... --AAA
277
 
    ... Content-Type: text/plain
278
 
    ...
279
 
    ... Approved: abcxyz
280
 
    ... An important message.
281
 
    ... --AAA--
282
 
    ... """)
283
 
    >>> rule.check(mlist, msg, {})
284
 
    True
285
 
 
286
 
And the header is removed.
287
 
 
288
 
    >>> print msg.as_string()
289
 
    From: aperson@example.com
290
 
    MIME-Version: 1.0
291
 
    Content-Type: multipart/mixed; boundary="AAA"
292
 
    <BLANKLINE>
293
 
    --AAA
294
 
    Content-Type: application/x-ignore
295
 
    <BLANKLINE>
296
 
    Approved: 123456
297
 
    The above line will be ignored.
298
 
    <BLANKLINE>
299
 
    --AAA
300
 
    Content-Transfer-Encoding: 7bit
301
 
    MIME-Version: 1.0
302
 
    Content-Type: text/plain; charset="us-ascii"
303
 
    <BLANKLINE>
304
 
    An important message.
305
 
    --AAA--
306
 
    <BLANKLINE>
307
 
 
308
 
Here, the correct password is in the non-``text/plain`` part, so it is ignored.
309
 
 
310
 
    >>> msg = message_from_string("""\
311
 
    ... From: aperson@example.com
312
 
    ... MIME-Version: 1.0
313
 
    ... Content-Type: multipart/mixed; boundary="AAA"
314
 
    ...
315
 
    ... --AAA
316
 
    ... Content-Type: application/x-ignore
317
 
    ...
318
 
    ... Approve: abcxyz
319
 
    ... The above line will be ignored.
320
 
    ...
321
 
    ... --AAA
322
 
    ... Content-Type: text/plain
323
 
    ...
324
 
    ... Approve: 123456
325
 
    ... An important message.
326
 
    ... --AAA--
327
 
    ... """)
328
 
    >>> rule.check(mlist, msg, {})
329
 
    False
330
 
 
331
 
And yet the pseudo-header is still stripped.
332
 
 
333
 
    >>> print msg.as_string()
334
 
    From: aperson@example.com
335
 
    MIME-Version: 1.0
336
 
    Content-Type: multipart/mixed; boundary="AAA"
337
 
    <BLANKLINE>
338
 
    --AAA
339
 
    Content-Type: application/x-ignore
340
 
    <BLANKLINE>
341
 
    Approve: abcxyz
342
 
    The above line will be ignored.
343
 
    <BLANKLINE>
344
 
    --AAA
345
 
    Content-Transfer-Encoding: 7bit
346
 
    MIME-Version: 1.0
347
 
    Content-Type: text/plain; charset="us-ascii"
348
 
    <BLANKLINE>
349
 
    An important message.
350
 
    --AAA--
351
 
 
352
 
As before, the same goes for the ``Approved:`` header.
353
 
 
354
 
    >>> msg = message_from_string("""\
355
 
    ... From: aperson@example.com
356
 
    ... MIME-Version: 1.0
357
 
    ... Content-Type: multipart/mixed; boundary="AAA"
358
 
    ...
359
 
    ... --AAA
360
 
    ... Content-Type: application/x-ignore
361
 
    ...
362
 
    ... Approved: abcxyz
363
 
    ... The above line will be ignored.
364
 
    ...
365
 
    ... --AAA
366
 
    ... Content-Type: text/plain
367
 
    ...
368
 
    ... Approved: 123456
369
 
    ... An important message.
370
 
    ... --AAA--
371
 
    ... """)
372
 
    >>> rule.check(mlist, msg, {})
373
 
    False
374
 
 
375
 
And the pseudo-header is removed.
376
 
 
377
 
    >>> print msg.as_string()
378
 
    From: aperson@example.com
379
 
    MIME-Version: 1.0
380
 
    Content-Type: multipart/mixed; boundary="AAA"
381
 
    <BLANKLINE>
382
 
    --AAA
383
 
    Content-Type: application/x-ignore
384
 
    <BLANKLINE>
385
 
    Approved: abcxyz
 
209
    Approved: not the password
 
210
    The above line will be ignored.
 
211
    <BLANKLINE>
 
212
    --AAA
 
213
    Content-Transfer-Encoding: 7bit
 
214
    MIME-Version: 1.0
 
215
    Content-Type: text/plain; charset="us-ascii"
 
216
    <BLANKLINE>
 
217
    An important message.
 
218
    --AAA--
 
219
    <BLANKLINE>
 
220
 
 
221
If the correct password is in the non-``text/plain`` part, it is ignored.
 
222
 
 
223
    >>> msg = message_from_string("""\
 
224
    ... From: aperson@example.com
 
225
    ... MIME-Version: 1.0
 
226
    ... Content-Type: multipart/mixed; boundary="AAA"
 
227
    ...
 
228
    ... --AAA
 
229
    ... Content-Type: application/x-ignore
 
230
    ...
 
231
    ... Approved: super secret
 
232
    ... The above line will be ignored.
 
233
    ...
 
234
    ... --AAA
 
235
    ... Content-Type: text/plain
 
236
    ...
 
237
    ... Approved: not the password
 
238
    ... An important message.
 
239
    ... --AAA--
 
240
    ... """)
 
241
    >>> rule.check(mlist, msg, {})
 
242
    False
 
243
 
 
244
Pseudo-header is still stripped, but only from the ``text/plain`` part.
 
245
 
 
246
    >>> print msg.as_string()
 
247
    From: aperson@example.com
 
248
    MIME-Version: 1.0
 
249
    Content-Type: multipart/mixed; boundary="AAA"
 
250
    <BLANKLINE>
 
251
    --AAA
 
252
    Content-Type: application/x-ignore
 
253
    <BLANKLINE>
 
254
    Approved: super secret
386
255
    The above line will be ignored.
387
256
    <BLANKLINE>
388
257
    --AAA
397
266
Stripping text/html parts
398
267
=========================
399
268
 
400
 
Because some mail readers will include both a ``text/plain`` part and a
401
 
``text/html`` alternative, the ``approved`` rule has to search the
402
 
alternatives and strip anything that looks like an ``Approve:`` or
403
 
``Approved:`` headers.
 
269
Because some mail programs will include both a ``text/plain`` part and a
 
270
``text/html`` alternative, the rule must search the alternatives and strip
 
271
anything that looks like an ``Approved:`` header.
404
272
 
405
273
    >>> msg = message_from_string("""\
406
274
    ... From: aperson@example.com
413
281
    ... <html>
414
282
    ... <head></head>
415
283
    ... <body>
416
 
    ... <b>Approved: abcxyz</b>
 
284
    ... <b>Approved: super secret</b>
417
285
    ... <p>The above line will be ignored.
418
286
    ... </body>
419
287
    ... </html>
421
289
    ... --AAA
422
290
    ... Content-Type: text/plain
423
291
    ...
424
 
    ... Approved: abcxyz
 
292
    ... Approved: super secret
425
293
    ... An important message.
426
294
    ... --AAA--
427
295
    ... """)
457
325
    --AAA--
458
326
    <BLANKLINE>
459
327
 
460
 
This is true even if the rule does not match.
 
328
This is true even if the rule does not match (i.e. the incorrect password was
 
329
given).
461
330
::
462
331
 
463
332
    >>> msg = message_from_string("""\
471
340
    ... <html>
472
341
    ... <head></head>
473
342
    ... <body>
474
 
    ... <b>Approve: 123456</b>
 
343
    ... <b>Approved: not the password</b>
475
344
    ... <p>The above line will be ignored.
476
345
    ... </body>
477
346
    ... </html>
479
348
    ... --AAA
480
349
    ... Content-Type: text/plain
481
350
    ...
482
 
    ... Approve: 123456
 
351
    ... Approved: not the password
483
352
    ... An important message.
484
353
    ... --AAA--
485
354
    ... """)