Remove the mailman.interface magic. Use the more specific interface imports.
[mailman.git] / mailman / app / moderator.py
blob73a341534ed1256bef1bceea0d6ee129567acf3d
1 # Copyright (C) 2007-2009 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 __metaclass__ = type
21 __all__ = [
22 'handle_message',
23 'handle_subscription',
24 'handle_unsubscription',
25 'hold_message',
26 'hold_subscription',
27 'hold_unsubscription',
30 import logging
32 from datetime import datetime
33 from email.utils import formataddr, formatdate, getaddresses, make_msgid
35 from mailman import Message
36 from mailman import Utils
37 from mailman import i18n
38 from mailman.app.membership import add_member, delete_member
39 from mailman.app.notifications import (
40 send_admin_subscription_notice, send_welcome_message)
41 from mailman.config import config
42 from mailman.core import errors
43 from mailman.interfaces import Action
44 from mailman.interfaces.member import AlreadySubscribedError, DeliveryMode
45 from mailman.interfaces.requests import RequestType
47 _ = i18n._
49 vlog = logging.getLogger('mailman.vette')
50 slog = logging.getLogger('mailman.subscribe')
54 def hold_message(mlist, msg, msgdata=None, reason=None):
55 """Hold a message for moderator approval.
57 The message is added to the mailing list's request database.
59 :param mlist: The mailing list to hold the message on.
60 :param msg: The message to hold.
61 :param msgdata: Optional message metadata to hold. If not given, a new
62 metadata dictionary is created and held with the message.
63 :param reason: Optional string reason why the message is being held. If
64 not given, the empty string is used.
65 :return: An id used to handle the held message later.
66 """
67 if msgdata is None:
68 msgdata = {}
69 else:
70 # Make a copy of msgdata so that subsequent changes won't corrupt the
71 # request database. TBD: remove the `filebase' key since this will
72 # not be relevant when the message is resurrected.
73 msgdata = msgdata.copy()
74 if reason is None:
75 reason = ''
76 # Add the message to the message store. It is required to have a
77 # Message-ID header.
78 message_id = msg.get('message-id')
79 if message_id is None:
80 msg['Message-ID'] = message_id = unicode(make_msgid())
81 assert isinstance(message_id, unicode), (
82 'Message-ID is not a unicode: %s' % message_id)
83 config.db.message_store.add(msg)
84 # Prepare the message metadata with some extra information needed only by
85 # the moderation interface.
86 msgdata['_mod_message_id'] = message_id
87 msgdata['_mod_fqdn_listname'] = mlist.fqdn_listname
88 msgdata['_mod_sender'] = msg.get_sender()
89 msgdata['_mod_subject'] = msg.get('subject', _('(no subject)'))
90 msgdata['_mod_reason'] = reason
91 msgdata['_mod_hold_date'] = datetime.now().isoformat()
92 # Now hold this request. We'll use the message_id as the key.
93 requestsdb = config.db.requests.get_list_requests(mlist)
94 request_id = requestsdb.hold_request(
95 RequestType.held_message, message_id, msgdata)
96 return request_id
100 def handle_message(mlist, id, action,
101 comment=None, preserve=False, forward=None):
102 requestdb = config.db.requests.get_list_requests(mlist)
103 key, msgdata = requestdb.get_request(id)
104 # Handle the action.
105 rejection = None
106 message_id = msgdata['_mod_message_id']
107 sender = msgdata['_mod_sender']
108 subject = msgdata['_mod_subject']
109 if action is Action.defer:
110 # Nothing to do, but preserve the message for later.
111 preserve = True
112 elif action is Action.discard:
113 rejection = 'Discarded'
114 elif action is Action.reject:
115 rejection = 'Refused'
116 member = mlist.members.get_member(sender)
117 if member:
118 language = member.preferred_language
119 else:
120 language = None
121 _refuse(mlist, _('Posting of your message titled "$subject"'),
122 sender, comment or _('[No reason given]'), language)
123 elif action is Action.accept:
124 # Start by getting the message from the message store.
125 msg = config.db.message_store.get_message_by_id(message_id)
126 # Delete moderation-specific entries from the message metadata.
127 for key in msgdata.keys():
128 if key.startswith('_mod_'):
129 del msgdata[key]
130 # Add some metadata to indicate this message has now been approved.
131 msgdata['approved'] = True
132 msgdata['moderator_approved'] = True
133 # Calculate a new filebase for the approved message, otherwise
134 # delivery errors will cause duplicates.
135 if 'filebase' in msgdata:
136 del msgdata['filebase']
137 # Queue the file for delivery by qrunner. Trying to deliver the
138 # message directly here can lead to a huge delay in web turnaround.
139 # Log the moderation and add a header.
140 msg['X-Mailman-Approved-At'] = formatdate(localtime=True)
141 vlog.info('held message approved, message-id: %s',
142 msg.get('message-id', 'n/a'))
143 # Stick the message back in the incoming queue for further
144 # processing.
145 config.switchboards['in'].enqueue(msg, _metadata=msgdata)
146 else:
147 raise AssertionError('Unexpected action: %s' % action)
148 # Forward the message.
149 if forward:
150 # Get a copy of the original message from the message store.
151 msg = config.db.message_store.get_message_by_id(message_id)
152 # It's possible the forwarding address list is a comma separated list
153 # of realname/address pairs.
154 addresses = [addr[1] for addr in getaddresses(forward)]
155 language = mlist.preferred_language
156 if len(addresses) == 1:
157 # If the address getting the forwarded message is a member of
158 # the list, we want the headers of the outer message to be
159 # encoded in their language. Otherwise it'll be the preferred
160 # language of the mailing list. This is better than sending a
161 # separate message per recipient.
162 member = mlist.members.get_member(addresses[0])
163 if member:
164 language = member.preferred_language
165 with i18n.using_language(language):
166 fmsg = Message.UserNotification(
167 addresses, mlist.bounces_address,
168 _('Forward of moderated message'),
169 lang=language)
170 fmsg.set_type('message/rfc822')
171 fmsg.attach(msg)
172 fmsg.send(mlist)
173 # Delete the message from the message store if it is not being preserved.
174 if not preserve:
175 config.db.message_store.delete_message(message_id)
176 requestdb.delete_request(id)
177 # Log the rejection
178 if rejection:
179 note = """%s: %s posting:
180 \tFrom: %s
181 \tSubject: %s"""
182 if comment:
183 note += '\n\tReason: ' + comment
184 vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
188 def hold_subscription(mlist, address, realname, password, mode, language):
189 data = dict(when=datetime.now().isoformat(),
190 address=address,
191 realname=realname,
192 password=password,
193 delivery_mode=str(mode),
194 language=language)
195 # Now hold this request. We'll use the address as the key.
196 requestsdb = config.db.requests.get_list_requests(mlist)
197 request_id = requestsdb.hold_request(
198 RequestType.subscription, address, data)
199 vlog.info('%s: held subscription request from %s',
200 mlist.fqdn_listname, address)
201 # Possibly notify the administrator in default list language
202 if mlist.admin_immed_notify:
203 subject = _(
204 'New subscription request to list $mlist.real_name from $address')
205 text = Utils.maketext(
206 'subauth.txt',
207 {'username' : address,
208 'listname' : mlist.fqdn_listname,
209 'admindb_url': mlist.script_url('admindb'),
210 }, mlist=mlist)
211 # This message should appear to come from the <list>-owner so as
212 # to avoid any useless bounce processing.
213 msg = Message.UserNotification(
214 mlist.owner_address, mlist.owner_address,
215 subject, text, mlist.preferred_language)
216 msg.send(mlist, tomoderators=True)
217 return request_id
221 def handle_subscription(mlist, id, action, comment=None):
222 requestdb = config.db.requests.get_list_requests(mlist)
223 if action is Action.defer:
224 # Nothing to do.
225 return
226 elif action is Action.discard:
227 # Nothing to do except delete the request from the database.
228 pass
229 elif action is Action.reject:
230 key, data = requestdb.get_request(id)
231 _refuse(mlist, _('Subscription request'),
232 data['address'],
233 comment or _('[No reason given]'),
234 lang=data['language'])
235 elif action is Action.accept:
236 key, data = requestdb.get_request(id)
237 enum_value = data['delivery_mode'].split('.')[-1]
238 delivery_mode = DeliveryMode(enum_value)
239 address = data['address']
240 realname = data['realname']
241 language = data['language']
242 password = data['password']
243 try:
244 add_member(mlist, address, realname, password,
245 delivery_mode, language)
246 except AlreadySubscribedError:
247 # The address got subscribed in some other way after the original
248 # request was made and accepted.
249 pass
250 else:
251 if mlist.send_welcome_msg:
252 send_welcome_message(mlist, address, language, delivery_mode)
253 if mlist.admin_notify_mchanges:
254 send_admin_subscription_notice(
255 mlist, address, realname, language)
256 slog.info('%s: new %s, %s %s', mlist.fqdn_listname,
257 delivery_mode, formataddr((realname, address)),
258 'via admin approval')
259 else:
260 raise AssertionError('Unexpected action: %s' % action)
261 # Delete the request from the database.
262 requestdb.delete_request(id)
266 def hold_unsubscription(mlist, address):
267 data = dict(address=address)
268 requestsdb = config.db.requests.get_list_requests(mlist)
269 request_id = requestsdb.hold_request(
270 RequestType.unsubscription, address, data)
271 vlog.info('%s: held unsubscription request from %s',
272 mlist.fqdn_listname, address)
273 # Possibly notify the administrator of the hold
274 if mlist.admin_immed_notify:
275 subject = _(
276 'New unsubscription request from $mlist.real_name by $address')
277 text = Utils.maketext(
278 'unsubauth.txt',
279 {'address' : address,
280 'listname' : mlist.fqdn_listname,
281 'admindb_url': mlist.script_url('admindb'),
282 }, mlist=mlist)
283 # This message should appear to come from the <list>-owner so as
284 # to avoid any useless bounce processing.
285 msg = Message.UserNotification(
286 mlist.owner_address, mlist.owner_address,
287 subject, text, mlist.preferred_language)
288 msg.send(mlist, tomoderators=True)
289 return request_id
293 def handle_unsubscription(mlist, id, action, comment=None):
294 requestdb = config.db.requests.get_list_requests(mlist)
295 key, data = requestdb.get_request(id)
296 address = data['address']
297 if action is Action.defer:
298 # Nothing to do.
299 return
300 elif action is Action.discard:
301 # Nothing to do except delete the request from the database.
302 pass
303 elif action is Action.reject:
304 key, data = requestdb.get_request(id)
305 _refuse(mlist, _('Unsubscription request'), address,
306 comment or _('[No reason given]'))
307 elif action is Action.accept:
308 key, data = requestdb.get_request(id)
309 try:
310 delete_member(mlist, address)
311 except errors.NotAMemberError:
312 # User has already been unsubscribed.
313 pass
314 slog.info('%s: deleted %s', mlist.fqdn_listname, address)
315 else:
316 raise AssertionError('Unexpected action: %s' % action)
317 # Delete the request from the database.
318 requestdb.delete_request(id)
322 def _refuse(mlist, request, recip, comment, origmsg=None, lang=None):
323 # As this message is going to the requester, try to set the language to
324 # his/her language choice, if they are a member. Otherwise use the list's
325 # preferred language.
326 realname = mlist.real_name
327 if lang is None:
328 member = mlist.members.get_member(recip)
329 if member:
330 lang = member.preferred_language
331 text = Utils.maketext(
332 'refuse.txt',
333 {'listname' : mlist.fqdn_listname,
334 'request' : request,
335 'reason' : comment,
336 'adminaddr': mlist.owner_address,
337 }, lang=lang, mlist=mlist)
338 with i18n.using_language(lang):
339 # add in original message, but not wrap/filled
340 if origmsg:
341 text = NL.join(
342 [text,
343 '---------- ' + _('Original Message') + ' ----------',
344 str(origmsg)
346 subject = _('Request to mailing list "$realname" rejected')
347 msg = Message.UserNotification(recip, mlist.bounces_address,
348 subject, text, lang)
349 msg.send(mlist)