~barry/mailman/work1

« back to all changes in this revision

Viewing changes to src/mailman/model/docs/requests.rst

  • Committer: Barry Warsaw
  • Date: 2013-03-11 19:24:48 UTC
  • mfrom: (7178.2.26 3.0)
  • Revision ID: barry@list.org-20130311192448-gb1h8ca77weapdkx
trunk merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
.. _model-requests:
 
2
 
1
3
==================
2
4
Moderator requests
3
5
==================
6
8
closed lists, or postings by non-members.  The requests database is the low
7
9
level interface to these actions requiring approval.
8
10
 
9
 
.. Here is a helper function for printing out held requests.
10
 
 
11
 
    >>> def show_holds(requests):
12
 
    ...     for request in requests.held_requests:
13
 
    ...         key, data = requests.get_request(request.id)
14
 
    ...         print request.id, str(request.request_type), key
15
 
    ...         if data is not None:
16
 
    ...             for key in sorted(data):
17
 
    ...                 print '    {0}: {1}'.format(key, data[key])
 
11
An :ref:`application level interface <app-moderator>` for holding messages and
 
12
membership changes is also available.
18
13
 
19
14
 
20
15
Mailing list-centric
21
16
====================
22
17
 
23
 
A set of requests are always related to a particular mailing list, so given a
24
 
mailing list you need to get its requests object.
 
18
A set of requests are always related to a particular mailing list.  Adapt the
 
19
mailing list to get its requests.
25
20
::
26
21
 
27
22
    >>> from mailman.interfaces.requests import IListRequests
48
43
At the lowest level, the requests database is very simple.  Holding a request
49
44
requires a request type (as an enum value), a key, and an optional dictionary
50
45
of associated data.  The request database assigns no semantics to the held
51
 
data, except for the request type.  Here we hold some simple bits of data.
 
46
data, except for the request type.
52
47
 
53
48
    >>> from mailman.interfaces.requests import RequestType
54
 
    >>> id_1 = requests.hold_request(RequestType.held_message,   'hold_1')
55
 
    >>> id_2 = requests.hold_request(RequestType.subscription,   'hold_2')
56
 
    >>> id_3 = requests.hold_request(RequestType.unsubscription, 'hold_3')
57
 
    >>> id_4 = requests.hold_request(RequestType.held_message,   'hold_4')
58
 
    >>> id_1, id_2, id_3, id_4
59
 
    (1, 2, 3, 4)
60
 
 
61
 
And of course, now we can see that there are four requests being held.
 
49
 
 
50
We can hold messages for moderator approval.
 
51
 
 
52
    >>> requests.hold_request(RequestType.held_message, 'hold_1')
 
53
    1
 
54
 
 
55
We can hold subscription requests for moderator approval.
 
56
 
 
57
    >>> requests.hold_request(RequestType.subscription, 'hold_2')
 
58
    2
 
59
 
 
60
We can hold unsubscription requests for moderator approval.
 
61
 
 
62
    >>> requests.hold_request(RequestType.unsubscription, 'hold_3')
 
63
    3
 
64
 
 
65
 
 
66
Getting requests
 
67
================
 
68
 
 
69
We can see the total number of requests being held.
62
70
 
63
71
    >>> requests.count
64
 
    4
65
 
    >>> requests.count_of(RequestType.held_message)
66
 
    2
 
72
    3
 
73
 
 
74
We can also see the number of requests being held by request type.
 
75
 
67
76
    >>> requests.count_of(RequestType.subscription)
68
77
    1
69
78
    >>> requests.count_of(RequestType.unsubscription)
70
79
    1
71
 
    >>> show_holds(requests)
72
 
    1 RequestType.held_message hold_1
73
 
    2 RequestType.subscription hold_2
74
 
    3 RequestType.unsubscription hold_3
75
 
    4 RequestType.held_message hold_4
76
 
 
77
 
If we try to hold a request with a bogus type, we get an exception.
78
 
 
79
 
    >>> requests.hold_request(5, 'foo')
80
 
    Traceback (most recent call last):
81
 
    ...
82
 
    TypeError: 5
83
 
 
84
 
We can hold requests with additional data.
85
 
 
86
 
    >>> data = dict(foo='yes', bar='no')
87
 
    >>> id_5 = requests.hold_request(RequestType.held_message, 'hold_5', data)
88
 
    >>> id_5
89
 
    5
90
 
    >>> requests.count
91
 
    5
92
 
    >>> show_holds(requests)
93
 
    1 RequestType.held_message hold_1
94
 
    2 RequestType.subscription hold_2
95
 
    3 RequestType.unsubscription hold_3
96
 
    4 RequestType.held_message hold_4
97
 
    5 RequestType.held_message hold_5
98
 
        bar: no
99
 
        foo: yes
100
 
 
101
 
 
102
 
Getting requests
103
 
================
 
80
 
 
81
We can also see when there are multiple held requests of a particular type.
 
82
 
 
83
    >>> requests.hold_request(RequestType.held_message, 'hold_4')
 
84
    4
 
85
    >>> requests.count_of(RequestType.held_message)
 
86
    2
104
87
 
105
88
We can ask the requests database for a specific request, by providing the id
106
89
of the request data we want.  This returns a 2-tuple of the key and data we
110
93
    >>> print key
111
94
    hold_2
112
95
 
113
 
Because we did not store additional data with request 2, it comes back as None
114
 
now.
 
96
There was no additional data associated with request 2.
115
97
 
116
98
    >>> print data
117
99
    None
118
100
 
119
 
However, if we ask for a request that had data, we'd get it back now.
 
101
If we ask for a request that is not in the database, we get None back.
 
102
 
 
103
    >>> print requests.get_request(801)
 
104
    None
 
105
 
 
106
 
 
107
Additional data
 
108
===============
 
109
 
 
110
When a request is held, additional data can be associated with it, in the form
 
111
of a dictionary with string values.
 
112
 
 
113
    >>> data = dict(foo='yes', bar='no')
 
114
    >>> requests.hold_request(RequestType.held_message, 'hold_5', data)
 
115
    5
 
116
 
 
117
The data is returned when the request is retrieved.  The dictionary will have
 
118
an additional key which holds the name of the request type.
120
119
 
121
120
    >>> key, data = requests.get_request(5)
122
121
    >>> print key
123
122
    hold_5
124
123
    >>> dump_msgdata(data)
125
 
    bar: no
126
 
    foo: yes
127
 
 
128
 
If we ask for a request that is not in the database, we get None back.
129
 
 
130
 
    >>> print requests.get_request(801)
131
 
    None
 
124
    _request_type: held_message
 
125
    bar          : no
 
126
    foo          : yes
132
127
 
133
128
 
134
129
Iterating over requests
140
135
    >>> requests.count_of(RequestType.held_message)
141
136
    3
142
137
    >>> for request in requests.of_type(RequestType.held_message):
143
 
    ...     assert request.request_type is RequestType.held_message
144
138
    ...     key, data = requests.get_request(request.id)
145
 
    ...     print request.id, key
 
139
    ...     print request.id, request.request_type, key
146
140
    ...     if data is not None:
147
141
    ...         for key in sorted(data):
148
142
    ...             print '    {0}: {1}'.format(key, data[key])
149
 
    1 hold_1
150
 
    4 hold_4
151
 
    5 hold_5
152
 
    bar: no
153
 
    foo: yes
 
143
    1 RequestType.held_message hold_1
 
144
    4 RequestType.held_message hold_4
 
145
    5 RequestType.held_message hold_5
 
146
        _request_type: held_message
 
147
        bar: no
 
148
        foo: yes
154
149
 
155
150
 
156
151
Deleting requests
157
152
=================
158
153
 
159
 
Once a specific request has been handled, it will be deleted from the requests
 
154
Once a specific request has been handled, it can be deleted from the requests
160
155
database.
161
156
 
 
157
    >>> requests.count
 
158
    5
162
159
    >>> requests.delete_request(2)
163
160
    >>> requests.count
164
161
    4
165
 
    >>> show_holds(requests)
166
 
    1 RequestType.held_message hold_1
167
 
    3 RequestType.unsubscription hold_3
168
 
    4 RequestType.held_message hold_4
169
 
    5 RequestType.held_message hold_5
170
 
        bar: no
171
 
        foo: yes
 
162
 
 
163
Request 2 is no longer in the database.
 
164
 
172
165
    >>> print requests.get_request(2)
173
166
    None
174
167
 
175
 
We get an exception if we ask to delete a request that isn't in the database.
176
 
 
177
 
    >>> requests.delete_request(801)
178
 
    Traceback (most recent call last):
179
 
    ...
180
 
    KeyError: 801
181
 
 
182
 
For the next section, we first clean up all the current requests.
183
 
 
184
168
    >>> for request in requests.held_requests:
185
169
    ...     requests.delete_request(request.id)
186
170
    >>> requests.count
187
171
    0
188
 
 
189
 
 
190
 
Application support
191
 
===================
192
 
 
193
 
There are several higher level interfaces available in the ``mailman.app``
194
 
package which can be used to hold messages, subscription, and unsubscriptions.
195
 
There are also interfaces for disposing of these requests in an application
196
 
specific and consistent way.
197
 
 
198
 
    >>> from mailman.app import moderator
199
 
 
200
 
 
201
 
Holding messages
202
 
================
203
 
 
204
 
For this section, we need a mailing list and at least one message.
205
 
 
206
 
    >>> mlist = create_list('alist@example.com')
207
 
    >>> mlist.preferred_language = 'en'
208
 
    >>> mlist.display_name = 'A Test List'
209
 
    >>> msg = message_from_string("""\
210
 
    ... From: aperson@example.org
211
 
    ... To: alist@example.com
212
 
    ... Subject: Something important
213
 
    ...
214
 
    ... Here's something important about our mailing list.
215
 
    ... """)
216
 
 
217
 
Holding a message means keeping a copy of it that a moderator must approve
218
 
before the message is posted to the mailing list.  To hold the message, you
219
 
must supply the message, message metadata, and a text reason for the hold.  In
220
 
this case, we won't include any additional metadata.
221
 
 
222
 
    >>> id_1 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
223
 
    >>> requests.get_request(id_1) is not None
224
 
    True
225
 
 
226
 
We can also hold a message with some additional metadata.
227
 
::
228
 
 
229
 
    # Delete the Message-ID from the previous hold so we don't try to store
230
 
    # collisions in the message storage.
231
 
    >>> del msg['message-id']
232
 
    >>> msgdata = dict(sender='aperson@example.com',
233
 
    ...                approved=True)
234
 
    >>> id_2 = moderator.hold_message(mlist, msg, msgdata, 'Feeling ornery')
235
 
    >>> requests.get_request(id_2) is not None
236
 
    True
237
 
 
238
 
Once held, the moderator can select one of several dispositions.  The most
239
 
trivial is to simply defer a decision for now.
240
 
 
241
 
    >>> from mailman.interfaces.action import Action
242
 
    >>> moderator.handle_message(mlist, id_1, Action.defer)
243
 
    >>> requests.get_request(id_1) is not None
244
 
    True
245
 
 
246
 
The moderator can also discard the message.  This is often done with spam.
247
 
Bye bye message!
248
 
 
249
 
    >>> moderator.handle_message(mlist, id_1, Action.discard)
250
 
    >>> print requests.get_request(id_1)
251
 
    None
252
 
    >>> from mailman.testing.helpers import get_queue_messages
253
 
    >>> get_queue_messages('virgin')
254
 
    []
255
 
 
256
 
The message can be rejected, meaning it is bounced back to the sender.
257
 
 
258
 
    >>> moderator.handle_message(mlist, id_2, Action.reject, 'Off topic')
259
 
    >>> print requests.get_request(id_2)
260
 
    None
261
 
    >>> messages = get_queue_messages('virgin')
262
 
    >>> len(messages)
263
 
    1
264
 
    >>> print messages[0].msg.as_string()
265
 
    MIME-Version: 1.0
266
 
    Content-Type: text/plain; charset="us-ascii"
267
 
    Content-Transfer-Encoding: 7bit
268
 
    Subject: Request to mailing list "A Test List" rejected
269
 
    From: alist-bounces@example.com
270
 
    To: aperson@example.org
271
 
    Message-ID: ...
272
 
    Date: ...
273
 
    Precedence: bulk
274
 
    <BLANKLINE>
275
 
    Your request to the alist@example.com mailing list
276
 
    <BLANKLINE>
277
 
        Posting of your message titled "Something important"
278
 
    <BLANKLINE>
279
 
    has been rejected by the list moderator.  The moderator gave the
280
 
    following reason for rejecting your request:
281
 
    <BLANKLINE>
282
 
    "Off topic"
283
 
    <BLANKLINE>
284
 
    Any questions or comments should be directed to the list administrator
285
 
    at:
286
 
    <BLANKLINE>
287
 
        alist-owner@example.com
288
 
    <BLANKLINE>
289
 
    >>> dump_msgdata(messages[0].msgdata)
290
 
    _parsemsg           : False
291
 
    listname            : alist@example.com
292
 
    nodecorate          : True
293
 
    recipients          : set([u'aperson@example.org'])
294
 
    reduced_list_headers: True
295
 
    version             : 3
296
 
 
297
 
Or the message can be approved.  This actually places the message back into
298
 
the incoming queue for further processing, however the message metadata
299
 
indicates that the message has been approved.
300
 
 
301
 
    >>> id_3 = moderator.hold_message(mlist, msg, msgdata, 'Needs approval')
302
 
    >>> moderator.handle_message(mlist, id_3, Action.accept)
303
 
    >>> messages = get_queue_messages('pipeline')
304
 
    >>> len(messages)
305
 
    1
306
 
    >>> print messages[0].msg.as_string()
307
 
    From: aperson@example.org
308
 
    To: alist@example.com
309
 
    Subject: Something important
310
 
    Message-ID: ...
311
 
    X-Message-ID-Hash: ...
312
 
    X-Mailman-Approved-At: ...
313
 
    <BLANKLINE>
314
 
    Here's something important about our mailing list.
315
 
    <BLANKLINE>
316
 
    >>> dump_msgdata(messages[0].msgdata)
317
 
    _parsemsg         : False
318
 
    approved          : True
319
 
    moderator_approved: True
320
 
    sender            : aperson@example.com
321
 
    version           : 3
322
 
 
323
 
In addition to any of the above dispositions, the message can also be
324
 
preserved for further study.  Ordinarily the message is removed from the
325
 
global message store after its disposition (though approved messages may be
326
 
re-added to the message store).  When handling a message, we can tell the
327
 
moderator interface to also preserve a copy, essentially telling it not to
328
 
delete the message from the storage.  First, without the switch, the message
329
 
is deleted.
330
 
::
331
 
 
332
 
    >>> msg = message_from_string("""\
333
 
    ... From: aperson@example.org
334
 
    ... To: alist@example.com
335
 
    ... Subject: Something important
336
 
    ... Message-ID: <12345>
337
 
    ...
338
 
    ... Here's something important about our mailing list.
339
 
    ... """)
340
 
    >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
341
 
    >>> moderator.handle_message(mlist, id_4, Action.discard)
342
 
 
343
 
    >>> from mailman.interfaces.messages import IMessageStore
344
 
    >>> from zope.component import getUtility
345
 
    >>> message_store = getUtility(IMessageStore)
346
 
 
347
 
    >>> print message_store.get_message_by_id('<12345>')
348
 
    None
349
 
 
350
 
But if we ask to preserve the message when we discard it, it will be held in
351
 
the message store after disposition.
352
 
 
353
 
    >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
354
 
    >>> moderator.handle_message(mlist, id_4, Action.discard, preserve=True)
355
 
    >>> stored_msg = message_store.get_message_by_id('<12345>')
356
 
    >>> print stored_msg.as_string()
357
 
    From: aperson@example.org
358
 
    To: alist@example.com
359
 
    Subject: Something important
360
 
    Message-ID: <12345>
361
 
    X-Message-ID-Hash: 4CF7EAU3SIXBPXBB5S6PEUMO62MWGQN6
362
 
    <BLANKLINE>
363
 
    Here's something important about our mailing list.
364
 
    <BLANKLINE>
365
 
 
366
 
Orthogonal to preservation, the message can also be forwarded to another
367
 
address.  This is helpful for getting the message into the inbox of one of the
368
 
moderators.
369
 
::
370
 
 
371
 
    # Set a new Message-ID from the previous hold so we don't try to store
372
 
    # collisions in the message storage.
373
 
    >>> del msg['message-id']
374
 
    >>> msg['Message-ID'] = '<abcde>'
375
 
    >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
376
 
    >>> moderator.handle_message(mlist, id_4, Action.discard,
377
 
    ...                          forward=['zperson@example.com'])
378
 
 
379
 
    >>> messages = get_queue_messages('virgin')
380
 
    >>> len(messages)
381
 
    1
382
 
    >>> print messages[0].msg.as_string()
383
 
    Subject: Forward of moderated message
384
 
    From: alist-bounces@example.com
385
 
    To: zperson@example.com
386
 
    MIME-Version: 1.0
387
 
    Content-Type: message/rfc822
388
 
    Message-ID: ...
389
 
    Date: ...
390
 
    Precedence: bulk
391
 
    <BLANKLINE>
392
 
    From: aperson@example.org
393
 
    To: alist@example.com
394
 
    Subject: Something important
395
 
    Message-ID: <abcde>
396
 
    X-Message-ID-Hash: EN2R5UQFMOUTCL44FLNNPLSXBIZW62ER
397
 
    <BLANKLINE>
398
 
    Here's something important about our mailing list.
399
 
    <BLANKLINE>
400
 
 
401
 
    >>> dump_msgdata(messages[0].msgdata)
402
 
    _parsemsg           : False
403
 
    listname            : alist@example.com
404
 
    nodecorate          : True
405
 
    recipients          : [u'zperson@example.com']
406
 
    reduced_list_headers: True
407
 
    version             : 3
408
 
 
409
 
 
410
 
Holding subscription requests
411
 
=============================
412
 
 
413
 
For closed lists, subscription requests will also be held for moderator
414
 
approval.  In this case, several pieces of information related to the
415
 
subscription must be provided, including the subscriber's address and real
416
 
name, their password (possibly hashed), what kind of delivery option they are
417
 
choosing and their preferred language.
418
 
 
419
 
    >>> from mailman.interfaces.member import DeliveryMode
420
 
    >>> mlist.admin_immed_notify = False
421
 
    >>> id_3 = moderator.hold_subscription(mlist,
422
 
    ...     'bperson@example.org', 'Ben Person',
423
 
    ...     '{NONE}abcxyz', DeliveryMode.regular, 'en')
424
 
    >>> requests.get_request(id_3) is not None
425
 
    True
426
 
 
427
 
In the above case the mailing list was not configured to send the list
428
 
moderators a notice about the hold, so no email message is in the virgin
429
 
queue.
430
 
 
431
 
    >>> get_queue_messages('virgin')
432
 
    []
433
 
 
434
 
But if we set the list up to notify the list moderators immediately when a
435
 
message is held for approval, there will be a message placed in the virgin
436
 
queue when the message is held.
437
 
::
438
 
 
439
 
    >>> mlist.admin_immed_notify = True
440
 
    >>> # XXX This will almost certainly change once we've worked out the web
441
 
    >>> # space layout for mailing lists now.
442
 
    >>> id_4 = moderator.hold_subscription(mlist,
443
 
    ...     'cperson@example.org', 'Claire Person',
444
 
    ...     '{NONE}zyxcba', DeliveryMode.regular, 'en')
445
 
    >>> requests.get_request(id_4) is not None
446
 
    True
447
 
    >>> messages = get_queue_messages('virgin')
448
 
    >>> len(messages)
449
 
    1
450
 
 
451
 
    >>> print messages[0].msg.as_string()
452
 
    MIME-Version: 1.0
453
 
    Content-Type: text/plain; charset="us-ascii"
454
 
    Content-Transfer-Encoding: 7bit
455
 
    Subject: New subscription request to A Test List from cperson@example.org
456
 
    From: alist-owner@example.com
457
 
    To: alist-owner@example.com
458
 
    Message-ID: ...
459
 
    Date: ...
460
 
    Precedence: bulk
461
 
    <BLANKLINE>
462
 
    Your authorization is required for a mailing list subscription request
463
 
    approval:
464
 
    <BLANKLINE>
465
 
        For:  cperson@example.org
466
 
        List: alist@example.com
467
 
    <BLANKLINE>
468
 
    At your convenience, visit:
469
 
    <BLANKLINE>
470
 
        http://lists.example.com/admindb/alist@example.com
471
 
    <BLANKLINE>
472
 
    to process the request.
473
 
    <BLANKLINE>
474
 
 
475
 
    >>> dump_msgdata(messages[0].msgdata)
476
 
    _parsemsg           : False
477
 
    listname            : alist@example.com
478
 
    nodecorate          : True
479
 
    recipients          : set([u'alist-owner@example.com'])
480
 
    reduced_list_headers: True
481
 
    tomoderators        : True
482
 
    version             : 3
483
 
 
484
 
Once held, the moderator can select one of several dispositions.  The most
485
 
trivial is to simply defer a decision for now.
486
 
 
487
 
    >>> moderator.handle_subscription(mlist, id_3, Action.defer)
488
 
    >>> requests.get_request(id_3) is not None
489
 
    True
490
 
 
491
 
The held subscription can also be discarded.
492
 
 
493
 
    >>> moderator.handle_subscription(mlist, id_3, Action.discard)
494
 
    >>> print requests.get_request(id_3)
495
 
    None
496
 
 
497
 
The request can be rejected, in which case a message is sent to the
498
 
subscriber.
499
 
::
500
 
 
501
 
    >>> moderator.handle_subscription(mlist, id_4, Action.reject,
502
 
    ...     'This is a closed list')
503
 
    >>> print requests.get_request(id_4)
504
 
    None
505
 
    >>> messages = get_queue_messages('virgin')
506
 
    >>> len(messages)
507
 
    1
508
 
 
509
 
    >>> print messages[0].msg.as_string()
510
 
    MIME-Version: 1.0
511
 
    Content-Type: text/plain; charset="us-ascii"
512
 
    Content-Transfer-Encoding: 7bit
513
 
    Subject: Request to mailing list "A Test List" rejected
514
 
    From: alist-bounces@example.com
515
 
    To: cperson@example.org
516
 
    Message-ID: ...
517
 
    Date: ...
518
 
    Precedence: bulk
519
 
    <BLANKLINE>
520
 
    Your request to the alist@example.com mailing list
521
 
    <BLANKLINE>
522
 
        Subscription request
523
 
    <BLANKLINE>
524
 
    has been rejected by the list moderator.  The moderator gave the
525
 
    following reason for rejecting your request:
526
 
    <BLANKLINE>
527
 
    "This is a closed list"
528
 
    <BLANKLINE>
529
 
    Any questions or comments should be directed to the list administrator
530
 
    at:
531
 
    <BLANKLINE>
532
 
        alist-owner@example.com
533
 
    <BLANKLINE>
534
 
 
535
 
    >>> dump_msgdata(messages[0].msgdata)
536
 
    _parsemsg           : False
537
 
    listname            : alist@example.com
538
 
    nodecorate          : True
539
 
    recipients          : set([u'cperson@example.org'])
540
 
    reduced_list_headers: True
541
 
    version             : 3
542
 
 
543
 
The subscription can also be accepted.  This subscribes the address to the
544
 
mailing list.
545
 
 
546
 
    >>> mlist.send_welcome_message = True
547
 
    >>> mlist.welcome_message_uri = 'mailman:///welcome.txt'
548
 
    >>> id_4 = moderator.hold_subscription(mlist,
549
 
    ...     'fperson@example.org', 'Frank Person',
550
 
    ...     'abcxyz', DeliveryMode.regular, 'en')
551
 
 
552
 
A message will be sent to the moderators telling them about the held
553
 
subscription and the fact that they may need to approve it.
554
 
::
555
 
 
556
 
    >>> messages = get_queue_messages('virgin')
557
 
    >>> len(messages)
558
 
    1
559
 
 
560
 
    >>> print messages[0].msg.as_string()
561
 
    MIME-Version: 1.0
562
 
    Content-Type: text/plain; charset="us-ascii"
563
 
    Content-Transfer-Encoding: 7bit
564
 
    Subject: New subscription request to A Test List from fperson@example.org
565
 
    From: alist-owner@example.com
566
 
    To: alist-owner@example.com
567
 
    Message-ID: ...
568
 
    Date: ...
569
 
    Precedence: bulk
570
 
    <BLANKLINE>
571
 
    Your authorization is required for a mailing list subscription request
572
 
    approval:
573
 
    <BLANKLINE>
574
 
        For:  fperson@example.org
575
 
        List: alist@example.com
576
 
    <BLANKLINE>
577
 
    At your convenience, visit:
578
 
    <BLANKLINE>
579
 
        http://lists.example.com/admindb/alist@example.com
580
 
    <BLANKLINE>
581
 
    to process the request.
582
 
    <BLANKLINE>
583
 
 
584
 
    >>> dump_msgdata(messages[0].msgdata)
585
 
    _parsemsg           : False
586
 
    listname            : alist@example.com
587
 
    nodecorate          : True
588
 
    recipients          : set([u'alist-owner@example.com'])
589
 
    reduced_list_headers: True
590
 
    tomoderators        : True
591
 
    version             : 3
592
 
 
593
 
Accept the subscription request.
594
 
 
595
 
    >>> mlist.admin_notify_mchanges = True
596
 
    >>> moderator.handle_subscription(mlist, id_4, Action.accept)
597
 
 
598
 
There are now two messages in the virgin queue.  One is a welcome message
599
 
being sent to the user and the other is a subscription notification that is
600
 
sent to the moderators.  The only good way to tell which is which is to look
601
 
at the recipient list.
602
 
 
603
 
    >>> messages = get_queue_messages('virgin', sort_on='subject')
604
 
    >>> len(messages)
605
 
    2
606
 
 
607
 
The welcome message is sent to the person who just subscribed.
608
 
::
609
 
 
610
 
    >>> print messages[1].msg.as_string()
611
 
    MIME-Version: 1.0
612
 
    Content-Type: text/plain; charset="us-ascii"
613
 
    Content-Transfer-Encoding: 7bit
614
 
    Subject: Welcome to the "A Test List" mailing list
615
 
    From: alist-request@example.com
616
 
    To: Frank Person <fperson@example.org>
617
 
    X-No-Archive: yes
618
 
    Message-ID: ...
619
 
    Date: ...
620
 
    Precedence: bulk
621
 
    <BLANKLINE>
622
 
    Welcome to the "A Test List" mailing list!
623
 
    <BLANKLINE>
624
 
    To post to this list, send your email to:
625
 
    <BLANKLINE>
626
 
      alist@example.com
627
 
    <BLANKLINE>
628
 
    General information about the mailing list is at:
629
 
    <BLANKLINE>
630
 
      http://lists.example.com/listinfo/alist@example.com
631
 
    <BLANKLINE>
632
 
    If you ever want to unsubscribe or change your options (eg, switch to
633
 
    or from digest mode, change your password, etc.), visit your
634
 
    subscription page at:
635
 
    <BLANKLINE>
636
 
      http://example.com/fperson@example.org
637
 
    <BLANKLINE>
638
 
    You can also make such adjustments via email by sending a message to:
639
 
    <BLANKLINE>
640
 
      alist-request@example.com
641
 
    <BLANKLINE>
642
 
    with the word 'help' in the subject or body (don't include the
643
 
    quotes), and you will get back a message with instructions.  You will
644
 
    need your password to change your options, but for security purposes,
645
 
    this email is not included here.  There is also a button on your
646
 
    options page that will send your current password to you.
647
 
    <BLANKLINE>
648
 
 
649
 
    >>> dump_msgdata(messages[1].msgdata)
650
 
    _parsemsg           : False
651
 
    listname            : alist@example.com
652
 
    nodecorate          : True
653
 
    recipients          : set([u'Frank Person <fperson@example.org>'])
654
 
    reduced_list_headers: True
655
 
    verp                : False
656
 
    version             : 3
657
 
 
658
 
The admin message is sent to the moderators.
659
 
::
660
 
 
661
 
    >>> print messages[0].msg.as_string()
662
 
    MIME-Version: 1.0
663
 
    Content-Type: text/plain; charset="us-ascii"
664
 
    Content-Transfer-Encoding: 7bit
665
 
    Subject: A Test List subscription notification
666
 
    From: noreply@example.com
667
 
    To: alist-owner@example.com
668
 
    Message-ID: ...
669
 
    Date: ...
670
 
    Precedence: bulk
671
 
    <BLANKLINE>
672
 
    Frank Person <fperson@example.org> has been successfully subscribed to
673
 
    A Test List.
674
 
    <BLANKLINE>
675
 
 
676
 
    >>> dump_msgdata(messages[0].msgdata)
677
 
    _parsemsg           : False
678
 
    envsender           : noreply@example.com
679
 
    listname            : alist@example.com
680
 
    nodecorate          : True
681
 
    recipients          : set([])
682
 
    reduced_list_headers: True
683
 
    version             : 3
684
 
 
685
 
Frank Person is now a member of the mailing list.
686
 
::
687
 
 
688
 
    >>> member = mlist.members.get_member('fperson@example.org')
689
 
    >>> member
690
 
    <Member: Frank Person <fperson@example.org>
691
 
             on alist@example.com as MemberRole.member>
692
 
    >>> member.preferred_language
693
 
    <Language [en] English (USA)>
694
 
    >>> print member.delivery_mode
695
 
    DeliveryMode.regular
696
 
    >>> print member.user.display_name
697
 
    Frank Person
698
 
    >>> print member.user.password
699
 
    {plaintext}abcxyz
700
 
 
701
 
 
702
 
Holding unsubscription requests
703
 
===============================
704
 
 
705
 
Some lists, though it is rare, require moderator approval for unsubscriptions.
706
 
In this case, only the unsubscribing address is required.  Like subscriptions,
707
 
unsubscription holds can send the list's moderators an immediate
708
 
notification.
709
 
::
710
 
 
711
 
 
712
 
    >>> from mailman.interfaces.usermanager import IUserManager
713
 
    >>> from zope.component import getUtility
714
 
    >>> user_manager = getUtility(IUserManager)
715
 
 
716
 
    >>> mlist.admin_immed_notify = False
717
 
    >>> from mailman.interfaces.member import MemberRole
718
 
    >>> user_1 = user_manager.create_user('gperson@example.com')
719
 
    >>> address_1 = list(user_1.addresses)[0]
720
 
    >>> mlist.subscribe(address_1, MemberRole.member)
721
 
    <Member: gperson@example.com on alist@example.com as MemberRole.member>
722
 
 
723
 
    >>> user_2 = user_manager.create_user('hperson@example.com')
724
 
    >>> address_2 = list(user_2.addresses)[0]
725
 
    >>> mlist.subscribe(address_2, MemberRole.member)
726
 
    <Member: hperson@example.com on alist@example.com as MemberRole.member>
727
 
 
728
 
    >>> id_5 = moderator.hold_unsubscription(mlist, 'gperson@example.com')
729
 
    >>> requests.get_request(id_5) is not None
730
 
    True
731
 
    >>> get_queue_messages('virgin')
732
 
    []
733
 
    >>> mlist.admin_immed_notify = True
734
 
    >>> id_6 = moderator.hold_unsubscription(mlist, 'hperson@example.com')
735
 
 
736
 
    >>> messages = get_queue_messages('virgin')
737
 
    >>> len(messages)
738
 
    1
739
 
    >>> print messages[0].msg.as_string()
740
 
    MIME-Version: 1.0
741
 
    Content-Type: text/plain; charset="us-ascii"
742
 
    Content-Transfer-Encoding: 7bit
743
 
    Subject: New unsubscription request from A Test List by hperson@example.com
744
 
    From: alist-owner@example.com
745
 
    To: alist-owner@example.com
746
 
    Message-ID: ...
747
 
    Date: ...
748
 
    Precedence: bulk
749
 
    <BLANKLINE>
750
 
    Your authorization is required for a mailing list unsubscription
751
 
    request approval:
752
 
    <BLANKLINE>
753
 
        By:   hperson@example.com
754
 
        From: alist@example.com
755
 
    <BLANKLINE>
756
 
    At your convenience, visit:
757
 
    <BLANKLINE>
758
 
        http://lists.example.com/admindb/alist@example.com
759
 
    <BLANKLINE>
760
 
    to process the request.
761
 
    <BLANKLINE>
762
 
 
763
 
    >>> dump_msgdata(messages[0].msgdata)
764
 
    _parsemsg           : False
765
 
    listname            : alist@example.com
766
 
    nodecorate          : True
767
 
    recipients          : set([u'alist-owner@example.com'])
768
 
    reduced_list_headers: True
769
 
    tomoderators        : True
770
 
    version             : 3
771
 
 
772
 
There are now two addresses with held unsubscription requests.  As above, one
773
 
of the actions we can take is to defer to the decision.
774
 
 
775
 
    >>> moderator.handle_unsubscription(mlist, id_5, Action.defer)
776
 
    >>> requests.get_request(id_5) is not None
777
 
    True
778
 
 
779
 
The held unsubscription can also be discarded, and the member will remain
780
 
subscribed.
781
 
 
782
 
    >>> moderator.handle_unsubscription(mlist, id_5, Action.discard)
783
 
    >>> print requests.get_request(id_5)
784
 
    None
785
 
    >>> mlist.members.get_member('gperson@example.com')
786
 
    <Member: gperson@example.com on alist@example.com as MemberRole.member>
787
 
 
788
 
The request can be rejected, in which case a message is sent to the member,
789
 
and the person remains a member of the mailing list.
790
 
::
791
 
 
792
 
    >>> moderator.handle_unsubscription(mlist, id_6, Action.reject,
793
 
    ...     'This list is a prison.')
794
 
    >>> print requests.get_request(id_6)
795
 
    None
796
 
    >>> messages = get_queue_messages('virgin')
797
 
    >>> len(messages)
798
 
    1
799
 
 
800
 
    >>> print messages[0].msg.as_string()
801
 
    MIME-Version: 1.0
802
 
    Content-Type: text/plain; charset="us-ascii"
803
 
    Content-Transfer-Encoding: 7bit
804
 
    Subject: Request to mailing list "A Test List" rejected
805
 
    From: alist-bounces@example.com
806
 
    To: hperson@example.com
807
 
    Message-ID: ...
808
 
    Date: ...
809
 
    Precedence: bulk
810
 
    <BLANKLINE>
811
 
    Your request to the alist@example.com mailing list
812
 
    <BLANKLINE>
813
 
        Unsubscription request
814
 
    <BLANKLINE>
815
 
    has been rejected by the list moderator.  The moderator gave the
816
 
    following reason for rejecting your request:
817
 
    <BLANKLINE>
818
 
    "This list is a prison."
819
 
    <BLANKLINE>
820
 
    Any questions or comments should be directed to the list administrator
821
 
    at:
822
 
    <BLANKLINE>
823
 
        alist-owner@example.com
824
 
    <BLANKLINE>
825
 
 
826
 
    >>> dump_msgdata(messages[0].msgdata)
827
 
    _parsemsg           : False
828
 
    listname            : alist@example.com
829
 
    nodecorate          : True
830
 
    recipients          : set([u'hperson@example.com'])
831
 
    reduced_list_headers: True
832
 
    version             : 3
833
 
 
834
 
    >>> mlist.members.get_member('hperson@example.com')
835
 
    <Member: hperson@example.com on alist@example.com as MemberRole.member>
836
 
 
837
 
The unsubscription request can also be accepted.  This removes the member from
838
 
the mailing list.
839
 
 
840
 
    >>> mlist.send_goodbye_msg = True
841
 
    >>> mlist.goodbye_msg = 'So long!'
842
 
    >>> mlist.admin_immed_notify = False
843
 
    >>> id_7 = moderator.hold_unsubscription(mlist, 'gperson@example.com')
844
 
    >>> moderator.handle_unsubscription(mlist, id_7, Action.accept)
845
 
    >>> print mlist.members.get_member('gperson@example.com')
846
 
    None
847
 
 
848
 
There are now two messages in the virgin queue, one to the member who was just
849
 
unsubscribed and another to the moderators informing them of this membership
850
 
change.
851
 
 
852
 
    >>> messages = get_queue_messages('virgin')
853
 
    >>> len(messages)
854
 
    2
855
 
 
856
 
The goodbye message...
857
 
::
858
 
 
859
 
    >>> print messages[0].msg.as_string()
860
 
    MIME-Version: 1.0
861
 
    Content-Type: text/plain; charset="us-ascii"
862
 
    Content-Transfer-Encoding: 7bit
863
 
    Subject: You have been unsubscribed from the A Test List mailing list
864
 
    From: alist-bounces@example.com
865
 
    To: gperson@example.com
866
 
    Message-ID: ...
867
 
    Date: ...
868
 
    Precedence: bulk
869
 
    <BLANKLINE>
870
 
    So long!
871
 
    <BLANKLINE>
872
 
 
873
 
    >>> dump_msgdata(messages[0].msgdata)
874
 
    _parsemsg           : False
875
 
    listname            : alist@example.com
876
 
    nodecorate          : True
877
 
    recipients          : set([u'gperson@example.com'])
878
 
    reduced_list_headers: True
879
 
    verp                : False
880
 
    version             : 3
881
 
 
882
 
...and the admin message.
883
 
::
884
 
 
885
 
    >>> print messages[1].msg.as_string()
886
 
    MIME-Version: 1.0
887
 
    Content-Type: text/plain; charset="us-ascii"
888
 
    Content-Transfer-Encoding: 7bit
889
 
    Subject: A Test List unsubscription notification
890
 
    From: noreply@example.com
891
 
    To: alist-owner@example.com
892
 
    Message-ID: ...
893
 
    Date: ...
894
 
    Precedence: bulk
895
 
    <BLANKLINE>
896
 
    gperson@example.com has been removed from A Test List.
897
 
    <BLANKLINE>
898
 
 
899
 
    >>> dump_msgdata(messages[1].msgdata)
900
 
    _parsemsg           : False
901
 
    envsender           : noreply@example.com
902
 
    listname            : alist@example.com
903
 
    nodecorate          : True
904
 
    recipients          : set([])
905
 
    reduced_list_headers: True
906
 
    version             : 3