summaryrefslogtreecommitdiff
path: root/src/mailman/rest/docs/lists.rst
blob: 6a034df9492626f18cb13d659a735c94fbf96396 (plain)
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
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
=============
Mailing lists
=============

The REST API can be queried for the set of known mailing lists.  There is a
top level collection that can return all the mailing lists.  There aren't any
yet though.

    >>> dump_json('http://localhost:9001/3.0/lists')
    http_etag: "..."
    start: 0
    total_size: 0

Create a mailing list in a domain and it's accessible via the API.
::

    >>> mlist = create_list('ant@example.com')
    >>> transaction.commit()

    >>> dump_json('http://localhost:9001/3.0/lists')
    entry 0:
        display_name: Ant
        fqdn_listname: ant@example.com
        http_etag: "..."
        list_id: ant.example.com
        list_name: ant
        mail_host: example.com
        member_count: 0
        self_link: http://localhost:9001/3.0/lists/ant.example.com
        volume: 1
    http_etag: "..."
    start: 0
    total_size: 1

You can also query for lists from a particular domain.

    >>> dump_json('http://localhost:9001/3.0/domains/example.com/lists')
    entry 0:
        display_name: Ant
        fqdn_listname: ant@example.com
        http_etag: "..."
        list_id: ant.example.com
        list_name: ant
        mail_host: example.com
        member_count: 0
        self_link: http://localhost:9001/3.0/lists/ant.example.com
        volume: 1
    http_etag: "..."
    start: 0
    total_size: 1

Advertised lists can be filtered using the ``advertised`` query parameter.
::

    >>> mlist = create_list('elk@example.com')
    >>> mlist.advertised = False
    >>> transaction.commit()

    >>> dump_json('http://localhost:9001/3.0/lists?advertised=true')
    entry 0:
        ...
        list_id: ant.example.com
        ...
    http_etag: "..."
    start: 0
    total_size: 1

The same applies to lists from a particular domain.

    >>> dump_json('http://localhost:9001/3.0/domains/example.com'
    ...           '/lists?advertised=true')
    entry 0:
        ...
        list_id: ant.example.com
        ...
    http_etag: "..."
    start: 0
    total_size: 1


Paginating over list records
----------------------------

Instead of returning all the list records at once, it's possible to return
them in pages by adding the GET parameters ``count`` and ``page`` to the
request URI.  Page 1 is the first page and ``count`` defines the size of the
page.
::

    >>> dump_json('http://localhost:9001/3.0/domains/example.com/lists'
    ...           '?count=1&page=1')
    entry 0:
        display_name: Ant
        fqdn_listname: ant@example.com
        http_etag: "..."
        list_id: ant.example.com
        list_name: ant
        mail_host: example.com
        member_count: 0
        self_link: http://localhost:9001/3.0/lists/ant.example.com
        volume: 1
    http_etag: "..."
    start: 0
    total_size: 2

    >>> dump_json('http://localhost:9001/3.0/domains/example.com/lists'
    ...           '?count=1&page=2')
    entry 0:
        display_name: Elk
        fqdn_listname: elk@example.com
        http_etag: "..."
        list_id: elk.example.com
        list_name: elk
        mail_host: example.com
        member_count: 0
        self_link: http://localhost:9001/3.0/lists/elk.example.com
        volume: 1
    http_etag: "..."
    start: 1
    total_size: 2


Creating lists via the API
==========================

New mailing lists can also be created through the API, by posting to the
``lists`` URL.

    >>> dump_json('http://localhost:9001/3.0/lists', {
    ...           'fqdn_listname': 'bee@example.com',
    ...           })
    content-length: 0
    content-type: application/json; charset=UTF-8
    date: ...
    location: http://localhost:9001/3.0/lists/bee.example.com
    ...

The mailing list exists in the database.
::

    >>> from mailman.interfaces.listmanager import IListManager
    >>> from zope.component import getUtility
    >>> list_manager = getUtility(IListManager)

    >>> bee = list_manager.get('bee@example.com')
    >>> bee
    <mailing list "bee@example.com" at ...>

The mailing list was created using the default style, which allows list posts.

    >>> bee.allow_list_posts
    True

.. Abort the Storm transaction.
    >>> transaction.abort()

It is also available in the REST API via the location given in the response.

    >>> dump_json('http://localhost:9001/3.0/lists/bee.example.com')
    display_name: Bee
    fqdn_listname: bee@example.com
    http_etag: "..."
    list_id: bee.example.com
    list_name: bee
    mail_host: example.com
    member_count: 0
    self_link: http://localhost:9001/3.0/lists/bee.example.com
    volume: 1

Normally, you access the list via its RFC 2369 list-id as shown above, but for
backward compatibility purposes, you can also access it via the list's posting
address, if that has never been changed (since the list-id is immutable, but
the posting address is not).

    >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com')
    display_name: Bee
    fqdn_listname: bee@example.com
    http_etag: "..."
    list_id: bee.example.com
    list_name: bee
    mail_host: example.com
    member_count: 0
    self_link: http://localhost:9001/3.0/lists/bee.example.com
    volume: 1


Apply a style at list creation time
-----------------------------------

:ref:`List styles <list-styles>` allow you to more easily create mailing lists
of a particular type, e.g. discussion lists.  We can see which styles are
available, and which is the default style.

    >>> dump_json('http://localhost:9001/3.0/lists/styles')
    default: legacy-default
    http_etag: "..."
    style_names: ['legacy-announce', 'legacy-default']

When creating a list, if we don't specify a style to apply, the default style
is used.  However, we can provide a style name in the POST data to choose a
different style.

    >>> dump_json('http://localhost:9001/3.0/lists', {
    ...           'fqdn_listname': 'cat@example.com',
    ...           'style_name': 'legacy-announce',
    ...           })
    content-length: 0
    content-type: application/json; charset=UTF-8
    date: ...
    location: http://localhost:9001/3.0/lists/cat.example.com
    ...

We can tell that the list was created using the `legacy-announce` style,
because announce lists don't allow posting by the general public.

    >>> cat = list_manager.get('cat@example.com')
    >>> cat.allow_list_posts
    False

.. Abort the Storm transaction.
    >>> transaction.abort()


Deleting lists via the API
==========================

Existing mailing lists can be deleted through the API, by doing an HTTP
``DELETE`` on the mailing list URL.
::

    >>> dump_json('http://localhost:9001/3.0/lists/bee.example.com',
    ...           method='DELETE')
    content-length: 0
    date: ...
    server: ...
    status: 204

The mailing list does not exist.

    >>> print(list_manager.get('bee@example.com'))
    None

.. Abort the Storm transaction.
    >>> transaction.abort()

For backward compatibility purposes, you can delete a list via its posting
address as well.

    >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com',
    ...           method='DELETE')
    content-length: 0
    date: ...
    server: ...
    status: 204

The mailing list does not exist.

    >>> print(list_manager.get('ant@example.com'))
    None


Managing mailing list archivers
===============================

The Mailman system has some site-wide enabled archivers, and each mailing list
can enable or disable these archivers individually.  This gives list owners
control over where traffic to their list is archived.  You can see which
archivers are available, and whether they are enabled for this mailing list.
::

    >>> mlist = create_list('dog@example.com')
    >>> transaction.commit()

    >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
    http_etag: "..."
    mail-archive: True
    mhonarc: True

You can set all the archiver states by putting new state flags on the
resource.
::

    >>> dump_json(
    ...     'http://localhost:9001/3.0/lists/dog@example.com/archivers', {
    ...         'mail-archive': False,
    ...         'mhonarc': True,
    ...         }, method='PUT')
    content-length: 0
    date: ...
    server: ...
    status: 204

    >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
    http_etag: "..."
    mail-archive: False
    mhonarc: True

You can change the state of a subset of the list archivers.
::

    >>> dump_json(
    ...     'http://localhost:9001/3.0/lists/dog@example.com/archivers', {
    ...         'mhonarc': False,
    ...         }, method='PATCH')
    content-length: 0
    date: ...
    server: ...
    status: 204

    >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
    http_etag: "..."
    mail-archive: False
    mhonarc: False


List digests
============

A list collects messages and prepares a digest which can be periodically sent
to all members who elect to receive digests.  Digests are usually sent
whenever their size has reached a threshold, but you can force a digest to be
sent immediately via the REST API.

Let's create a mailing list that has a digest recipient.

    >>> from mailman.interfaces.member import DeliveryMode
    >>> from mailman.testing.helpers import subscribe
    >>> emu = create_list('emu@example.com')
    >>> emu.send_welcome_message = False
    >>> anne = subscribe(emu, 'Anne')
    >>> anne.preferences.delivery_mode = DeliveryMode.plaintext_digests

The mailing list has a fairly high size threshold so that sending a single
message through the list won't trigger an automatic digest.  The threshold is
the maximum digest size in kibibytes (1024 bytes).

    >>> emu.digest_size_threshold = 100
    >>> transaction.commit()

We send a message through the mailing list to start collecting for a digest.

    >>> from mailman.runners.digest import DigestRunner
    >>> from mailman.testing.helpers import make_testable_runner
    >>> msg = message_from_string("""\
    ... From: anne@example.com
    ... To: emu@example.com
    ... Subject: Message #1
    ...
    ... """)
    >>> config.handlers['to-digest'].process(emu, msg, {})
    >>> runner = make_testable_runner(DigestRunner, 'digest')
    >>> runner.run()

No digest was sent because it didn't reach the size threshold.

    >>> from mailman.testing.helpers import get_queue_messages
    >>> len(get_queue_messages('virgin'))
    0

By POSTing to the list's digest end-point with the ``send`` parameter set, we
can force the digest to be sent.

    >>> dump_json('http://localhost:9001/3.0/lists/emu.example.com/digest', {
    ...           'send': True,
    ...           })
    content-length: 0
    content-type: application/json; charset=UTF-8
    date: ...

Once the runner does its thing, the digest message will be sent.

    >>> runner.run()
    >>> items = get_queue_messages('virgin')
    >>> len(items)
    1
    >>> print(items[0].msg)
    From: emu-request@example.com
    Subject: Emu Digest, Vol 1, Issue 1
    To: emu@example.com
    ...
    From: anne@example.com
    Subject: Message #1
    To: emu@example.com
    ...
    End of Emu Digest, Vol 1, Issue 1
    *********************************
    <BLANKLINE>

Digests also have a volume number and digest number which can be bumped, also
by POSTing to the REST API.  Bumping the digest for this list will increment
the digest volume and reset the digest number to 1.  We have to fake that the
last digest was sent a couple of days ago.

    >>> from datetime import timedelta
    >>> from mailman.interfaces.digests import DigestFrequency
    >>> emu.digest_volume_frequency = DigestFrequency.daily
    >>> emu.digest_last_sent_at -= timedelta(days=2)
    >>> transaction.commit()

Before bumping, we can get the next digest volume and number.  Doing a GET on
the digest resource is just a shorthand for getting some interesting
information about the digest.  Note that ``volume`` and ``next_digest_number``
can also be retrieved from the list's configuration resource.

    >>> dump_json('http://localhost:9001/3.0/lists/emu.example.com/digest')
    http_etag: ...
    next_digest_number: 2
    volume: 1

Let's bump the digest.

    >>> dump_json('http://localhost:9001/3.0/lists/emu.example.com/digest', {
    ...           'bump': True,
    ...           })
    content-length: 0
    content-type: application/json; charset=UTF-8
    date: ...

And now the next digest to be sent will have a new volume number.

    >>> dump_json('http://localhost:9001/3.0/lists/emu.example.com/digest')
    http_etag: ...
    next_digest_number: 1
    volume: 2