Branch his ready.
[mailman.git] / src / mailman / app / moderator.py
blobeb848ea08432b0da4400c161c0c4abe97dc3f8e8
1 # Copyright (C) 2007-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Application support for moderators."""
20 __all__ = [
21 'handle_ListDeletingEvent',
22 'handle_message',
23 'handle_subscription',
24 'handle_unsubscription',
25 'hold_message',
26 'hold_subscription',
27 'hold_unsubscription',
28 'send_rejection',
32 import time
33 import logging
35 from email.utils import formataddr, formatdate, getaddresses, make_msgid
36 from mailman.app.membership import add_member, delete_member
37 from mailman.app.notifications import send_admin_subscription_notice
38 from mailman.config import config
39 from mailman.core.i18n import _
40 from mailman.email.message import UserNotification
41 from mailman.interfaces.action import Action
42 from mailman.interfaces.languages import ILanguageManager
43 from mailman.interfaces.listmanager import ListDeletingEvent
44 from mailman.interfaces.member import (
45 AlreadySubscribedError, DeliveryMode, NotAMemberError)
46 from mailman.interfaces.messages import IMessageStore
47 from mailman.interfaces.requests import IListRequests, RequestType
48 from mailman.interfaces.subscriptions import RequestRecord
49 from mailman.utilities.datetime import now
50 from mailman.utilities.i18n import make
51 from zope.component import getUtility
54 NL = '\n'
56 vlog = logging.getLogger('mailman.vette')
57 slog = logging.getLogger('mailman.subscribe')
61 def hold_message(mlist, msg, msgdata=None, reason=None):
62 """Hold a message for moderator approval.
64 The message is added to the mailing list's request database.
66 :param mlist: The mailing list to hold the message on.
67 :param msg: The message to hold.
68 :param msgdata: Optional message metadata to hold. If not given, a new
69 metadata dictionary is created and held with the message.
70 :param reason: Optional string reason why the message is being held. If
71 not given, the empty string is used.
72 :return: An id used to handle the held message later.
73 """
74 if msgdata is None:
75 msgdata = {}
76 else:
77 # Make a copy of msgdata so that subsequent changes won't corrupt the
78 # request database. TBD: remove the `filebase' key since this will
79 # not be relevant when the message is resurrected.
80 msgdata = msgdata.copy()
81 if reason is None:
82 reason = ''
83 # Add the message to the message store. It is required to have a
84 # Message-ID header.
85 message_id = msg.get('message-id')
86 if message_id is None:
87 msg['Message-ID'] = message_id = make_msgid()
88 elif isinstance(message_id, bytes):
89 message_id = message_id.decode('ascii')
90 getUtility(IMessageStore).add(msg)
91 # Prepare the message metadata with some extra information needed only by
92 # the moderation interface.
93 msgdata['_mod_message_id'] = message_id
94 msgdata['_mod_listid'] = mlist.list_id
95 msgdata['_mod_sender'] = msg.sender
96 msgdata['_mod_subject'] = msg.get('subject', _('(no subject)'))
97 msgdata['_mod_reason'] = reason
98 msgdata['_mod_hold_date'] = now().isoformat()
99 # Now hold this request. We'll use the message_id as the key.
100 requestsdb = IListRequests(mlist)
101 request_id = requestsdb.hold_request(
102 RequestType.held_message, message_id, msgdata)
103 return request_id
107 def handle_message(mlist, id, action,
108 comment=None, preserve=False, forward=None):
109 message_store = getUtility(IMessageStore)
110 requestdb = IListRequests(mlist)
111 key, msgdata = requestdb.get_request(id)
112 # Handle the action.
113 rejection = None
114 message_id = msgdata['_mod_message_id']
115 sender = msgdata['_mod_sender']
116 subject = msgdata['_mod_subject']
117 if action in (Action.defer, Action.hold):
118 # Nothing to do, but preserve the message for later.
119 preserve = True
120 elif action is Action.discard:
121 rejection = 'Discarded'
122 elif action is Action.reject:
123 rejection = 'Refused'
124 member = mlist.members.get_member(sender)
125 if member:
126 language = member.preferred_language
127 else:
128 language = None
129 send_rejection(
130 mlist, _('Posting of your message titled "$subject"'),
131 sender, comment or _('[No reason given]'), language)
132 elif action is Action.accept:
133 # Start by getting the message from the message store.
134 msg = message_store.get_message_by_id(message_id)
135 # Delete moderation-specific entries from the message metadata.
136 for key in list(msgdata):
137 if key.startswith('_mod_'):
138 del msgdata[key]
139 # Add some metadata to indicate this message has now been approved.
140 msgdata['approved'] = True
141 msgdata['moderator_approved'] = True
142 # Calculate a new filebase for the approved message, otherwise
143 # delivery errors will cause duplicates.
144 if 'filebase' in msgdata:
145 del msgdata['filebase']
146 # Queue the file for delivery. Trying to deliver the message directly
147 # here can lead to a huge delay in web turnaround. Log the moderation
148 # and add a header.
149 msg['X-Mailman-Approved-At'] = formatdate(
150 time.mktime(now().timetuple()), localtime=True)
151 vlog.info('held message approved, message-id: %s',
152 msg.get('message-id', 'n/a'))
153 # Stick the message back in the incoming queue for further
154 # processing.
155 config.switchboards['pipeline'].enqueue(msg, _metadata=msgdata)
156 else:
157 raise AssertionError('Unexpected action: {0}'.format(action))
158 # Forward the message.
159 if forward:
160 # Get a copy of the original message from the message store.
161 msg = message_store.get_message_by_id(message_id)
162 # It's possible the forwarding address list is a comma separated list
163 # of display_name/address pairs.
164 addresses = [addr[1] for addr in getaddresses(forward)]
165 language = mlist.preferred_language
166 if len(addresses) == 1:
167 # If the address getting the forwarded message is a member of
168 # the list, we want the headers of the outer message to be
169 # encoded in their language. Otherwise it'll be the preferred
170 # language of the mailing list. This is better than sending a
171 # separate message per recipient.
172 member = mlist.members.get_member(addresses[0])
173 if member:
174 language = member.preferred_language
175 with _.using(language.code):
176 fmsg = UserNotification(
177 addresses, mlist.bounces_address,
178 _('Forward of moderated message'),
179 lang=language)
180 fmsg.set_type('message/rfc822')
181 fmsg.attach(msg)
182 fmsg.send(mlist)
183 # Delete the message from the message store if it is not being preserved.
184 if not preserve:
185 message_store.delete_message(message_id)
186 requestdb.delete_request(id)
187 # Log the rejection
188 if rejection:
189 note = """%s: %s posting:
190 \tFrom: %s
191 \tSubject: %s"""
192 if comment:
193 note += '\n\tReason: ' + comment
194 vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
198 def hold_subscription(mlist, record):
199 data = dict(when=now().isoformat(),
200 email=record.email,
201 display_name=record.display_name,
202 delivery_mode=record.delivery_mode.name,
203 language=record.language)
204 # Now hold this request. We'll use the email address as the key.
205 requestsdb = IListRequests(mlist)
206 request_id = requestsdb.hold_request(
207 RequestType.subscription, record.email, data)
208 vlog.info('%s: held subscription request from %s',
209 mlist.fqdn_listname, record.email)
210 # Possibly notify the administrator in default list language
211 if mlist.admin_immed_notify:
212 email = record.email # XXX: seems unnecessary
213 subject = _(
214 'New subscription request to $mlist.display_name from $email')
215 text = make('subauth.txt',
216 mailing_list=mlist,
217 username=record.email,
218 listname=mlist.fqdn_listname,
219 admindb_url=mlist.script_url('admindb'),
221 # This message should appear to come from the <list>-owner so as
222 # to avoid any useless bounce processing.
223 msg = UserNotification(
224 mlist.owner_address, mlist.owner_address,
225 subject, text, mlist.preferred_language)
226 msg.send(mlist, tomoderators=True)
227 return request_id
231 def handle_subscription(mlist, id, action, comment=None):
232 requestdb = IListRequests(mlist)
233 if action is Action.defer:
234 # Nothing to do.
235 return
236 elif action is Action.discard:
237 # Nothing to do except delete the request from the database.
238 pass
239 elif action is Action.reject:
240 key, data = requestdb.get_request(id)
241 send_rejection(
242 mlist, _('Subscription request'),
243 data['email'],
244 comment or _('[No reason given]'),
245 lang=getUtility(ILanguageManager)[data['language']])
246 elif action is Action.accept:
247 key, data = requestdb.get_request(id)
248 delivery_mode = DeliveryMode[data['delivery_mode']]
249 email = data['email']
250 display_name = data['display_name']
251 language = getUtility(ILanguageManager)[data['language']]
252 try:
253 add_member(
254 mlist,
255 RequestRecord(email, display_name, delivery_mode, language))
256 except AlreadySubscribedError:
257 # The address got subscribed in some other way after the original
258 # request was made and accepted.
259 pass
260 else:
261 if mlist.admin_notify_mchanges:
262 send_admin_subscription_notice(
263 mlist, email, display_name, language)
264 slog.info('%s: new %s, %s %s', mlist.fqdn_listname,
265 delivery_mode, formataddr((display_name, email)),
266 'via admin approval')
267 else:
268 raise AssertionError('Unexpected action: {0}'.format(action))
269 # Delete the request from the database.
270 requestdb.delete_request(id)
274 def hold_unsubscription(mlist, email):
275 data = dict(email=email)
276 requestsdb = IListRequests(mlist)
277 request_id = requestsdb.hold_request(
278 RequestType.unsubscription, email, data)
279 vlog.info('%s: held unsubscription request from %s',
280 mlist.fqdn_listname, email)
281 # Possibly notify the administrator of the hold
282 if mlist.admin_immed_notify:
283 subject = _(
284 'New unsubscription request from $mlist.display_name by $email')
285 text = make('unsubauth.txt',
286 mailing_list=mlist,
287 email=email,
288 listname=mlist.fqdn_listname,
289 admindb_url=mlist.script_url('admindb'),
291 # This message should appear to come from the <list>-owner so as
292 # to avoid any useless bounce processing.
293 msg = UserNotification(
294 mlist.owner_address, mlist.owner_address,
295 subject, text, mlist.preferred_language)
296 msg.send(mlist, tomoderators=True)
297 return request_id
301 def handle_unsubscription(mlist, id, action, comment=None):
302 requestdb = IListRequests(mlist)
303 key, data = requestdb.get_request(id)
304 email = data['email']
305 if action is Action.defer:
306 # Nothing to do.
307 return
308 elif action is Action.discard:
309 # Nothing to do except delete the request from the database.
310 pass
311 elif action is Action.reject:
312 key, data = requestdb.get_request(id)
313 send_rejection(
314 mlist, _('Unsubscription request'), email,
315 comment or _('[No reason given]'))
316 elif action is Action.accept:
317 key, data = requestdb.get_request(id)
318 try:
319 delete_member(mlist, email)
320 except NotAMemberError:
321 # User has already been unsubscribed.
322 pass
323 slog.info('%s: deleted %s', mlist.fqdn_listname, email)
324 else:
325 raise AssertionError('Unexpected action: {0}'.format(action))
326 # Delete the request from the database.
327 requestdb.delete_request(id)
331 def send_rejection(mlist, request, recip, comment, origmsg=None, lang=None):
332 # As this message is going to the requester, try to set the language to
333 # his/her language choice, if they are a member. Otherwise use the list's
334 # preferred language.
335 display_name = mlist.display_name
336 if lang is None:
337 member = mlist.members.get_member(recip)
338 lang = (mlist.preferred_language
339 if member is None
340 else member.preferred_language)
341 text = make('refuse.txt',
342 mailing_list=mlist,
343 language=lang.code,
344 listname=mlist.fqdn_listname,
345 request=request,
346 reason=comment,
347 adminaddr=mlist.owner_address,
349 with _.using(lang.code):
350 # add in original message, but not wrap/filled
351 if origmsg:
352 text = NL.join(
353 [text,
354 '---------- ' + _('Original Message') + ' ----------',
355 str(origmsg)
357 subject = _('Request to mailing list "$display_name" rejected')
358 msg = UserNotification(recip, mlist.bounces_address, subject, text, lang)
359 msg.send(mlist)
363 def handle_ListDeletingEvent(event):
364 if not isinstance(event, ListDeletingEvent):
365 return
366 # Get the held requests database for the mailing list. Since the mailing
367 # list is about to get deleted, we can delete all associated requests.
368 requestsdb = IListRequests(event.mailing_list)
369 for request in requestsdb.held_requests:
370 requestsdb.delete_request(request.id)