1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
|
==================
Message decoration
==================
Message decoration is the process of adding headers and footers to the
original message. A handler module takes care of this based on the settings
of the mailing list and the type of message being processed.
>>> mlist = create_list('ant@example.com')
>>> msg_text = """\
... From: aperson@example.org
...
... Here is a message.
... """
>>> msg = message_from_string(msg_text)
Short circuiting
================
Digest messages get decorated during the digest creation phase so no extra
decorations are added for digest messages.
::
>>> from mailman.handlers.decorate import process
>>> process(mlist, msg, dict(isdigest=True))
>>> print(msg.as_string())
From: aperson@example.org
<BLANKLINE>
Here is a message.
>>> process(mlist, msg, dict(nodecorate=True))
>>> print(msg.as_string())
From: aperson@example.org
<BLANKLINE>
Here is a message.
Simple decorations
==================
Message decorations are specified by URI and can be specialized by the mailing
list and language. Internal Mailman decorations can be referenced by using
the ``mailman:///`` URL scheme. Here we create a simple English header and
footer for all mailing lists in our site.
::
>>> import os, tempfile
>>> template_dir = tempfile.mkdtemp()
>>> site_dir = os.path.join(template_dir, 'site', 'en')
>>> os.makedirs(site_dir)
>>> config.push('templates', """
... [paths.testing]
... template_dir: {}
... """.format(template_dir))
>>> myheader_path = os.path.join(site_dir, 'myheader.txt')
>>> with open(myheader_path, 'w') as fp:
... print('header', file=fp)
>>> myfooter_path = os.path.join(site_dir, 'myfooter.txt')
>>> with open(myfooter_path, 'w') as fp:
... print('footer', file=fp)
Adding these template URIs to the template manager sets the mailing list up to
use these templates. Since these are site-global templates, we can use a
shorter path.
>>> from mailman.interfaces.template import ITemplateManager
>>> from zope.component import getUtility
>>> manager = getUtility(ITemplateManager)
>>> manager.set('list:member:regular:header',
... mlist.list_id, 'mailman:///myheader.txt')
>>> manager.set('list:member:regular:footer',
... mlist.list_id, 'mailman:///myfooter.txt')
Text messages that have no declared content type are, by default encoded in
ASCII. When the mailing list's preferred language is ``en`` (i.e. English),
the character set of the mailing list and of the message will match, allowing
Mailman to simply prepend the header and append the footer verbatim.
>>> mlist.preferred_language = 'en'
>>> process(mlist, msg, {})
>>> print(msg.as_string())
From: aperson@example.org
...
<BLANKLINE>
header
Here is a message.
footer
Mailman supports a number of interpolation variables, placeholders in the
header and footer for information to be filled in with mailing list specific
data. An example of such information is the mailing list's `real name` (a
short descriptive name for the mailing list).
::
>>> with open(myheader_path, 'w') as fp:
... print('$display_name header', file=fp)
>>> with open(myfooter_path, 'w') as fp:
... print('$display_name footer', file=fp)
>>> msg = message_from_string(msg_text)
>>> mlist.display_name = 'Ant'
>>> process(mlist, msg, {})
>>> print(msg.as_string())
From: aperson@example.org
...
Ant header
Here is a message.
Ant footer
You can't just pick any interpolation variable though; if you do, the variable
will remain in the header or footer unchanged.
::
>>> with open(myheader_path, 'w') as fp:
... print('$dummy header', file=fp)
>>> with open(myfooter_path, 'w') as fp:
... print('$dummy footer', file=fp)
>>> msg = message_from_string(msg_text)
>>> process(mlist, msg, {})
>>> print(msg.as_string())
From: aperson@example.org
...
$dummy header
Here is a message.
$dummy footer
Adding archiver permalink URLs in the message footer
====================================================
You can add links to archived messages in the footer using special placeholder
variables. For all available and enabled archiver for the mailing list, use a
placeholder of the format ``$<archiver_name>_url``. For example, if you have
HyperKitty enabled you can add ``${hyperkitty_url}`` to point to the message
in HyperKitty.
Handling RFC 3676 'format=flowed' parameters
============================================
RFC 3676 describes a standard by which text/plain messages can marked by
generating MUAs for better readability in compatible receiving MUAs. The
``format`` parameter on the text/plain ``Content-Type`` header gives hints as
to how the receiving MUA may flow and delete trailing whitespace for better
display in a proportional font.
When Mailman sees text/plain messages with such RFC 3676 parameters, it
preserves these parameters when it concatenates headers and footers to the
message payload.
::
>>> with open(myheader_path, 'w') as fp:
... print('header', file=fp)
>>> with open(myfooter_path, 'w') as fp:
... print('footer', file=fp)
>>> mlist.preferred_language = 'en'
>>> msg = message_from_string("""\
... From: aperson@example.org
... Content-Type: text/plain; format=flowed; delsp=no
...
... Here is a message\x20
... with soft line breaks.
... """)
>>> process(mlist, msg, {})
>>> # Don't use 'print' here as above because it won't be obvious from the
>>> # output that the soft-line break space at the end of the 'Here is a
>>> # message' line will be retained in the output.
>>> print(msg['content-type'])
text/plain; format="flowed"; delsp="no"; charset="us-ascii"
>>> for line in msg.get_payload().splitlines():
... print('>{0}<'.format(line))
>header<
>Here is a message <
>with soft line breaks.<
>footer<
Decorating mixed-charset messages
=================================
When a message has no explicit character set, it is assumed to be ASCII.
However, if the mailing list's preferred language has a different character
set, Mailman will still try to concatenate the header and footer, but it will
convert the text to utf-8 and base-64 encode the message payload.
::
# 'ja' = Japanese; charset = 'euc-jp'
>>> mlist.preferred_language = 'ja'
>>> with open(myheader_path, 'w') as fp:
... print('$description header', file=fp)
>>> with open(myfooter_path, 'w') as fp:
... print('$description footer', file=fp)
>>> mlist.description = '\u65e5\u672c\u8a9e'
>>> from email.message import Message
>>> msg = Message()
>>> msg.set_payload('Fran\xe7aise', 'iso-8859-1')
>>> print(msg.as_string())
MIME-Version: 1.0
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
<BLANKLINE>
Fran=E7aise
>>> process(mlist, msg, {})
>>> print(msg.as_string())
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: base64
<BLANKLINE>
5pel5pys6KqeIGhlYWRlcgpGcmFuw6dhaXNlCuaXpeacrOiqniBmb290ZXIK
Sometimes the message even has an unknown character set. In this case,
Mailman has no choice but to decorate the original message with MIME
attachments.
::
>>> mlist.preferred_language = 'en'
>>> with open(myheader_path, 'w') as fp:
... print('header', file=fp)
>>> with open(myfooter_path, 'w') as fp:
... print('footer', file=fp)
>>> msg = message_from_string("""\
... From: aperson@example.org
... Content-Type: text/plain; charset=unknown
... Content-Transfer-Encoding: 7bit
...
... Here is a message.
... """)
>>> process(mlist, msg, {})
>>> msg.set_boundary('BOUNDARY')
>>> print(msg.as_string())
From: aperson@example.org
Content-Type: multipart/mixed; boundary="BOUNDARY"
<BLANKLINE>
--BOUNDARY
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
<BLANKLINE>
header
--BOUNDARY
Content-Type: text/plain; charset=unknown
Content-Transfer-Encoding: 7bit
<BLANKLINE>
Here is a message.
<BLANKLINE>
--BOUNDARY
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
<BLANKLINE>
footer
--BOUNDARY--
Decorating multipart messages
=============================
Multipart messages have to be decorated differently. The header and footer
cannot be simply concatenated into the payload because that will break the
MIME structure of the message. Instead, the header and footer are attached as
separate MIME subparts.
When the outer part is ``multipart/mixed``, the header and footer can have a
``Content-Disposition`` of ``inline`` so that MUAs can display these headers
as if they were simply concatenated.
>>> part_1 = message_from_string("""\
... From: aperson@example.org
...
... Here is the first message.
... """)
>>> part_2 = message_from_string("""\
... From: bperson@example.com
...
... Here is the second message.
... """)
>>> from email.mime.multipart import MIMEMultipart
>>> msg = MIMEMultipart('mixed', boundary='BOUNDARY',
... _subparts=(part_1, part_2))
>>> process(mlist, msg, {})
>>> print(msg.as_string())
Content-Type: multipart/mixed; boundary="BOUNDARY"
MIME-Version: 1.0
<BLANKLINE>
--BOUNDARY
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
<BLANKLINE>
header
--BOUNDARY
From: aperson@example.org
<BLANKLINE>
Here is the first message.
<BLANKLINE>
--BOUNDARY
From: bperson@example.com
<BLANKLINE>
Here is the second message.
<BLANKLINE>
--BOUNDARY
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
<BLANKLINE>
footer
--BOUNDARY--
Decorating other content types
==============================
Non-multipart non-text content types will get wrapped in a ``multipart/mixed``
so that the header and footer can be added as attachments.
>>> msg = message_from_string("""\
... From: aperson@example.org
... Content-Type: image/x-beautiful
...
... IMAGEDATAIMAGEDATAIMAGEDATA
... """)
>>> process(mlist, msg, {})
>>> msg.set_boundary('BOUNDARY')
>>> print(msg.as_string())
From: aperson@example.org
...
--BOUNDARY
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
<BLANKLINE>
header
--BOUNDARY
Content-Type: image/x-beautiful
<BLANKLINE>
IMAGEDATAIMAGEDATAIMAGEDATA
<BLANKLINE>
--BOUNDARY
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
<BLANKLINE>
footer
--BOUNDARY--
.. Clean up
>>> config.pop('templates')
>>> import shutil
>>> shutil.rmtree(template_dir)
|