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)
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."""
21 'handle_ListDeletingEvent',
23 'handle_subscription',
24 'handle_unsubscription',
27 'hold_unsubscription',
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
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.
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()
83 # Add the message to the message store. It is required to have a
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
)
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)
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.
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
)
126 language
= member
.preferred_language
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_'):
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
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
155 config
.switchboards
['pipeline'].enqueue(msg
, _metadata
=msgdata
)
157 raise AssertionError('Unexpected action: {0}'.format(action
))
158 # Forward the message.
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])
174 language
= member
.preferred_language
175 with _
.using(language
.code
):
176 fmsg
= UserNotification(
177 addresses
, mlist
.bounces_address
,
178 _('Forward of moderated message'),
180 fmsg
.set_type('message/rfc822')
183 # Delete the message from the message store if it is not being preserved.
185 message_store
.delete_message(message_id
)
186 requestdb
.delete_request(id)
189 note
= """%s: %s posting:
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(),
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
214 'New subscription request to $mlist.display_name from $email')
215 text
= make('subauth.txt',
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)
231 def handle_subscription(mlist
, id, action
, comment
=None):
232 requestdb
= IListRequests(mlist
)
233 if action
is Action
.defer
:
236 elif action
is Action
.discard
:
237 # Nothing to do except delete the request from the database.
239 elif action
is Action
.reject
:
240 key
, data
= requestdb
.get_request(id)
242 mlist
, _('Subscription request'),
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']]
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.
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')
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
:
284 'New unsubscription request from $mlist.display_name by $email')
285 text
= make('unsubauth.txt',
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)
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
:
308 elif action
is Action
.discard
:
309 # Nothing to do except delete the request from the database.
311 elif action
is Action
.reject
:
312 key
, data
= requestdb
.get_request(id)
314 mlist
, _('Unsubscription request'), email
,
315 comment
or _('[No reason given]'))
316 elif action
is Action
.accept
:
317 key
, data
= requestdb
.get_request(id)
319 delete_member(mlist
, email
)
320 except NotAMemberError
:
321 # User has already been unsubscribed.
323 slog
.info('%s: deleted %s', mlist
.fqdn_listname
, email
)
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
337 member
= mlist
.members
.get_member(recip
)
338 lang
= (mlist
.preferred_language
340 else member
.preferred_language
)
341 text
= make('refuse.txt',
344 listname
=mlist
.fqdn_listname
,
347 adminaddr
=mlist
.owner_address
,
349 with _
.using(lang
.code
):
350 # add in original message, but not wrap/filled
354 '---------- ' + _('Original Message') + ' ----------',
357 subject
= _('Request to mailing list "$display_name" rejected')
358 msg
= UserNotification(recip
, mlist
.bounces_address
, subject
, text
, lang
)
363 def handle_ListDeletingEvent(event
):
364 if not isinstance(event
, ListDeletingEvent
):
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)