140
135
>>> requests.count_of(RequestType.held_message)
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])
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
156
151
Deleting requests
157
152
=================
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
162
159
>>> requests.delete_request(2)
163
160
>>> requests.count
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
163
Request 2 is no longer in the database.
172
165
>>> print requests.get_request(2)
175
We get an exception if we ask to delete a request that isn't in the database.
177
>>> requests.delete_request(801)
178
Traceback (most recent call last):
182
For the next section, we first clean up all the current requests.
184
168
>>> for request in requests.held_requests:
185
169
... requests.delete_request(request.id)
186
170
>>> requests.count
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.
198
>>> from mailman.app import moderator
204
For this section, we need a mailing list and at least one message.
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
214
... Here's something important about our mailing list.
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.
222
>>> id_1 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
223
>>> requests.get_request(id_1) is not None
226
We can also hold a message with some additional metadata.
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',
234
>>> id_2 = moderator.hold_message(mlist, msg, msgdata, 'Feeling ornery')
235
>>> requests.get_request(id_2) is not None
238
Once held, the moderator can select one of several dispositions. The most
239
trivial is to simply defer a decision for now.
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
246
The moderator can also discard the message. This is often done with spam.
249
>>> moderator.handle_message(mlist, id_1, Action.discard)
250
>>> print requests.get_request(id_1)
252
>>> from mailman.testing.helpers import get_queue_messages
253
>>> get_queue_messages('virgin')
256
The message can be rejected, meaning it is bounced back to the sender.
258
>>> moderator.handle_message(mlist, id_2, Action.reject, 'Off topic')
259
>>> print requests.get_request(id_2)
261
>>> messages = get_queue_messages('virgin')
264
>>> print messages[0].msg.as_string()
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
275
Your request to the alist@example.com mailing list
277
Posting of your message titled "Something important"
279
has been rejected by the list moderator. The moderator gave the
280
following reason for rejecting your request:
284
Any questions or comments should be directed to the list administrator
287
alist-owner@example.com
289
>>> dump_msgdata(messages[0].msgdata)
291
listname : alist@example.com
293
recipients : set([u'aperson@example.org'])
294
reduced_list_headers: True
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.
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')
306
>>> print messages[0].msg.as_string()
307
From: aperson@example.org
308
To: alist@example.com
309
Subject: Something important
311
X-Message-ID-Hash: ...
312
X-Mailman-Approved-At: ...
314
Here's something important about our mailing list.
316
>>> dump_msgdata(messages[0].msgdata)
319
moderator_approved: True
320
sender : aperson@example.com
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
332
>>> msg = message_from_string("""\
333
... From: aperson@example.org
334
... To: alist@example.com
335
... Subject: Something important
336
... Message-ID: <12345>
338
... Here's something important about our mailing list.
340
>>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
341
>>> moderator.handle_message(mlist, id_4, Action.discard)
343
>>> from mailman.interfaces.messages import IMessageStore
344
>>> from zope.component import getUtility
345
>>> message_store = getUtility(IMessageStore)
347
>>> print message_store.get_message_by_id('<12345>')
350
But if we ask to preserve the message when we discard it, it will be held in
351
the message store after disposition.
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
361
X-Message-ID-Hash: 4CF7EAU3SIXBPXBB5S6PEUMO62MWGQN6
363
Here's something important about our mailing list.
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
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'])
379
>>> messages = get_queue_messages('virgin')
382
>>> print messages[0].msg.as_string()
383
Subject: Forward of moderated message
384
From: alist-bounces@example.com
385
To: zperson@example.com
387
Content-Type: message/rfc822
392
From: aperson@example.org
393
To: alist@example.com
394
Subject: Something important
396
X-Message-ID-Hash: EN2R5UQFMOUTCL44FLNNPLSXBIZW62ER
398
Here's something important about our mailing list.
401
>>> dump_msgdata(messages[0].msgdata)
403
listname : alist@example.com
405
recipients : [u'zperson@example.com']
406
reduced_list_headers: True
410
Holding subscription requests
411
=============================
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.
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
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
431
>>> get_queue_messages('virgin')
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.
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
447
>>> messages = get_queue_messages('virgin')
451
>>> print messages[0].msg.as_string()
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
462
Your authorization is required for a mailing list subscription request
465
For: cperson@example.org
466
List: alist@example.com
468
At your convenience, visit:
470
http://lists.example.com/admindb/alist@example.com
472
to process the request.
475
>>> dump_msgdata(messages[0].msgdata)
477
listname : alist@example.com
479
recipients : set([u'alist-owner@example.com'])
480
reduced_list_headers: True
484
Once held, the moderator can select one of several dispositions. The most
485
trivial is to simply defer a decision for now.
487
>>> moderator.handle_subscription(mlist, id_3, Action.defer)
488
>>> requests.get_request(id_3) is not None
491
The held subscription can also be discarded.
493
>>> moderator.handle_subscription(mlist, id_3, Action.discard)
494
>>> print requests.get_request(id_3)
497
The request can be rejected, in which case a message is sent to the
501
>>> moderator.handle_subscription(mlist, id_4, Action.reject,
502
... 'This is a closed list')
503
>>> print requests.get_request(id_4)
505
>>> messages = get_queue_messages('virgin')
509
>>> print messages[0].msg.as_string()
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
520
Your request to the alist@example.com mailing list
524
has been rejected by the list moderator. The moderator gave the
525
following reason for rejecting your request:
527
"This is a closed list"
529
Any questions or comments should be directed to the list administrator
532
alist-owner@example.com
535
>>> dump_msgdata(messages[0].msgdata)
537
listname : alist@example.com
539
recipients : set([u'cperson@example.org'])
540
reduced_list_headers: True
543
The subscription can also be accepted. This subscribes the address to the
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')
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.
556
>>> messages = get_queue_messages('virgin')
560
>>> print messages[0].msg.as_string()
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
571
Your authorization is required for a mailing list subscription request
574
For: fperson@example.org
575
List: alist@example.com
577
At your convenience, visit:
579
http://lists.example.com/admindb/alist@example.com
581
to process the request.
584
>>> dump_msgdata(messages[0].msgdata)
586
listname : alist@example.com
588
recipients : set([u'alist-owner@example.com'])
589
reduced_list_headers: True
593
Accept the subscription request.
595
>>> mlist.admin_notify_mchanges = True
596
>>> moderator.handle_subscription(mlist, id_4, Action.accept)
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.
603
>>> messages = get_queue_messages('virgin', sort_on='subject')
607
The welcome message is sent to the person who just subscribed.
610
>>> print messages[1].msg.as_string()
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>
622
Welcome to the "A Test List" mailing list!
624
To post to this list, send your email to:
628
General information about the mailing list is at:
630
http://lists.example.com/listinfo/alist@example.com
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:
636
http://example.com/fperson@example.org
638
You can also make such adjustments via email by sending a message to:
640
alist-request@example.com
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.
649
>>> dump_msgdata(messages[1].msgdata)
651
listname : alist@example.com
653
recipients : set([u'Frank Person <fperson@example.org>'])
654
reduced_list_headers: True
658
The admin message is sent to the moderators.
661
>>> print messages[0].msg.as_string()
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
672
Frank Person <fperson@example.org> has been successfully subscribed to
676
>>> dump_msgdata(messages[0].msgdata)
678
envsender : noreply@example.com
679
listname : alist@example.com
682
reduced_list_headers: True
685
Frank Person is now a member of the mailing list.
688
>>> member = mlist.members.get_member('fperson@example.org')
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
696
>>> print member.user.display_name
698
>>> print member.user.password
702
Holding unsubscription requests
703
===============================
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
712
>>> from mailman.interfaces.usermanager import IUserManager
713
>>> from zope.component import getUtility
714
>>> user_manager = getUtility(IUserManager)
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>
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>
728
>>> id_5 = moderator.hold_unsubscription(mlist, 'gperson@example.com')
729
>>> requests.get_request(id_5) is not None
731
>>> get_queue_messages('virgin')
733
>>> mlist.admin_immed_notify = True
734
>>> id_6 = moderator.hold_unsubscription(mlist, 'hperson@example.com')
736
>>> messages = get_queue_messages('virgin')
739
>>> print messages[0].msg.as_string()
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
750
Your authorization is required for a mailing list unsubscription
753
By: hperson@example.com
754
From: alist@example.com
756
At your convenience, visit:
758
http://lists.example.com/admindb/alist@example.com
760
to process the request.
763
>>> dump_msgdata(messages[0].msgdata)
765
listname : alist@example.com
767
recipients : set([u'alist-owner@example.com'])
768
reduced_list_headers: True
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.
775
>>> moderator.handle_unsubscription(mlist, id_5, Action.defer)
776
>>> requests.get_request(id_5) is not None
779
The held unsubscription can also be discarded, and the member will remain
782
>>> moderator.handle_unsubscription(mlist, id_5, Action.discard)
783
>>> print requests.get_request(id_5)
785
>>> mlist.members.get_member('gperson@example.com')
786
<Member: gperson@example.com on alist@example.com as MemberRole.member>
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.
792
>>> moderator.handle_unsubscription(mlist, id_6, Action.reject,
793
... 'This list is a prison.')
794
>>> print requests.get_request(id_6)
796
>>> messages = get_queue_messages('virgin')
800
>>> print messages[0].msg.as_string()
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
811
Your request to the alist@example.com mailing list
813
Unsubscription request
815
has been rejected by the list moderator. The moderator gave the
816
following reason for rejecting your request:
818
"This list is a prison."
820
Any questions or comments should be directed to the list administrator
823
alist-owner@example.com
826
>>> dump_msgdata(messages[0].msgdata)
828
listname : alist@example.com
830
recipients : set([u'hperson@example.com'])
831
reduced_list_headers: True
834
>>> mlist.members.get_member('hperson@example.com')
835
<Member: hperson@example.com on alist@example.com as MemberRole.member>
837
The unsubscription request can also be accepted. This removes the member from
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')
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
852
>>> messages = get_queue_messages('virgin')
856
The goodbye message...
859
>>> print messages[0].msg.as_string()
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
873
>>> dump_msgdata(messages[0].msgdata)
875
listname : alist@example.com
877
recipients : set([u'gperson@example.com'])
878
reduced_list_headers: True
882
...and the admin message.
885
>>> print messages[1].msg.as_string()
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
896
gperson@example.com has been removed from A Test List.
899
>>> dump_msgdata(messages[1].msgdata)
901
envsender : noreply@example.com
902
listname : alist@example.com
905
reduced_list_headers: True