summaryrefslogtreecommitdiff
path: root/src/mailman/app/moderator.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/app/moderator.py')
-rw-r--r--src/mailman/app/moderator.py351
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)