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