Fix regression.
[mailman.git] / src / mailman / app / docs / moderator.rst
blob43dc7688f82a976aaef5f7fc3fee9d0858411aae
1 .. _app-moderator:
3 ============================
4 Application level moderation
5 ============================
7 At an application level, moderation involves holding messages and membership
8 changes for moderator approval.  This utilizes the :ref:`lower level interface
9 <model-requests>` for list-centric moderation requests.
11 Moderation is always mailing list-centric.
13     >>> mlist = create_list('ant@example.com')
14     >>> mlist.preferred_language = 'en'
15     >>> mlist.display_name = 'A Test List'
16     >>> mlist.admin_immed_notify = False
18 We'll use the lower level API for diagnostic purposes.
20     >>> from mailman.interfaces.requests import IListRequests
21     >>> requests = IListRequests(mlist)
24 Message moderation
25 ==================
27 Holding messages
28 ----------------
30 Anne posts a message to the mailing list, but she is not a member of the list,
31 so the message is held for moderator approval.
33     >>> msg = message_from_string("""\
34     ... From: anne@example.org
35     ... To: ant@example.com
36     ... Subject: Something important
37     ... Message-ID: <aardvark>
38     ...
39     ... Here's something important about our mailing list.
40     ... """)
42 *Holding a message* means keeping a copy of it that a moderator must approve
43 before the message is posted to the mailing list.  To hold the message, the
44 message, its metadata, and a reason for the hold must be provided.  In this
45 case, we won't include any additional metadata.
47     >>> from mailman.app.moderator import hold_message
48     >>> hold_message(mlist, msg, {}, 'Needs approval')
49     1
51 We can also hold a message with some additional metadata.
54     >>> msg = message_from_string("""\
55     ... From: bart@example.org
56     ... To: ant@example.com
57     ... Subject: Something important
58     ... Message-ID: <badger>
59     ...
60     ... Here's something important about our mailing list.
61     ... """)
62     >>> msgdata = dict(sender='anne@example.com', approved=True)
64     >>> hold_message(mlist, msg, msgdata, 'Feeling ornery')
65     2
68 Disposing of messages
69 ---------------------
71 The moderator can select one of several dispositions:
73   * discard - throw the message away.
74   * reject - bounces the message back to the original author.
75   * defer - defer any action on the message (continue to hold it)
76   * accept - accept the message for posting.
78 The most trivial is to simply defer a decision for now.
80     >>> from mailman.interfaces.action import Action
81     >>> from mailman.app.moderator import handle_message
82     >>> handle_message(mlist, 1, Action.defer)
84 This leaves the message in the requests database.
86     >>> key, data = requests.get_request(1)
87     >>> print(key)
88     <aardvark>
90 The moderator can also discard the message.
92     >>> handle_message(mlist, 1, Action.discard)
93     >>> print(requests.get_request(1))
94     None
96 The message can be rejected, which bounces the message back to the original
97 sender.
99     >>> handle_message(mlist, 2, Action.reject, 'Off topic')
101 The message is no longer available in the requests database.
103     >>> print(requests.get_request(2))
104     None
106 And there is one message in the *virgin* queue - the rejection notice.
108     >>> from mailman.testing.helpers import get_queue_messages
109     >>> messages = get_queue_messages('virgin')
110     >>> len(messages)
111     1
112     >>> print(messages[0].msg.as_string())
113     MIME-Version: 1.0
114     ...
115     Subject: Request to mailing list "A Test List" rejected
116     From: ant-bounces@example.com
117     To: bart@example.org
118     ...
119     <BLANKLINE>
120     Your request to the ant@example.com mailing list
121     <BLANKLINE>
122         Posting of your message titled "Something important"
123     <BLANKLINE>
124     has been rejected by the list moderator.  The moderator gave the
125     following reason for rejecting your request:
126     <BLANKLINE>
127     "Off topic"
128     <BLANKLINE>
129     Any questions or comments should be directed to the list administrator
130     at:
131     <BLANKLINE>
132         ant-owner@example.com
133     <BLANKLINE>
135 The bounce gets sent to the original sender.
137     >>> for recipient in sorted(messages[0].msgdata['recipients']):
138     ...     print(recipient)
139     bart@example.org
141 Or the message can be approved.
143     >>> msg = message_from_string("""\
144     ... From: cris@example.org
145     ... To: ant@example.com
146     ... Subject: Something important
147     ... Message-ID: <caribou>
148     ...
149     ... Here's something important about our mailing list.
150     ... """)
151     >>> id = hold_message(mlist, msg, {}, 'Needs approval')
152     >>> handle_message(mlist, id, Action.accept)
154 This places the message back into the incoming queue for further processing,
155 however the message metadata indicates that the message has been approved.
158     >>> messages = get_queue_messages('pipeline')
159     >>> len(messages)
160     1
161     >>> print(messages[0].msg.as_string())
162     From: cris@example.org
163     To: ant@example.com
164     Subject: Something important
165     ...
167     >>> dump_msgdata(messages[0].msgdata)
168     _parsemsg         : False
169     approved          : True
170     moderator_approved: True
171     version           : 3
174 Preserving and forwarding the message
175 -------------------------------------
177 In addition to any of the above dispositions, the message can also be
178 preserved for further study.  Ordinarily the message is removed from the
179 global message store after its disposition (though approved messages may be
180 re-added to the message store later).  When handling a message, we can ask for
181 a copy to be preserve, which skips deleting the message from the storage.
184     >>> msg = message_from_string("""\
185     ... From: dave@example.org
186     ... To: ant@example.com
187     ... Subject: Something important
188     ... Message-ID: <dolphin>
189     ...
190     ... Here's something important about our mailing list.
191     ... """)
192     >>> id = hold_message(mlist, msg, {}, 'Needs approval')
193     >>> handle_message(mlist, id, Action.discard, preserve=True)
195     >>> from mailman.interfaces.messages import IMessageStore
196     >>> from zope.component import getUtility
197     >>> message_store = getUtility(IMessageStore)
198     >>> print(message_store.get_message_by_id('<dolphin>')['message-id'])
199     <dolphin>
201 Orthogonal to preservation, the message can also be forwarded to another
202 address.  This is helpful for getting the message into the inbox of one of the
203 moderators.
206     >>> msg = message_from_string("""\
207     ... From: elly@example.org
208     ... To: ant@example.com
209     ... Subject: Something important
210     ... Message-ID: <elephant>
211     ...
212     ... Here's something important about our mailing list.
213     ... """)
214     >>> req_id = hold_message(mlist, msg, {}, 'Needs approval')
215     >>> handle_message(mlist, req_id, Action.discard,
216     ...                forward=['zack@example.com'])
218 The forwarded message is in the virgin queue, destined for the moderator.
221     >>> messages = get_queue_messages('virgin')
222     >>> len(messages)
223     1
224     >>> print(messages[0].msg.as_string())
225     Subject: Forward of moderated message
226     From: ant-bounces@example.com
227     To: zack@example.com
228     ...
230     >>> for recipient in sorted(messages[0].msgdata['recipients']):
231     ...     print(recipient)
232     zack@example.com
235 Holding subscription requests
236 =============================
238 For closed lists, subscription requests will also be held for moderator
239 approval.  In this case, several pieces of information related to the
240 subscription must be provided, including the subscriber's address and real
241 name, what kind of delivery option they are choosing and their preferred
242 language.
244     >>> from mailman.app.moderator import hold_subscription
245     >>> from mailman.interfaces.member import DeliveryMode
246     >>> from mailman.interfaces.subscriptions import RequestRecord
247     >>> req_id = hold_subscription(
248     ...     mlist,
249     ...     RequestRecord('fred@example.org', 'Fred Person',
250     ...                   DeliveryMode.regular, 'en'))
253 Disposing of membership change requests
254 ---------------------------------------
256 Just as with held messages, the moderator can select one of several
257 dispositions for this membership change request.  The most trivial is to
258 simply defer a decision for now.
260     >>> from mailman.app.moderator import handle_subscription
261     >>> handle_subscription(mlist, req_id, Action.defer)
262     >>> requests.get_request(req_id) is not None
263     True
265 The held subscription can also be discarded.
267     >>> handle_subscription(mlist, req_id, Action.discard)
268     >>> print(requests.get_request(req_id))
269     None
271 Gwen tries to subscribe to the mailing list, but...
273     >>> req_id = hold_subscription(
274     ...     mlist,
275     ...     RequestRecord('gwen@example.org', 'Gwen Person',
276     ...                   DeliveryMode.regular, 'en'))
279 ...her request is rejected...
281     >>> handle_subscription(
282     ...     mlist, req_id, Action.reject, 'This is a closed list')
283     >>> messages = get_queue_messages('virgin')
284     >>> len(messages)
285     1
287 ...and she receives a rejection notice.
289     >>> print(messages[0].msg.as_string())
290     MIME-Version: 1.0
291     ...
292     Subject: Request to mailing list "A Test List" rejected
293     From: ant-bounces@example.com
294     To: gwen@example.org
295     ...
296     Your request to the ant@example.com mailing list
297     <BLANKLINE>
298         Subscription request
299     <BLANKLINE>
300     has been rejected by the list moderator.  The moderator gave the
301     following reason for rejecting your request:
302     <BLANKLINE>
303     "This is a closed list"
304     ...
306 The subscription can also be accepted.  This subscribes the address to the
307 mailing list.
309     >>> mlist.send_welcome_message = False
310     >>> req_id = hold_subscription(
311     ...     mlist,
312     ...     RequestRecord('herb@example.org', 'Herb Person',
313     ...                   DeliveryMode.regular, 'en'))
315 The moderators accept the subscription request.
317     >>> handle_subscription(mlist, req_id, Action.accept)
319 And now Herb is a member of the mailing list.
321     >>> print(mlist.members.get_member('herb@example.org').address)
322     Herb Person <herb@example.org>
325 Holding unsubscription requests
326 ===============================
328 Some lists require moderator approval for unsubscriptions.  In this case, only
329 the unsubscribing address is required.
331 Herb now wants to leave the mailing list, but his request must be approved.
333     >>> from mailman.app.moderator import hold_unsubscription
334     >>> req_id = hold_unsubscription(mlist, 'herb@example.org')
336 As with subscription requests, the unsubscription request can be deferred.
338     >>> from mailman.app.moderator import handle_unsubscription
339     >>> handle_unsubscription(mlist, req_id, Action.defer)
340     >>> print(mlist.members.get_member('herb@example.org').address)
341     Herb Person <herb@example.org>
343 The held unsubscription can also be discarded, and the member will remain
344 subscribed.
346     >>> handle_unsubscription(mlist, req_id, Action.discard)
347     >>> print(mlist.members.get_member('herb@example.org').address)
348     Herb Person <herb@example.org>
350 The request can be rejected, in which case a message is sent to the member,
351 and the person remains a member of the mailing list.
353     >>> req_id = hold_unsubscription(mlist, 'herb@example.org')
354     >>> handle_unsubscription(mlist, req_id, Action.reject, 'No can do')
355     >>> print(mlist.members.get_member('herb@example.org').address)
356     Herb Person <herb@example.org>
358 Herb gets a rejection notice.
361     >>> messages = get_queue_messages('virgin')
362     >>> len(messages)
363     1
365     >>> print(messages[0].msg.as_string())
366     MIME-Version: 1.0
367     ...
368     Subject: Request to mailing list "A Test List" rejected
369     From: ant-bounces@example.com
370     To: herb@example.org
371     ...
372     Your request to the ant@example.com mailing list
373     <BLANKLINE>
374         Unsubscription request
375     <BLANKLINE>
376     has been rejected by the list moderator.  The moderator gave the
377     following reason for rejecting your request:
378     <BLANKLINE>
379     "No can do"
380     ...
382 The unsubscription request can also be accepted.  This removes the member from
383 the mailing list.
385     >>> req_id = hold_unsubscription(mlist, 'herb@example.org')
386     >>> mlist.send_goodbye_message = False
387     >>> handle_unsubscription(mlist, req_id, Action.accept)
388     >>> print(mlist.members.get_member('herb@example.org'))
389     None
392 Notifications
393 =============
395 Membership change requests
396 --------------------------
398 Usually, the list administrators want to be notified when there are membership
399 change requests they need to moderate.  These notifications are sent when the
400 list is configured to send them.
402     >>> mlist.admin_immed_notify = True
404 Iris tries to subscribe to the mailing list.
406     >>> req_id = hold_subscription(mlist,
407     ...     RequestRecord('iris@example.org', 'Iris Person',
408     ...                   DeliveryMode.regular, 'en'))
410 There's now a message in the virgin queue, destined for the list owner.
412     >>> messages = get_queue_messages('virgin')
413     >>> len(messages)
414     1
415     >>> print(messages[0].msg.as_string())
416     MIME-Version: 1.0
417     ...
418     Subject: New subscription request to A Test List from iris@example.org
419     From: ant-owner@example.com
420     To: ant-owner@example.com
421     ...
422     Your authorization is required for a mailing list subscription request
423     approval:
424     <BLANKLINE>
425         For:  iris@example.org
426         List: ant@example.com
428 Similarly, the administrator gets notifications on unsubscription requests.
429 Jeff is a member of the mailing list, and chooses to unsubscribe.
431     >>> unsub_req_id = hold_unsubscription(mlist, 'jeff@example.org')
432     >>> messages = get_queue_messages('virgin')
433     >>> len(messages)
434     1
435     >>> print(messages[0].msg.as_string())
436     MIME-Version: 1.0
437     ...
438     Subject: New unsubscription request from A Test List by jeff@example.org
439     From: ant-owner@example.com
440     To: ant-owner@example.com
441     ...
442     Your authorization is required for a mailing list unsubscription
443     request approval:
444     <BLANKLINE>
445         By:   jeff@example.org
446         From: ant@example.com
447     ...
450 Membership changes
451 ------------------
453 When a new member request is accepted, the mailing list administrators can
454 receive a membership change notice.
456     >>> mlist.admin_notify_mchanges = True
457     >>> mlist.admin_immed_notify = False
458     >>> handle_subscription(mlist, req_id, Action.accept)
459     >>> messages = get_queue_messages('virgin')
460     >>> len(messages)
461     1
462     >>> print(messages[0].msg.as_string())
463     MIME-Version: 1.0
464     ...
465     Subject: A Test List subscription notification
466     From: noreply@example.com
467     To: ant-owner@example.com
468     ...
469     Iris Person <iris@example.org> has been successfully subscribed to A
470     Test List.
472 Similarly when an unsubscription request is accepted, the administrators can
473 get a notification.
475     >>> req_id = hold_unsubscription(mlist, 'iris@example.org')
476     >>> handle_unsubscription(mlist, req_id, Action.accept)
477     >>> messages = get_queue_messages('virgin')
478     >>> len(messages)
479     1
480     >>> print(messages[0].msg.as_string())
481     MIME-Version: 1.0
482     ...
483     Subject: A Test List unsubscription notification
484     From: noreply@example.com
485     To: ant-owner@example.com
486     ...
487     Iris Person <iris@example.org> has been removed from A Test List.
490 Welcome messages
491 ----------------
493 When a member is subscribed to the mailing list via moderator approval, she
494 can get a welcome message.
496     >>> mlist.admin_notify_mchanges = False
497     >>> mlist.send_welcome_message = True
498     >>> req_id = hold_subscription(mlist,
499     ...     RequestRecord('kate@example.org', 'Kate Person',
500     ...                   DeliveryMode.regular, 'en'))
501     >>> handle_subscription(mlist, req_id, Action.accept)
502     >>> messages = get_queue_messages('virgin')
503     >>> len(messages)
504     1
505     >>> print(messages[0].msg.as_string())
506     MIME-Version: 1.0
507     ...
508     Subject: Welcome to the "A Test List" mailing list
509     From: ant-request@example.com
510     To: Kate Person <kate@example.org>
511     ...
512     Welcome to the "A Test List" mailing list!
513     ...
516 Goodbye messages
517 ----------------
519 Similarly, when the member's unsubscription request is approved, she'll get a
520 goodbye message.
522     >>> mlist.send_goodbye_message = True
523     >>> req_id = hold_unsubscription(mlist, 'kate@example.org')
524     >>> handle_unsubscription(mlist, req_id, Action.accept)
525     >>> messages = get_queue_messages('virgin')
526     >>> len(messages)
527     1
528     >>> print(messages[0].msg.as_string())
529     MIME-Version: 1.0
530     ...
531     Subject: You have been unsubscribed from the A Test List mailing list
532     From: ant-bounces@example.com
533     To: kate@example.org
534     ...