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)
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
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."""
23 'handle_subscription',
24 'handle_unsubscription',
27 'hold_unsubscription',
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
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.
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()
76 # Add the message to the message store. It is required to have a
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
)
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)
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.
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
)
118 language
= member
.preferred_language
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_'):
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
145 config
.switchboards
['in'].enqueue(msg
, _metadata
=msgdata
)
147 raise AssertionError('Unexpected action: %s' % action
)
148 # Forward the message.
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])
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'),
170 fmsg
.set_type('message/rfc822')
173 # Delete the message from the message store if it is not being preserved.
175 config
.db
.message_store
.delete_message(message_id
)
176 requestdb
.delete_request(id)
179 note
= """%s: %s posting:
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(),
193 delivery_mode
=str(mode
),
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
:
204 'New subscription request to list $mlist.real_name from $address')
205 text
= Utils
.maketext(
207 {'username' : address
,
208 'listname' : mlist
.fqdn_listname
,
209 'admindb_url': mlist
.script_url('admindb'),
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)
221 def handle_subscription(mlist
, id, action
, comment
=None):
222 requestdb
= config
.db
.requests
.get_list_requests(mlist
)
223 if action
is Action
.defer
:
226 elif action
is Action
.discard
:
227 # Nothing to do except delete the request from the database.
229 elif action
is Action
.reject
:
230 key
, data
= requestdb
.get_request(id)
231 _refuse(mlist
, _('Subscription request'),
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']
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.
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')
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
:
276 'New unsubscription request from $mlist.real_name by $address')
277 text
= Utils
.maketext(
279 {'address' : address
,
280 'listname' : mlist
.fqdn_listname
,
281 'admindb_url': mlist
.script_url('admindb'),
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)
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
:
300 elif action
is Action
.discard
:
301 # Nothing to do except delete the request from the database.
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)
310 delete_member(mlist
, address
)
311 except errors
.NotAMemberError
:
312 # User has already been unsubscribed.
314 slog
.info('%s: deleted %s', mlist
.fqdn_listname
, address
)
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
328 member
= mlist
.members
.get_member(recip
)
330 lang
= member
.preferred_language
331 text
= Utils
.maketext(
333 {'listname' : mlist
.fqdn_listname
,
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
343 '---------- ' + _('Original Message') + ' ----------',
346 subject
= _('Request to mailing list "$realname" rejected')
347 msg
= Message
.UserNotification(recip
, mlist
.bounces_address
,