summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bzrignore2
-rw-r--r--mailman/Commands/__init__.py0
-rw-r--r--mailman/app/commands.py42
-rw-r--r--mailman/commands/__init__.py (renamed from mailman/Commands/cmd_echo.py)18
-rw-r--r--mailman/commands/cmd_confirm.py (renamed from mailman/Commands/cmd_confirm.py)0
-rw-r--r--mailman/commands/cmd_end.py (renamed from mailman/Commands/cmd_end.py)0
-rw-r--r--mailman/commands/cmd_help.py (renamed from mailman/Commands/cmd_help.py)0
-rw-r--r--mailman/commands/cmd_info.py (renamed from mailman/Commands/cmd_info.py)0
-rw-r--r--mailman/commands/cmd_join.py (renamed from mailman/Commands/cmd_join.py)0
-rw-r--r--mailman/commands/cmd_leave.py (renamed from mailman/Commands/cmd_leave.py)0
-rw-r--r--mailman/commands/cmd_lists.py (renamed from mailman/Commands/cmd_lists.py)0
-rw-r--r--mailman/commands/cmd_password.py (renamed from mailman/Commands/cmd_password.py)0
-rw-r--r--mailman/commands/cmd_remove.py (renamed from mailman/Commands/cmd_remove.py)0
-rw-r--r--mailman/commands/cmd_set.py (renamed from mailman/Commands/cmd_set.py)0
-rw-r--r--mailman/commands/cmd_stop.py (renamed from mailman/Commands/cmd_stop.py)0
-rw-r--r--mailman/commands/cmd_subscribe.py (renamed from mailman/Commands/cmd_subscribe.py)0
-rw-r--r--mailman/commands/cmd_unsubscribe.py (renamed from mailman/Commands/cmd_unsubscribe.py)0
-rw-r--r--mailman/commands/cmd_who.py (renamed from mailman/Commands/cmd_who.py)0
-rw-r--r--mailman/commands/echo.py47
-rw-r--r--mailman/configuration.py3
-rw-r--r--mailman/initialize.py2
-rw-r--r--mailman/interfaces/chain.py2
-rw-r--r--mailman/interfaces/command.py50
-rw-r--r--mailman/queue/command.py277
-rw-r--r--mailman/queue/docs/command.txt61
-rw-r--r--setup.py12
26 files changed, 351 insertions, 165 deletions
diff --git a/.bzrignore b/.bzrignore
index a365285be..d845e73db 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -1,6 +1,7 @@
*.mo
.bzrignore
build/
+bzr-*-linux-i686.egg/
cron/crontab.in
dist/
mailman.egg-info
@@ -9,3 +10,4 @@ setuptools_bzr-*.egg
staging
TAGS
var/
+.shelf
diff --git a/mailman/Commands/__init__.py b/mailman/Commands/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/mailman/Commands/__init__.py
+++ /dev/null
diff --git a/mailman/app/commands.py b/mailman/app/commands.py
new file mode 100644
index 000000000..1f79b8ef7
--- /dev/null
+++ b/mailman/app/commands.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2008 by the Free Software Foundation, Inc.
+#
+# This program 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 2
+# of the License, or (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Initialize the email commands."""
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ ]
+
+
+from mailman.app.plugins import get_plugins
+from mailman.configuration import config
+from mailman.interfaces import IEmailCommand
+
+
+
+def initialize():
+ """Initialize the email commands."""
+ for module in get_plugins('mailman.commands'):
+ for name in module.__all__:
+ command_class = getattr(module, name)
+ if not IEmailCommand.implementedBy(command_class):
+ continue
+ assert command_class.name not in config.commands, (
+ 'Duplicate email command "%s" found in %s' %
+ (command_class.name, module))
+ config.commands[command_class.name] = command_class()
diff --git a/mailman/Commands/cmd_echo.py b/mailman/commands/__init__.py
index 4a85b2070..81035e44a 100644
--- a/mailman/Commands/cmd_echo.py
+++ b/mailman/commands/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2008 by the Free Software Foundation, Inc.
+# Copyright (C) 2008 by the Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -12,15 +12,9 @@
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
-"""
- echo [args]
- Simply echo an acknowledgement. Args are echoed back unchanged.
-"""
-
-SPACE = ' '
-
-def process(res, args):
- res.results.append('echo %s' % SPACE.join(args))
- return 1
+__all__ = [
+ 'echo',
+ ]
diff --git a/mailman/Commands/cmd_confirm.py b/mailman/commands/cmd_confirm.py
index 47d88a704..47d88a704 100644
--- a/mailman/Commands/cmd_confirm.py
+++ b/mailman/commands/cmd_confirm.py
diff --git a/mailman/Commands/cmd_end.py b/mailman/commands/cmd_end.py
index 81cb15a4a..81cb15a4a 100644
--- a/mailman/Commands/cmd_end.py
+++ b/mailman/commands/cmd_end.py
diff --git a/mailman/Commands/cmd_help.py b/mailman/commands/cmd_help.py
index 94951ba02..94951ba02 100644
--- a/mailman/Commands/cmd_help.py
+++ b/mailman/commands/cmd_help.py
diff --git a/mailman/Commands/cmd_info.py b/mailman/commands/cmd_info.py
index 449c73ad4..449c73ad4 100644
--- a/mailman/Commands/cmd_info.py
+++ b/mailman/commands/cmd_info.py
diff --git a/mailman/Commands/cmd_join.py b/mailman/commands/cmd_join.py
index 7a80cd72b..7a80cd72b 100644
--- a/mailman/Commands/cmd_join.py
+++ b/mailman/commands/cmd_join.py
diff --git a/mailman/Commands/cmd_leave.py b/mailman/commands/cmd_leave.py
index 8cb3b60a8..8cb3b60a8 100644
--- a/mailman/Commands/cmd_leave.py
+++ b/mailman/commands/cmd_leave.py
diff --git a/mailman/Commands/cmd_lists.py b/mailman/commands/cmd_lists.py
index 7087ff488..7087ff488 100644
--- a/mailman/Commands/cmd_lists.py
+++ b/mailman/commands/cmd_lists.py
diff --git a/mailman/Commands/cmd_password.py b/mailman/commands/cmd_password.py
index 4e64ad78e..4e64ad78e 100644
--- a/mailman/Commands/cmd_password.py
+++ b/mailman/commands/cmd_password.py
diff --git a/mailman/Commands/cmd_remove.py b/mailman/commands/cmd_remove.py
index c68f28800..c68f28800 100644
--- a/mailman/Commands/cmd_remove.py
+++ b/mailman/commands/cmd_remove.py
diff --git a/mailman/Commands/cmd_set.py b/mailman/commands/cmd_set.py
index 77d5d2ac4..77d5d2ac4 100644
--- a/mailman/Commands/cmd_set.py
+++ b/mailman/commands/cmd_set.py
diff --git a/mailman/Commands/cmd_stop.py b/mailman/commands/cmd_stop.py
index ecbd5bd88..ecbd5bd88 100644
--- a/mailman/Commands/cmd_stop.py
+++ b/mailman/commands/cmd_stop.py
diff --git a/mailman/Commands/cmd_subscribe.py b/mailman/commands/cmd_subscribe.py
index e1f7e6721..e1f7e6721 100644
--- a/mailman/Commands/cmd_subscribe.py
+++ b/mailman/commands/cmd_subscribe.py
diff --git a/mailman/Commands/cmd_unsubscribe.py b/mailman/commands/cmd_unsubscribe.py
index ea2b96f03..ea2b96f03 100644
--- a/mailman/Commands/cmd_unsubscribe.py
+++ b/mailman/commands/cmd_unsubscribe.py
diff --git a/mailman/Commands/cmd_who.py b/mailman/commands/cmd_who.py
index 02d500043..02d500043 100644
--- a/mailman/Commands/cmd_who.py
+++ b/mailman/commands/cmd_who.py
diff --git a/mailman/commands/echo.py b/mailman/commands/echo.py
new file mode 100644
index 000000000..d95e72aa1
--- /dev/null
+++ b/mailman/commands/echo.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2002-2008 by the Free Software Foundation, Inc.
+#
+# This program 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 2 of the License, or (at your option)
+# any later version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+"""The email command 'echo'."""
+
+__metaclass__ = type
+__all__ = [
+ 'Echo',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces import IEmailCommand
+
+
+SPACE = ' '
+
+
+
+class Echo:
+ """The email 'echo' command."""
+ implements(IEmailCommand)
+
+ name = 'echo'
+ argument_description = '[args]'
+ description = _(
+ 'Echo an acknowledgement. Arguments are return unchanged.')
+
+ def process(self, mlist, msg, msgdata, arguments, results):
+ """See `IEmailCommand`."""
+ print >> results, 'echo', SPACE.join(arguments)
+ return True
diff --git a/mailman/configuration.py b/mailman/configuration.py
index 0c2cff174..2f682d114 100644
--- a/mailman/configuration.py
+++ b/mailman/configuration.py
@@ -173,11 +173,12 @@ class Configuration(object):
# Always add and enable the default server language.
code = self.DEFAULT_SERVER_LANGUAGE
self.languages.enable_language(code)
- # Create the registry of rules and chains.
+ # Create various registries.
self.chains = {}
self.rules = {}
self.handlers = {}
self.pipelines = {}
+ self.commands = {}
def add_domain(self, email_host, url_host=None):
"""Add a virtual domain.
diff --git a/mailman/initialize.py b/mailman/initialize.py
index 0c9338128..6c2a5a8f4 100644
--- a/mailman/initialize.py
+++ b/mailman/initialize.py
@@ -68,9 +68,11 @@ def initialize_2(debug=False):
from mailman.app.chains import initialize as initialize_chains
from mailman.app.rules import initialize as initialize_rules
from mailman.app.pipelines import initialize as initialize_pipelines
+ from mailman.app.commands import initialize as initialize_commands
initialize_rules()
initialize_chains()
initialize_pipelines()
+ initialize_commands()
def initialize(config_path=None, propagate_logs=False):
diff --git a/mailman/interfaces/chain.py b/mailman/interfaces/chain.py
index d504d257f..542a19177 100644
--- a/mailman/interfaces/chain.py
+++ b/mailman/interfaces/chain.py
@@ -15,7 +15,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""Interface describing the basics of chains and links."""
+"""Interfaces describing the basics of chains and links."""
from munepy import Enum
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/command.py b/mailman/interfaces/command.py
new file mode 100644
index 000000000..553dcb0e3
--- /dev/null
+++ b/mailman/interfaces/command.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2008 by the Free Software Foundation, Inc.
+#
+# This program 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 2
+# of the License, or (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Interfaces defining email commands."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IEmailResults(Interface):
+ """The email command results object."""
+
+ output = Attribute('An output file object for printing results to.')
+
+
+
+class IEmailCommand(Interface):
+ """An email command."""
+
+ name = Attribute('Command name as seen in a -request email.')
+
+ argument_description = Attribute('Description of command arguments.')
+
+ description = Attribute('Command help.')
+
+ def process(mlist, msg, msgdata, arguments, results):
+ """Process the email command.
+
+ :param mlist: The mailing list target of the command.
+ :param msg: The original message object.
+ :param msgdata: The message metadata.
+ :param arguments: The command arguments tuple.
+ :param results: An IEmailResults object for these commands.
+ :return: True if further processing should be taken of the email
+ commands in this message.
+ """
diff --git a/mailman/queue/command.py b/mailman/queue/command.py
index c95504401..319cab87c 100644
--- a/mailman/queue/command.py
+++ b/mailman/queue/command.py
@@ -16,6 +16,12 @@
"""-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
@@ -25,17 +31,20 @@ import re
import sys
import logging
+from StringIO import StringIO
from email.Errors import HeaderParseError
from email.Header import decode_header, make_header, Header
from email.Iterators import typed_subpart_iterator
from email.MIMEMessage import MIMEMessage
from email.MIMEText import MIMEText
+from zope.interface import implements
from mailman import Message
from mailman import Utils
from mailman.app.replybot import autorespond_to_sender
from mailman.configuration import config
from mailman.i18n import _
+from mailman.interfaces import IEmailResults
from mailman.queue import Runner
NL = '\n'
@@ -44,149 +53,86 @@ log = logging.getLogger('mailman.vette')
-class Results:
- def __init__(self, mlist, msg, msgdata):
- self.mlist = mlist
- self.msg = msg
- self.msgdata = msgdata
- # Only set returnaddr if the response is to go to someone other than
- # the address specified in the From: header (e.g. for the password
- # command).
- self.returnaddr = None
- self.commands = []
- self.results = []
- self.ignored = []
- self.lineno = 0
- self.subjcmdretried = 0
- self.respond = True
- # Extract the subject header and do RFC 2047 decoding. Note that
- # Python 2.1's unicode() builtin doesn't call obj.__unicode__().
- subj = msg.get('subject', '')
+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.
+ if msgdata.get('tojoin'):
+ self.command_lines.append('join')
+ elif msgdata.get('toleave'):
+ self.command_lines.append('leave')
+ elif msgdata.get('toconfirm'):
+ mo = re.match(config.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:
- subj = make_header(decode_header(subj)).__unicode__()
- # TK: Currently we don't allow 8bit or multibyte in mail command.
- subj = subj.encode('us-ascii')
- # Always process the Subject: header first
- self.commands.append(subj)
+ 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):
- # We couldn't parse it so ignore the Subject header
+ # The Subject header was unparseable or not ASCII, so just ignore
+ # it.
pass
- # Find the first text/plain part
+ # 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.
- self.results.append(_('Ignoring non-text/plain MIME parts'))
+ print >> results, _('Ignoring non-text/plain MIME parts')
if part is None:
- # E.g the outer Content-Type: was text/html
+ # 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)
+ # text/plain parts better have string payloads.
+ assert isinstance(body, basestring), 'Non-string decoded payload'
lines = body.splitlines()
# Use no more lines than specified
- self.commands.extend(lines[:config.EMAIL_COMMANDS_MAX_LINES])
- self.ignored.extend(lines[config.EMAIL_COMMANDS_MAX_LINES:])
+ self.command_lines.extend(lines[:config.EMAIL_COMMANDS_MAX_LINES])
+ self.ignored_lines.extend(lines[config.EMAIL_COMMANDS_MAX_LINES:])
- def process(self):
- # Now, process each line until we find an error. The first
- # non-command line found stops processing.
- stop = False
- for line in self.commands:
- if line and line.strip():
- args = line.split()
- cmd = args.pop(0).lower()
- stop = self.do_command(cmd, args)
- self.lineno += 1
- if stop:
- break
+ def __iter__(self):
+ """Return each command line, split into commands and arguments.
- def do_command(self, cmd, args=None):
- if args is None:
- args = ()
- # Try to import a command handler module for this command
- modname = 'mailman.Commands.cmd_' + cmd
- try:
- __import__(modname)
- handler = sys.modules[modname]
- # ValueError can be raised if cmd has dots in it.
- except (ImportError, ValueError):
- # If we're on line zero, it was the Subject: header that didn't
- # contain a command. It's possible there's a Re: prefix (or
- # localized version thereof) on the Subject: line that's messing
- # things up. Pop the prefix off and try again... once.
- #
- # If that still didn't work it isn't enough to stop processing.
- # BAW: should we include a message that the Subject: was ignored?
- if not self.subjcmdretried and args:
- self.subjcmdretried += 1
- cmd = args.pop(0)
- return self.do_command(cmd, args)
- return self.lineno <> 0
- return handler.process(self, args)
+ :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()
+ yield parts[0], tuple(parts[1:])
- def send_response(self):
- # Helper
- def indent(lines):
- return [' ' + line for line in lines]
- # Quick exit for some commands which don't need a response
- if not self.respond:
- return
- resp = [Utils.wrap(_("""\
+
+
+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.
-Attached is your original message.
-"""))]
- if self.results:
- resp.append(_('- Results:'))
- resp.extend(indent(self.results))
- # Ignore empty lines
- unprocessed = [line for line in self.commands[self.lineno:]
- if line and line.strip()]
- if unprocessed:
- resp.append(_('\n- Unprocessed:'))
- resp.extend(indent(unprocessed))
- if not unprocessed and not self.results:
- # The user sent an empty message; return a helpful one.
- resp.append(Utils.wrap(_("""\
-No commands were found in this message.
-To obtain instructions, send a message containing just the word "help".
-""")))
- if self.ignored:
- resp.append(_('\n- Ignored:'))
- resp.extend(indent(self.ignored))
- resp.append(_('\n- Done.\n\n'))
- # Encode any unicode strings into the list charset, so we don't try to
- # join unicode strings and invalid ASCII.
- charset = Utils.GetCharSet(self.msgdata['lang'])
- encoded_resp = []
- for item in resp:
- if isinstance(item, unicode):
- item = item.encode(charset, 'replace')
- encoded_resp.append(item)
- results = MIMEText(NL.join(encoded_resp), _charset=charset)
- # Safety valve for mail loops with misconfigured email 'bots. We
- # don't respond to commands sent with "Precedence: bulk|junk|list"
- # unless they explicitly "X-Ack: yes", but not all mail 'bots are
- # correctly configured, so we max out the number of responses we'll
- # give to an address in a single day.
- #
- # BAW: We wait until now to make this decision since our sender may
- # not be self.msg.get_sender(), but I'm not sure this is right.
- recip = self.returnaddr or self.msg.get_sender()
- if not autorespond_to_sender(self.mlist, recip, self.msgdata['lang']):
- return
- msg = Message.UserNotification(
- recip,
- self.mlist.GetBouncesEmail(),
- _('The results of your email commands'),
- lang=self.msgdata['lang'])
- msg.set_type('multipart/mixed')
- msg.attach(results)
- orig = MIMEMessage(self.msg)
- msg.attach(orig)
- msg.send(self.mlist)
+""")
+
+ 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
@@ -194,37 +140,70 @@ class CommandRunner(Runner):
QDIR = config.CMDQUEUE_DIR
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
- # it to prevent replybot response storms.
+ # 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('Precedence: %s message discarded by: %s',
- precedence, mlist.GetRequestEmail())
+ log.info('%s Precedence: %s message discarded by: %s',
+ message_id, precedence, mlist.request_address)
return False
- # Do replybot for commands
- mlist.Load()
+ # Do replybot for commands.
replybot = config.handlers['replybot']
replybot.process(mlist, msg, msgdata)
if mlist.autorespond_requests == 1:
- log.info('replied and discard')
- # w/discard
+ # Respond and discard.
+ log.info('%s -request message replied and discard', message_id)
return False
- # Now craft the response
- res = Results(mlist, msg, msgdata)
- # This message will have been delivered to one of mylist-request,
- # mylist-join, or mylist-leave, and the message metadata will contain
- # a key to which one was used.
- if msgdata.get('torequest'):
- res.process()
- elif msgdata.get('tojoin'):
- res.do_command('join')
- elif msgdata.get('toleave'):
- res.do_command('leave')
- elif msgdata.get('toconfirm'):
- mo = re.match(config.VERP_CONFIRM_REGEXP, msg.get('to', ''))
- if mo:
- res.do_command('confirm', (mo.group('cookie'),))
- res.send_response()
- config.db.commit()
+ # 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:
+ command.process(mlist, msg, msgdata, arguments, results)
+ # All done, send the response.
+ if len(finder.command_lines) > 0:
+ print >> results, _('\n- Unprocessed:')
+ for line in finder.command_lines:
+ print >> results, line
+ if len(finder.ignored_lines) > 0:
+ print >> results, _('\n- Ignored:')
+ for line in finder.ignored_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.
+ reply = Message.UserNotification(
+ msg.get_sender(), mlist.bounces_address,
+ _('The results of your email commands'),
+ lang=msgdata['lang'])
+ # 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)
diff --git a/mailman/queue/docs/command.txt b/mailman/queue/docs/command.txt
new file mode 100644
index 000000000..5168dda07
--- /dev/null
+++ b/mailman/queue/docs/command.txt
@@ -0,0 +1,61 @@
+The command queue runner
+========================
+
+This queue runner's purpose is to process and respond to email commands.
+Commands are extensible using the Mailman plugin system, but Mailman comes
+with a number of email commands out of the box. These are processed when a
+message is sent to the list's -request address.
+
+ >>> from mailman.app.lifecycle import create_list
+ >>> mlist = create_list(u'test@example.com')
+
+For example, the 'echo' command simply echoes the original command back to the
+sender. The command can be in the Subject header.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: test-request@example.com
+ ... Subject: echo hello
+ ... Message-ID: <aardvark>
+ ...
+ ... """)
+
+ >>> from mailman.configuration import config
+ >>> from mailman.inject import inject_message
+ >>> inject_message(mlist, msg, qdir=config.CMDQUEUE_DIR)
+ >>> from mailman.queue.command import CommandRunner
+ >>> from mailman.tests.helpers import make_testable_runner
+ >>> command = make_testable_runner(CommandRunner)
+ >>> command.run()
+
+And now the response is in the virgin queue.
+
+ >>> from mailman.queue import Switchboard
+ >>> virgin_queue = Switchboard(config.VIRGINQUEUE_DIR)
+ >>> len(virgin_queue.files)
+ 1
+ >>> from mailman.tests.helpers import get_queue_messages
+ >>> item = get_queue_messages(virgin_queue)[0]
+ >>> print item.msg.as_string()
+ Subject: The results of your email commands
+ From: test-bounces@example.com
+ To: aperson@example.com
+ ...
+ <BLANKLINE>
+ The results of your email command are provided below.
+ <BLANKLINE>
+ - Original message details:
+ From: aperson@example.com
+ Subject: echo hello
+ Date: ...
+ Message-ID: <aardvark>
+ <BLANKLINE>
+ - Results:
+ echo hello
+ <BLANKLINE>
+ - Done.
+ <BLANKLINE>
+ >>> sorted(item.msgdata.items())
+ [..., ('listname', u'test@example.com'), ...,
+ ('recips', [u'aperson@example.com']),
+ ...]
diff --git a/setup.py b/setup.py
index 38e647259..36f13e716 100644
--- a/setup.py
+++ b/setup.py
@@ -37,6 +37,7 @@ if sys.hexversion < 0x20500f0:
# properly split out.
import os
+import mailman.commands
import mailman.messages
start_dir = os.path.dirname(mailman.messages.__file__)
@@ -57,9 +58,15 @@ for dirpath, dirnames, filenames in os.walk(start_dir):
# XXX The 'bin/' prefix here should be configurable.
template = Template('bin/$script = mailman.bin.$script:main')
scripts = set(
- template.substitute(script=os.path.splitext(script)[0])
+ template.substitute(script=script)
for script in mailman.bin.__all__
- if not script.startswith('_')
+ )
+
+# Default email commands
+template = Template('$command = mailman.commands.$command')
+commands = set(
+ template.substitute(command=command)
+ for command in mailman.commands.__all__
)
@@ -84,6 +91,7 @@ Any other spelling is incorrect.""",
'console_scripts': list(scripts),
# Entry point for plugging in different database backends.
'mailman.archiver' : 'default = mailman.app.archiving:Pipermail',
+ 'mailman.commands' : list(commands),
'mailman.database' : 'stock = mailman.database:StockDatabase',
'mailman.mta' : 'stock = mailman.MTA:Manual',
'mailman.styles' : 'default = mailman.app.styles:DefaultStyle',