diff options
| -rw-r--r-- | .bzrignore | 2 | ||||
| -rw-r--r-- | mailman/app/commands.py | 42 | ||||
| -rw-r--r-- | mailman/commands/__init__.py (renamed from mailman/Commands/__init__.py) | 0 | ||||
| -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 (renamed from mailman/Commands/echo.py) | 4 | ||||
| -rw-r--r-- | mailman/configuration.py | 3 | ||||
| -rw-r--r-- | mailman/initialize.py | 2 | ||||
| -rw-r--r-- | mailman/queue/command.py | 106 | ||||
| -rw-r--r-- | mailman/queue/docs/command.txt | 61 | ||||
| -rw-r--r-- | setup.py | 5 |
23 files changed, 162 insertions, 63 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/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/__init__.py b/mailman/commands/__init__.py index 81035e44a..81035e44a 100644 --- a/mailman/Commands/__init__.py +++ b/mailman/commands/__init__.py 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 index 3aae16344..d95e72aa1 100644 --- a/mailman/Commands/echo.py +++ b/mailman/commands/echo.py @@ -22,10 +22,10 @@ __all__ = [ ] -from zope.interfaces import implements +from zope.interface import implements from mailman.i18n import _ -from mailman.interface import IEmailCommand +from mailman.interfaces import IEmailCommand SPACE = ' ' 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/queue/command.py b/mailman/queue/command.py index b06a5387b..319cab87c 100644 --- a/mailman/queue/command.py +++ b/mailman/queue/command.py @@ -56,7 +56,7 @@ log = logging.getLogger('mailman.vette') class CommandFinder: """Generate commands from the content of a message.""" - def __init__(self, msg, results): + def __init__(self, msg, msgdata, results): self.command_lines = [] self.ignored_lines = [] self.processed_lines = [] @@ -73,7 +73,7 @@ class CommandFinder: if mo: self.command_lines.append('confirm ' + mo.group('cookie')) # Extract the subject header and do RFC 2047 decoding. - subject = msg.get('subject', '') + raw_subject = msg.get('subject', '') try: subject = unicode(make_header(decode_header(raw_subject))) # Mail commands must be ASCII. @@ -118,21 +118,21 @@ class CommandFinder: class Results: """The email command results.""" - implements(IResults) + implements(IEmailResults) def __init__(self): self._output = StringIO() print >> self._output, _("""\ The results of your email command are provided below. - """) - print >> self._output, _('- Results:') def write(self, text): self._output.write(text) - def __str__(self): - return self._output.getvalue() + def __unicode__(self): + value = self._output.getvalue() + assert isinstance(value, unicode), 'Not a unicode: %r' % value + return value @@ -159,61 +159,51 @@ class CommandRunner(Runner): return False # Now craft the response and process the command lines. results = Results() - finder = CommandFinder(msg, 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(msg, msgdata, arguments, results) + command.process(mlist, msg, msgdata, arguments, results) # All done, send the response. - print >> results, _('\n- Unprocessed:') - # XXX - - - # 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(), + 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=self.msgdata['lang']) - msg.set_type('multipart/mixed') - msg.attach(results) - orig = MIMEMessage(self.msg) - msg.attach(orig) - msg.send(self.mlist) + 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__) @@ -62,10 +63,10 @@ scripts = set( ) # Default email commands -template = Template('$command = mailman.command.$command') +template = Template('$command = mailman.commands.$command') commands = set( template.substitute(command=command) - for command in mailman.Commands.__all__ + for command in mailman.commands.__all__ ) |
