diff options
| -rw-r--r-- | .bzrignore | 2 | ||||
| -rw-r--r-- | mailman/Commands/__init__.py | 0 | ||||
| -rw-r--r-- | mailman/app/commands.py | 42 | ||||
| -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.py | 47 | ||||
| -rw-r--r-- | mailman/configuration.py | 3 | ||||
| -rw-r--r-- | mailman/initialize.py | 2 | ||||
| -rw-r--r-- | mailman/interfaces/chain.py | 2 | ||||
| -rw-r--r-- | mailman/interfaces/command.py | 50 | ||||
| -rw-r--r-- | mailman/queue/command.py | 277 | ||||
| -rw-r--r-- | mailman/queue/docs/command.txt | 61 | ||||
| -rw-r--r-- | setup.py | 12 |
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']), + ...] @@ -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', |
