------------------------------------------------------------
revno: 6632
committer: Barry Warsaw <[EMAIL PROTECTED]>
branch nick: 3.0
timestamp: Sat 2008-04-26 01:39:14 -0400
message:
  Update and reorganize the command queue runner so that it works with the
  plugin architecture.  Update the Echo command to the new architecture.  Add a
  doctest of course.
added:
  mailman/app/commands.py
  mailman/interfaces/command.py
  mailman/queue/docs/command.txt
renamed:
  mailman/Commands => mailman/commands
  mailman/Commands/cmd_echo.py => mailman/commands/echo.py
modified:
  .bzrignore
  mailman/commands/__init__.py
  mailman/configuration.py
  mailman/initialize.py
  mailman/interfaces/chain.py
  mailman/queue/command.py
  setup.py
  mailman/commands/echo.py
    ------------------------------------------------------------
    revno: 6631.1.1
    committer: Barry Warsaw <[EMAIL PROTECTED]>
    branch nick: 3.0
    timestamp: Tue 2008-04-22 22:34:45 -0400
    message:
      temporary start on command processor rewrite
    added:
      mailman/interfaces/command.py
    renamed:
      mailman/Commands/cmd_echo.py => mailman/Commands/echo.py
    modified:
      mailman/Commands/__init__.py
      mailman/interfaces/chain.py
      mailman/queue/command.py
      setup.py
      mailman/Commands/echo.py

=== modified file '.bzrignore'
--- a/.bzrignore        2008-01-15 04:24:43 +0000
+++ b/.bzrignore        2008-04-26 05:39:14 +0000
@@ -1,6 +1,7 @@
 *.mo
 .bzrignore
 build/
+bzr-*-linux-i686.egg/
 cron/crontab.in
 dist/
 mailman.egg-info
@@ -9,3 +10,4 @@
 staging
 TAGS
 var/
+.shelf

=== added file 'mailman/app/commands.py'
--- a/mailman/app/commands.py   1970-01-01 00:00:00 +0000
+++ b/mailman/app/commands.py   2008-04-26 05:39:14 +0000
@@ -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()

=== renamed directory 'mailman/Commands' => 'mailman/commands'
=== modified file 'mailman/commands/__init__.py'
--- a/mailman/Commands/__init__.py      2008-02-27 06:26:18 +0000
+++ b/mailman/commands/__init__.py      2008-04-23 02:34:45 +0000
@@ -0,0 +1,20 @@
+# 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.
+
+__all__ = [
+    'echo',
+    ]

=== renamed file 'mailman/Commands/cmd_echo.py' => 'mailman/commands/echo.py'
--- a/mailman/Commands/cmd_echo.py      2008-02-08 04:01:48 +0000
+++ b/mailman/commands/echo.py  2008-04-26 05:39:14 +0000
@@ -1,26 +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.
-
-"""
-    echo [args]
-        Simply echo an acknowledgement.  Args are echoed back unchanged.
-"""
+# 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 = ' '
 
-def process(res, args):
-    res.results.append('echo %s' % SPACE.join(args))
-    return 1
+
+
+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

=== modified file 'mailman/configuration.py'
--- a/mailman/configuration.py  2008-03-31 18:12:04 +0000
+++ b/mailman/configuration.py  2008-04-26 05:39:14 +0000
@@ -173,11 +173,12 @@
         # 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.

=== modified file 'mailman/initialize.py'
--- a/mailman/initialize.py     2008-02-27 06:26:18 +0000
+++ b/mailman/initialize.py     2008-04-26 05:39:14 +0000
@@ -68,9 +68,11 @@
     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):

=== modified file 'mailman/interfaces/chain.py'
--- a/mailman/interfaces/chain.py       2008-02-08 04:01:48 +0000
+++ b/mailman/interfaces/chain.py       2008-04-23 02:34:45 +0000
@@ -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

=== added file 'mailman/interfaces/command.py'
--- a/mailman/interfaces/command.py     1970-01-01 00:00:00 +0000
+++ b/mailman/interfaces/command.py     2008-04-23 02:34:45 +0000
@@ -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.
+        """

=== modified file 'mailman/queue/command.py'
--- a/mailman/queue/command.py  2008-02-27 06:26:18 +0000
+++ b/mailman/queue/command.py  2008-04-26 05:39:14 +0000
@@ -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 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 @@
 
 
 
-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:])
-
-    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 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)
-
-    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(_("""\
+        self.command_lines.extend(lines[:config.EMAIL_COMMANDS_MAX_LINES])
+        self.ignored_lines.extend(lines[config.EMAIL_COMMANDS_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()
+            yield parts[0], tuple(parts[1:])
+
+
+
+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 @@
     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)

=== added file 'mailman/queue/docs/command.txt'
--- a/mailman/queue/docs/command.txt    1970-01-01 00:00:00 +0000
+++ b/mailman/queue/docs/command.txt    2008-04-26 05:39:14 +0000
@@ -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'[EMAIL PROTECTED]')
+
+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: [EMAIL PROTECTED]
+    ... To: [EMAIL PROTECTED]
+    ... 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: [EMAIL PROTECTED]
+    To: [EMAIL PROTECTED]
+    ...
+    <BLANKLINE>
+    The results of your email command are provided below.
+    <BLANKLINE>
+    - Original message details:
+        From: [EMAIL PROTECTED]
+        Subject: echo hello
+        Date: ...
+        Message-ID: <aardvark>
+    <BLANKLINE>
+    - Results:
+    echo hello
+    <BLANKLINE>
+    - Done.
+    <BLANKLINE>
+    >>> sorted(item.msgdata.items())
+    [..., ('listname', u'[EMAIL PROTECTED]'), ...,
+     ('recips', [u'[EMAIL PROTECTED]']),
+     ...]

=== modified file 'setup.py'
--- a/setup.py  2008-04-09 02:16:12 +0000
+++ b/setup.py  2008-04-26 05:39:14 +0000
@@ -37,6 +37,7 @@
 # properly split out.
 
 import os
+import mailman.commands
 import mailman.messages
 
 start_dir = os.path.dirname(mailman.messages.__file__)
@@ -57,9 +58,15 @@
 # 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 @@
         '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',



--
Primary development focus
https://code.launchpad.net/~mailman-coders/mailman/3.0

You are receiving this branch notification because you are subscribed to it.
_______________________________________________
Mailman-checkins mailing list
[email protected]
Unsubscribe: 
http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to