diff options
| author | Barry Warsaw | 2009-01-25 13:01:41 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2009-01-25 13:01:41 -0500 |
| commit | eefd06f1b88b8ecbb23a9013cd223b72ca85c20d (patch) | |
| tree | 72c947fe16fce0e07e996ee74020b26585d7e846 /src/mailman/app/moderator.py | |
| parent | 07871212f74498abd56bef3919bf3e029eb8b930 (diff) | |
| download | mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.tar.gz mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.tar.zst mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.zip | |
Diffstat (limited to 'src/mailman/app/moderator.py')
| -rw-r--r-- | src/mailman/app/moderator.py | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py new file mode 100644 index 000000000..b40a34344 --- /dev/null +++ b/src/mailman/app/moderator.py @@ -0,0 +1,351 @@ +# Copyright (C) 2007-2009 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Application support for moderators.""" + +from __future__ import unicode_literals + +__metaclass__ = type +__all__ = [ + 'handle_message', + 'handle_subscription', + 'handle_unsubscription', + 'hold_message', + 'hold_subscription', + 'hold_unsubscription', + ] + +import logging + +from datetime import datetime +from email.utils import formataddr, formatdate, getaddresses, make_msgid + +from mailman import Message +from mailman import Utils +from mailman import i18n +from mailman.app.membership import add_member, delete_member +from mailman.app.notifications import ( + send_admin_subscription_notice, send_welcome_message) +from mailman.config import config +from mailman.core import errors +from mailman.interfaces import Action +from mailman.interfaces.member import AlreadySubscribedError, DeliveryMode +from mailman.interfaces.requests import RequestType + +_ = i18n._ + +vlog = logging.getLogger('mailman.vette') +slog = logging.getLogger('mailman.subscribe') + + + +def hold_message(mlist, msg, msgdata=None, reason=None): + """Hold a message for moderator approval. + + The message is added to the mailing list's request database. + + :param mlist: The mailing list to hold the message on. + :param msg: The message to hold. + :param msgdata: Optional message metadata to hold. If not given, a new + metadata dictionary is created and held with the message. + :param reason: Optional string reason why the message is being held. If + not given, the empty string is used. + :return: An id used to handle the held message later. + """ + if msgdata is None: + msgdata = {} + else: + # Make a copy of msgdata so that subsequent changes won't corrupt the + # request database. TBD: remove the `filebase' key since this will + # not be relevant when the message is resurrected. + msgdata = msgdata.copy() + if reason is None: + reason = '' + # Add the message to the message store. It is required to have a + # Message-ID header. + message_id = msg.get('message-id') + if message_id is None: + msg['Message-ID'] = message_id = unicode(make_msgid()) + assert isinstance(message_id, unicode), ( + 'Message-ID is not a unicode: %s' % message_id) + config.db.message_store.add(msg) + # Prepare the message metadata with some extra information needed only by + # the moderation interface. + msgdata['_mod_message_id'] = message_id + msgdata['_mod_fqdn_listname'] = mlist.fqdn_listname + msgdata['_mod_sender'] = msg.get_sender() + msgdata['_mod_subject'] = msg.get('subject', _('(no subject)')) + msgdata['_mod_reason'] = reason + msgdata['_mod_hold_date'] = datetime.now().isoformat() + # Now hold this request. We'll use the message_id as the key. + requestsdb = config.db.requests.get_list_requests(mlist) + request_id = requestsdb.hold_request( + RequestType.held_message, message_id, msgdata) + return request_id + + + +def handle_message(mlist, id, action, + comment=None, preserve=False, forward=None): + requestdb = config.db.requests.get_list_requests(mlist) + key, msgdata = requestdb.get_request(id) + # Handle the action. + rejection = None + message_id = msgdata['_mod_message_id'] + sender = msgdata['_mod_sender'] + subject = msgdata['_mod_subject'] + if action is Action.defer: + # Nothing to do, but preserve the message for later. + preserve = True + elif action is Action.discard: + rejection = 'Discarded' + elif action is Action.reject: + rejection = 'Refused' + member = mlist.members.get_member(sender) + if member: + language = member.preferred_language + else: + language = None + _refuse(mlist, _('Posting of your message titled "$subject"'), + sender, comment or _('[No reason given]'), language) + elif action is Action.accept: + # Start by getting the message from the message store. + msg = config.db.message_store.get_message_by_id(message_id) + # Delete moderation-specific entries from the message metadata. + for key in msgdata.keys(): + if key.startswith('_mod_'): + del msgdata[key] + # Add some metadata to indicate this message has now been approved. + msgdata['approved'] = True + msgdata['moderator_approved'] = True + # Calculate a new filebase for the approved message, otherwise + # delivery errors will cause duplicates. + if 'filebase' in msgdata: + del msgdata['filebase'] + # Queue the file for delivery by qrunner. Trying to deliver the + # message directly here can lead to a huge delay in web turnaround. + # Log the moderation and add a header. + msg['X-Mailman-Approved-At'] = formatdate(localtime=True) + vlog.info('held message approved, message-id: %s', + msg.get('message-id', 'n/a')) + # Stick the message back in the incoming queue for further + # processing. + config.switchboards['in'].enqueue(msg, _metadata=msgdata) + else: + raise AssertionError('Unexpected action: {0}'.format(action)) + # Forward the message. + if forward: + # Get a copy of the original message from the message store. + msg = config.db.message_store.get_message_by_id(message_id) + # It's possible the forwarding address list is a comma separated list + # of realname/address pairs. + addresses = [addr[1] for addr in getaddresses(forward)] + language = mlist.preferred_language + if len(addresses) == 1: + # If the address getting the forwarded message is a member of + # the list, we want the headers of the outer message to be + # encoded in their language. Otherwise it'll be the preferred + # language of the mailing list. This is better than sending a + # separate message per recipient. + member = mlist.members.get_member(addresses[0]) + if member: + language = member.preferred_language + with i18n.using_language(language): + fmsg = Message.UserNotification( + addresses, mlist.bounces_address, + _('Forward of moderated message'), + lang=language) + fmsg.set_type('message/rfc822') + fmsg.attach(msg) + fmsg.send(mlist) + # Delete the message from the message store if it is not being preserved. + if not preserve: + config.db.message_store.delete_message(message_id) + requestdb.delete_request(id) + # Log the rejection + if rejection: + note = """%s: %s posting: +\tFrom: %s +\tSubject: %s""" + if comment: + note += '\n\tReason: ' + comment + vlog.info(note, mlist.fqdn_listname, rejection, sender, subject) + + + +def hold_subscription(mlist, address, realname, password, mode, language): + data = dict(when=datetime.now().isoformat(), + address=address, + realname=realname, + password=password, + delivery_mode=str(mode), + language=language) + # Now hold this request. We'll use the address as the key. + requestsdb = config.db.requests.get_list_requests(mlist) + request_id = requestsdb.hold_request( + RequestType.subscription, address, data) + vlog.info('%s: held subscription request from %s', + mlist.fqdn_listname, address) + # Possibly notify the administrator in default list language + if mlist.admin_immed_notify: + subject = _( + 'New subscription request to list $mlist.real_name from $address') + text = Utils.maketext( + 'subauth.txt', + {'username' : address, + 'listname' : mlist.fqdn_listname, + 'admindb_url': mlist.script_url('admindb'), + }, mlist=mlist) + # This message should appear to come from the <list>-owner so as + # to avoid any useless bounce processing. + msg = Message.UserNotification( + mlist.owner_address, mlist.owner_address, + subject, text, mlist.preferred_language) + msg.send(mlist, tomoderators=True) + return request_id + + + +def handle_subscription(mlist, id, action, comment=None): + requestdb = config.db.requests.get_list_requests(mlist) + if action is Action.defer: + # Nothing to do. + return + elif action is Action.discard: + # Nothing to do except delete the request from the database. + pass + elif action is Action.reject: + key, data = requestdb.get_request(id) + _refuse(mlist, _('Subscription request'), + data['address'], + comment or _('[No reason given]'), + lang=data['language']) + elif action is Action.accept: + key, data = requestdb.get_request(id) + enum_value = data['delivery_mode'].split('.')[-1] + delivery_mode = DeliveryMode(enum_value) + address = data['address'] + realname = data['realname'] + language = data['language'] + password = data['password'] + try: + add_member(mlist, address, realname, password, + delivery_mode, language) + except AlreadySubscribedError: + # The address got subscribed in some other way after the original + # request was made and accepted. + pass + else: + if mlist.send_welcome_msg: + send_welcome_message(mlist, address, language, delivery_mode) + if mlist.admin_notify_mchanges: + send_admin_subscription_notice( + mlist, address, realname, language) + slog.info('%s: new %s, %s %s', mlist.fqdn_listname, + delivery_mode, formataddr((realname, address)), + 'via admin approval') + else: + raise AssertionError('Unexpected action: {0}'.format(action)) + # Delete the request from the database. + requestdb.delete_request(id) + + + +def hold_unsubscription(mlist, address): + data = dict(address=address) + requestsdb = config.db.requests.get_list_requests(mlist) + request_id = requestsdb.hold_request( + RequestType.unsubscription, address, data) + vlog.info('%s: held unsubscription request from %s', + mlist.fqdn_listname, address) + # Possibly notify the administrator of the hold + if mlist.admin_immed_notify: + subject = _( + 'New unsubscription request from $mlist.real_name by $address') + text = Utils.maketext( + 'unsubauth.txt', + {'address' : address, + 'listname' : mlist.fqdn_listname, + 'admindb_url': mlist.script_url('admindb'), + }, mlist=mlist) + # This message should appear to come from the <list>-owner so as + # to avoid any useless bounce processing. + msg = Message.UserNotification( + mlist.owner_address, mlist.owner_address, + subject, text, mlist.preferred_language) + msg.send(mlist, tomoderators=True) + return request_id + + + +def handle_unsubscription(mlist, id, action, comment=None): + requestdb = config.db.requests.get_list_requests(mlist) + key, data = requestdb.get_request(id) + address = data['address'] + if action is Action.defer: + # Nothing to do. + return + elif action is Action.discard: + # Nothing to do except delete the request from the database. + pass + elif action is Action.reject: + key, data = requestdb.get_request(id) + _refuse(mlist, _('Unsubscription request'), address, + comment or _('[No reason given]')) + elif action is Action.accept: + key, data = requestdb.get_request(id) + try: + delete_member(mlist, address) + except errors.NotAMemberError: + # User has already been unsubscribed. + pass + slog.info('%s: deleted %s', mlist.fqdn_listname, address) + else: + raise AssertionError('Unexpected action: {0}'.format(action)) + # Delete the request from the database. + requestdb.delete_request(id) + + + +def _refuse(mlist, request, recip, comment, origmsg=None, lang=None): + # As this message is going to the requester, try to set the language to + # his/her language choice, if they are a member. Otherwise use the list's + # preferred language. + realname = mlist.real_name + if lang is None: + member = mlist.members.get_member(recip) + if member: + lang = member.preferred_language + text = Utils.maketext( + 'refuse.txt', + {'listname' : mlist.fqdn_listname, + 'request' : request, + 'reason' : comment, + 'adminaddr': mlist.owner_address, + }, lang=lang, mlist=mlist) + with i18n.using_language(lang): + # add in original message, but not wrap/filled + if origmsg: + text = NL.join( + [text, + '---------- ' + _('Original Message') + ' ----------', + str(origmsg) + ]) + subject = _('Request to mailing list "$realname" rejected') + msg = Message.UserNotification(recip, mlist.bounces_address, + subject, text, lang) + msg.send(mlist) |
