summaryrefslogtreecommitdiff
path: root/src/mailman/runners/command.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/runners/command.py')
-rw-r--r--src/mailman/runners/command.py219
1 files changed, 219 insertions, 0 deletions
diff --git a/src/mailman/runners/command.py b/src/mailman/runners/command.py
new file mode 100644
index 000000000..68aabc784
--- /dev/null
+++ b/src/mailman/runners/command.py
@@ -0,0 +1,219 @@
+# Copyright (C) 1998-2011 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/>.
+
+"""-request robot command queue runner."""
+
+__metaclass__ = type
+__all__ = [
+ 'CommandRunner',
+ 'Results',
+ ]
+
+# See the delivery diagram in IncomingRunner.py. This module handles all
+# email destined for mylist-request, -join, and -leave. It no longer handles
+# bounce messages (i.e. -admin or -bounces), nor does it handle mail to
+# -owner.
+
+import re
+import logging
+
+from StringIO import StringIO
+from email.errors import HeaderParseError
+from email.header import decode_header, make_header
+from email.iterators import typed_subpart_iterator
+from zope.component import getUtility
+from zope.interface import implements
+
+from mailman.config import config
+from mailman.core.i18n import _
+from mailman.core.runner import Runner
+from mailman.email.message import UserNotification
+from mailman.interfaces.command import ContinueProcessing, IEmailResults
+from mailman.interfaces.languages import ILanguageManager
+
+
+NL = '\n'
+log = logging.getLogger('mailman.vette')
+
+
+
+class CommandFinder:
+ """Generate commands from the content of a message."""
+
+ def __init__(self, msg, msgdata, results):
+ self.command_lines = []
+ self.ignored_lines = []
+ self.processed_lines = []
+ # Depending on where the message was destined to, add some implicit
+ # commands. For example, if this was sent to the -join or -leave
+ # addresses, it's the same as if 'join' or 'leave' commands were sent
+ # to the -request address.
+ subaddress = msgdata.get('subaddress')
+ if subaddress == 'join':
+ self.command_lines.append('join')
+ elif subaddress == 'leave':
+ self.command_lines.append('leave')
+ elif subaddress == 'confirm':
+ mo = re.match(config.mta.verp_confirm_regexp, msg.get('to', ''))
+ if mo:
+ self.command_lines.append('confirm ' + mo.group('cookie'))
+ # Extract the subject header and do RFC 2047 decoding.
+ raw_subject = msg.get('subject', '')
+ try:
+ subject = unicode(make_header(decode_header(raw_subject)))
+ # Mail commands must be ASCII.
+ self.command_lines.append(subject.encode('us-ascii'))
+ except (HeaderParseError, UnicodeError, LookupError):
+ # The Subject header was unparseable or not ASCII, so just ignore
+ # it.
+ pass
+ # Find the first text/plain part of the message.
+ part = None
+ for part in typed_subpart_iterator(msg, 'text', 'plain'):
+ break
+ if part is None or part is not msg:
+ # Either there was no text/plain part or we ignored some
+ # non-text/plain parts.
+ print >> results, _('Ignoring non-text/plain MIME parts')
+ if part is None:
+ # There was no text/plain part to be found.
+ return
+ body = part.get_payload(decode=True)
+ # text/plain parts better have string payloads.
+ assert isinstance(body, basestring), 'Non-string decoded payload'
+ lines = body.splitlines()
+ # Use no more lines than specified
+ max_lines = int(config.mailman.email_commands_max_lines)
+ self.command_lines.extend(lines[:max_lines])
+ self.ignored_lines.extend(lines[max_lines:])
+
+ def __iter__(self):
+ """Return each command line, split into commands and arguments.
+
+ :return: 2-tuples where the first element is the command and the
+ second element is a tuple of the arguments.
+ """
+ while self.command_lines:
+ line = self.command_lines.pop(0)
+ self.processed_lines.append(line)
+ parts = line.strip().split()
+ if len(parts) == 0:
+ continue
+ command = parts.pop(0)
+ yield command, tuple(parts)
+
+
+
+class Results:
+ """The email command results."""
+
+ implements(IEmailResults)
+
+ def __init__(self):
+ self._output = StringIO()
+ print >> self._output, _("""\
+The results of your email command are provided below.
+""")
+
+ def write(self, text):
+ self._output.write(text)
+
+ def __unicode__(self):
+ value = self._output.getvalue()
+ assert isinstance(value, unicode), 'Not a unicode: %r' % value
+ return value
+
+
+
+class CommandRunner(Runner):
+ """The email command runner."""
+
+ def _dispose(self, mlist, msg, msgdata):
+ message_id = msg.get('message-id', 'n/a')
+ # The policy here is similar to the Replybot policy. If a message has
+ # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
+ # the command message.
+ precedence = msg.get('precedence', '').lower()
+ ack = msg.get('x-ack', '').lower()
+ if ack <> 'yes' and precedence in ('bulk', 'junk', 'list'):
+ log.info('%s Precedence: %s message discarded by: %s',
+ message_id, precedence, mlist.request_address)
+ return False
+ # Do replybot for commands.
+ replybot = config.handlers['replybot']
+ replybot.process(mlist, msg, msgdata)
+ if mlist.autorespond_requests == 1:
+ # Respond and discard.
+ log.info('%s -request message replied and discard', message_id)
+ return False
+ # Now craft the response and process the command lines.
+ results = Results()
+ # Include just a few key pieces of information from the original: the
+ # sender, date, and message id.
+ print >> results, _('- Original message details:')
+ subject = msg.get('subject', 'n/a')
+ date = msg.get('date', 'n/a')
+ from_ = msg.get('from', 'n/a')
+ print >> results, _(' From: $from_')
+ print >> results, _(' Subject: $subject')
+ print >> results, _(' Date: $date')
+ print >> results, _(' Message-ID: $message_id')
+ print >> results, _('\n- Results:')
+ finder = CommandFinder(msg, msgdata, results)
+ for command_name, arguments in finder:
+ command = config.commands.get(command_name)
+ if command is None:
+ print >> results, _('No such command: $command_name')
+ else:
+ status = command.process(
+ mlist, msg, msgdata, arguments, results)
+ assert status in ContinueProcessing, (
+ 'Invalid status: %s' % status)
+ if status == ContinueProcessing.no:
+ break
+ # All done. Strip blank lines and send the response.
+ lines = filter(None, (line.strip() for line in finder.command_lines))
+ if len(lines) > 0:
+ print >> results, _('\n- Unprocessed:')
+ for line in lines:
+ print >> results, line
+ lines = filter(None, (line.strip() for line in finder.ignored_lines))
+ if len(lines) > 0:
+ print >> results, _('\n- Ignored:')
+ for line in lines:
+ print >> results, line
+ print >> results, _('\n- Done.')
+ # Send a reply, but do not attach the original message. This is a
+ # compromise because the original message is often helpful in tracking
+ # down problems, but it's also a vector for backscatter spam.
+ language = getUtility(ILanguageManager)[msgdata['lang']]
+ reply = UserNotification(msg.sender, mlist.bounces_address,
+ _('The results of your email commands'),
+ lang=language)
+ # Find a charset for the response body. Try ascii first, then
+ # latin-1 and finally falling back to utf-8.
+ reply_body = unicode(results)
+ for charset in ('us-ascii', 'latin-1'):
+ try:
+ reply_body.encode(charset)
+ break
+ except UnicodeError:
+ pass
+ else:
+ charset = 'utf-8'
+ reply.set_payload(reply_body, charset=charset)
+ reply.send(mlist)