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:
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
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)
45 >>> mlist = config.db.list_manager.create('test@example.com')
47 >>> requests = config.db.requests.get_list_requests(mlist)
48 >>> verifyObject(IListRequests, requests)
50 >>> requests.mailing_list
51 <mailing list "test@example.com" at ...>
57 The list's requests database starts out empty.
61 >>> list(requests.held_requests)
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
78 And of course, now we can see that there are four requests being held.
82 >>> requests.count_of(RequestType.held_message)
84 >>> requests.count_of(RequestType.subscription)
86 >>> requests.count_of(RequestType.unsubscription)
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):
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)
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')]
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
125 >>> key, data = requests.get_request(2)
129 Because we did not store additional data with request 2, it comes back as 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)
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)
149 Iterating over requests
150 -----------------------
152 To make it easier to find specific requests, the list requests can be iterated
155 >>> requests.count_of(RequestType.held_message)
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
165 5 hold_5 [('bar', 'no'), ('foo', 'yes')]
171 Once a specific request has been handled, it will be deleted from the requests
174 >>> requests.delete_request(2)
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)
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):
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)
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
210 >>> from Mailman.app import moderator
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'
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
229 ... Here's something important about our mailing list.
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')
239 >>> requests.get_request(id_1) is not None
242 We can also hold a message with some additional metadata.
244 >>> msgdata = dict(sender='aperson@example.com',
246 ... received_time=123.45)
247 >>> id_2 = moderator.hold_message(mlist, msg, msgdata, 'Feeling ornery')
249 >>> requests.get_request(id_2) is not None
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)
258 >>> requests.get_request(id_1) is not None
261 The moderator can also discard the message. This is often done with spam.
264 >>> moderator.handle_message(mlist, id_1, Action.discard)
266 >>> print requests.get_request(id_1)
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')
275 >>> print requests.get_request(id_2)
277 >>> qmsg, qdata = dequeue()
278 >>> print qmsg.as_string()
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
289 Your request to the alist@example.com mailing list
291 Posting of your message titled "Something important"
293 has been rejected by the list moderator. The moderator gave the
294 following reason for rejecting your request:
298 Any questions or comments should be directed to the list administrator
301 alist-owner@example.com
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),
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')
318 >>> moderator.handle_message(mlist, id_3, Action.accept)
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
328 X-List-Sequence-Number: ...
329 X-Mailman-Approved-At: ...
331 Here's something important about our mailing list.
333 >>> sorted(qdata.items())
334 [('_parsemsg', False),
335 ('adminapproved', True), ('approved', True),
336 ('received_time', ...), ('sender', 'aperson@example.com'),
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
347 >>> msg = message_from_string("""\
348 ... From: aperson@example.org
349 ... To: alist@example.com
350 ... Subject: Something important
351 ... Message-ID: <12345>
353 ... Here's something important about our mailing list.
355 >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
357 >>> moderator.handle_message(mlist, id_4, Action.discard)
359 >>> msgs = config.db.message_store.get_messages_by_message_id('<12345>')
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')
368 >>> moderator.handle_message(mlist, id_4, Action.discard, preserve=True)
370 >>> msgs = config.db.message_store.get_messages_by_message_id('<12345>')
371 >>> msgs = list(msgs)
374 >>> print msgs[0].as_string()
375 From: aperson@example.org
376 To: alist@example.com
377 Subject: Something important
379 X-List-ID-Hash: 4CF7EAU3SIXBPXBB5S6PEUMO62MWGQN6
380 X-List-Sequence-Number: 1
382 Here's something important about our mailing list.
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
389 >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
391 >>> moderator.handle_message(mlist, id_4, Action.discard,
392 ... forward=['zperson@example.com'])
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
400 Content-Type: message/rfc822
405 From: aperson@example.org
406 To: alist@example.com
407 Subject: Something important
409 X-List-ID-Hash: 4CF7EAU3SIXBPXBB5S6PEUMO62MWGQN6
410 X-List-Sequence-Number: ...
412 Here's something important about our mailing list.
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
433 >>> id_3 = moderator.hold_subscription(mlist,
434 ... 'bperson@example.org', 'Ben Person',
435 ... '{NONE}abcxyz', DeliveryMode.regular, 'en')
437 >>> requests.get_request(id_3) is not None
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
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/'
456 >>> id_4 = moderator.hold_subscription(mlist,
457 ... 'cperson@example.org', 'Claire Person',
458 ... '{NONE}zyxcba', DeliveryMode.regular, 'en')
460 >>> requests.get_request(id_4) is not None
462 >>> qmsg, qdata = dequeue()
463 >>> print qmsg.as_string()
465 Content-Type: text/plain; charset="us-ascii"
466 Content-Transfer-Encoding: 7bit
467 Subject: New subscription request to list A Test List from
469 From: alist-owner@example.com
470 To: alist-owner@example.com
475 Your authorization is required for a mailing list subscription request
478 For: cperson@example.org
479 List: alist@example.com
481 At your convenience, visit:
483 http://www.example.com/admindb/alist@example.com
485 to process the request.
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)
500 >>> requests.get_request(id_3) is not None
503 The held subscription can also be discarded.
505 >>> moderator.handle_subscription(mlist, id_3, Action.discard)
507 >>> print requests.get_request(id_3)
510 The request can be rejected, in which case a message is sent to the
513 >>> moderator.handle_subscription(mlist, id_4, Action.reject,
514 ... 'This is a closed list')
516 >>> print requests.get_request(id_4)
518 >>> qmsg, qdata = dequeue()
519 >>> print qmsg.as_string()
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
530 Your request to the alist@example.com mailing list
534 has been rejected by the list moderator. The moderator gave the
535 following reason for rejecting your request:
537 "This is a closed list"
539 Any questions or comments should be directed to the list administrator
542 alist-owner@example.com
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
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')
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()
566 Content-Type: text/plain; charset="us-ascii"
567 Content-Transfer-Encoding: 7bit
568 Subject: New subscription request to list A Test List from
570 From: alist-owner@example.com
571 To: alist-owner@example.com
576 Your authorization is required for a mailing list subscription request
579 For: fperson@example.org
580 List: alist@example.com
582 At your convenience, visit:
584 http://www.example.com/admindb/alist@example.com
586 to process the request.
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)
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
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()
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
633 Welcome to the "A Test List" mailing list!
635 To post to this list, send your email to:
639 General information about the mailing list is at:
641 http://www.example.com/listinfo/alist@example.com
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:
647 http://example.com/fperson@example.org
649 You can also make such adjustments via email by sending a message to:
651 alist-request@example.com
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.
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()
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
678 Frank Person <fperson@example.org> has been successfully subscribed to
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')
691 <Member: Frank Person <fperson@example.org>
692 on alist@example.com as MemberRole.member>
693 >>> member.preferred_language
695 >>> print member.delivery_mode
697 >>> user = config.db.user_manager.get_user(member.address.address)
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
713 >>> from Mailman.constants import MemberRole
714 >>> user_1 = config.db.user_manager.create_user('gperson@example.com')
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')
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>
725 >>> id_5 = moderator.hold_unsubscription(mlist, 'gperson@example.com')
727 >>> requests.get_request(id_5) is not None
731 >>> mlist.admin_immed_notify = True
732 >>> id_6 = moderator.hold_unsubscription(mlist, 'hperson@example.com')
734 >>> qmsg, qdata = dequeue()
735 >>> print qmsg.as_string()
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
746 Your authorization is required for a mailing list unsubscription
749 By: hperson@example.com
750 From: alist@example.com
752 At your convenience, visit:
754 http://www.example.com/admindb/alist@example.com
756 to process the request.
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)
770 >>> requests.get_request(id_5) is not None
773 The held unsubscription can also be discarded, and the member will remain
776 >>> moderator.handle_unsubscription(mlist, id_5, Action.discard)
778 >>> print requests.get_request(id_5)
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.')
789 >>> print requests.get_request(id_6)
791 >>> qmsg, qdata = dequeue()
792 >>> print qmsg.as_string()
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
803 Your request to the alist@example.com mailing list
805 Unsubscription request
807 has been rejected by the list moderator. The moderator gave the
808 following reason for rejecting your request:
810 "This list is a prison."
812 Any questions or comments should be directed to the list administrator
815 alist-owner@example.com
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
829 >>> mlist.send_goodbye_msg = True
830 >>> mlist.goodbye_msg = 'So long!'
831 >>> mlist.admin_immed_notify = False
833 >>> id_7 = moderator.hold_unsubscription(mlist, 'gperson@example.com')
835 >>> moderator.handle_unsubscription(mlist, id_7, Action.accept)
837 >>> print mlist.members.get_member('gperson@example.com')
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
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
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()
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
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()
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
893 gperson@example.com has been removed from A Test List.
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)]