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