diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/commands/cli_withlist.py | 9 | ||||
| -rw-r--r-- | src/mailman/commands/docs/inject.rst | 1 | ||||
| -rw-r--r-- | src/mailman/commands/docs/withlist.rst | 19 | ||||
| -rw-r--r-- | src/mailman/config/mailman.cfg | 5 | ||||
| -rw-r--r-- | src/mailman/config/schema.cfg | 12 | ||||
| -rw-r--r-- | src/mailman/docs/NEWS.rst | 3 | ||||
| -rw-r--r-- | src/mailman/interact.py | 1 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_user.py | 3 | ||||
| -rw-r--r-- | src/mailman/runners/maildir.py | 195 | ||||
| -rw-r--r-- | src/mailman/styles/docs/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/styles/docs/styles.rst (renamed from src/mailman/app/docs/styles.rst) | 105 | ||||
| -rw-r--r-- | src/mailman/styles/tests/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/styles/tests/test_styles.py | 90 | ||||
| -rw-r--r-- | src/mailman/testing/testing.cfg | 3 |
14 files changed, 170 insertions, 276 deletions
diff --git a/src/mailman/commands/cli_withlist.py b/src/mailman/commands/cli_withlist.py index dd1596227..7bf7c4384 100644 --- a/src/mailman/commands/cli_withlist.py +++ b/src/mailman/commands/cli_withlist.py @@ -151,7 +151,6 @@ class Withlist: abort=config.db.abort, config=config, ) - banner = config.shell.banner + '\n' + banner if as_boolean(config.shell.use_ipython): self._start_ipython(overrides, banner) @@ -167,14 +166,14 @@ class Withlist: print _('ipython is not available, set use_ipython to no') def _start_python(self, overrides, banner): - # set the tab completion + # Set the tab completion. try: import readline, rlcompleter readline.parse_and_bind('tab: complete') except ImportError: pass else: - sys.ps1 = config.shell.ps1 + ' ' + sys.ps1 = config.shell.prompt + ' ' interact(upframe=False, banner=banner, overrides=overrides) def _details(self): @@ -240,9 +239,9 @@ and run this from the command line: % bin/mailman withlist -r change mylist@example.com 'My List'""") - + class Shell(Withlist): """An alias for `withlist`.""" - + name = 'shell' diff --git a/src/mailman/commands/docs/inject.rst b/src/mailman/commands/docs/inject.rst index 1c0843ff3..e8fc088c0 100644 --- a/src/mailman/commands/docs/inject.rst +++ b/src/mailman/commands/docs/inject.rst @@ -35,7 +35,6 @@ It's easy to find out which queues are available. digest in lmtp - maildir news out pipeline diff --git a/src/mailman/commands/docs/withlist.rst b/src/mailman/commands/docs/withlist.rst index 7632c726a..f00208490 100644 --- a/src/mailman/commands/docs/withlist.rst +++ b/src/mailman/commands/docs/withlist.rst @@ -119,7 +119,20 @@ You also get an error if no mailing list is named. --run requires a mailing list name -Clean up -======== +IPython +======= - >>> sys.path = old_path +You can use `IPython`_ as the interactive shell by changing certain +configuration variables in the `[shell]` section of your `mailman.cfg` file. +Set `use_ipython` to "yes" to switch to IPython, which must be installed on +your system. + +Other configuration variables in the `[shell]` section can be used to +configure other aspects of the interactive shell. You can change both the +prompt and the banner. + + +.. Clean up + >>> sys.path = old_path + +.. _`IPython`: http://ipython.org/ diff --git a/src/mailman/config/mailman.cfg b/src/mailman/config/mailman.cfg index 22c3a129b..0d37ceed9 100644 --- a/src/mailman/config/mailman.cfg +++ b/src/mailman/config/mailman.cfg @@ -61,11 +61,6 @@ class: mailman.runners.incoming.IncomingRunner [runner.lmtp] class: mailman.runners.lmtp.LMTPRunner -[runner.maildir] -class: mailman.runners.maildir.MaildirRunner -# This is still experimental. -start: no - [runner.news] class: mailman.runners.news.NewsRunner diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg index d0a8e9e20..e662633e6 100644 --- a/src/mailman/config/schema.cfg +++ b/src/mailman/config/schema.cfg @@ -63,14 +63,18 @@ post_hook: layout: dev [shell] +# `bin/mailman shell` (also `withlist`) gives you an interactive prompt that +# you can use to interact with an initialized and configured Mailman system. +# Use --help for more information. This section allows you to configure +# certain aspects of this interactive shell. -# customize the interpreter prompt -ps1: << MM >> +# Customize the interpreter prompt. +prompt: >>> # Banner to show on startup. -banner: welcome to the GNU Mailman shell +banner: Welcome to the GNU Mailman shell -# If IPython is found use it as the shell +# Use IPython as the shell, which must be found on the system. use_ipython: no diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index 7993795a1..a7e4a49ea 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -36,6 +36,7 @@ Architecture `owners_chain`. The default `built-in` chain is renamed to `default-posting-chain` while the `built-in` pipeline is renamed `default-posting-pipeline`. + * The experimental `maildir` runner is removed. Use LMTP. Database -------- @@ -77,6 +78,8 @@ Interfaces Commands -------- + * IPython support in `bin/mailman shell` contributed by Andrea Crotti. + (LP: #949926). * The `mailman.cfg` configuration file will now automatically be detected if it exists in an `etc` directory which is a sibling of argv0. * `bin/mailman shell` is an alias for `withlist`. diff --git a/src/mailman/interact.py b/src/mailman/interact.py index 25dcfbc85..dd6bac9f0 100644 --- a/src/mailman/interact.py +++ b/src/mailman/interact.py @@ -24,6 +24,7 @@ __all__ = [ 'interact', ] + import os import sys import code diff --git a/src/mailman/model/tests/test_user.py b/src/mailman/model/tests/test_user.py index c4c07ed22..2f04b7c3a 100644 --- a/src/mailman/model/tests/test_user.py +++ b/src/mailman/model/tests/test_user.py @@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'TestUser', ] @@ -36,6 +37,8 @@ from mailman.utilities.datetime import now class TestUser(unittest.TestCase): + """Test users.""" + layer = ConfigLayer def setUp(self): diff --git a/src/mailman/runners/maildir.py b/src/mailman/runners/maildir.py deleted file mode 100644 index 2d3a49285..000000000 --- a/src/mailman/runners/maildir.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (C) 2002-2012 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Maildir runner. - -Most MTAs can be configured to deliver messages to a `Maildir'[1]. This -runner will read messages from a maildir's new/ directory and inject them into -Mailman's qfiles/in directory for processing in the normal pipeline. This -delivery mechanism contrasts with mail program delivery, where incoming -messages end up in qfiles/in via the MTA executing the scripts/post script -(and likewise for the other -aliases for each mailing list). - -The advantage to Maildir delivery is that it is more efficient; there's no -need to fork an intervening program just to take the message from the MTA's -standard output, to the qfiles/in directory. - -[1] http://cr.yp.to/proto/maildir.html - -We're going to use the :info flag == 1, experimental status flag for our own -purposes. The :1 can be followed by one of these letters: - -- P means that MaildirRunner's in the process of parsing and enqueuing the - message. If successful, it will delete the file. - -- X means something failed during the parse/enqueue phase. An error message - will be logged to log/error and the file will be renamed <filename>:1,X. - MaildirRunner will never automatically return to this file, but once the - problem is fixed, you can manually move the file back to the new/ directory - and MaildirRunner will attempt to re-process it. At some point we may do - this automatically. - -See the variable USE_MAILDIR in Defaults.py.in for enabling this delivery -mechanism. -""" - -# NOTE: Maildir delivery is experimental in Mailman 2.1, and untested in -# Mailman 3. Instead, use LMTP delivery for Mailman 3. - -import os -import errno -import logging - -from email.parser import Parser -from email.utils import parseaddr - -from mailman.config import config -from mailman.core.runner import Runner -from mailman.core.switchboard import Switchboard -from mailman.message import Message - - -log = logging.getLogger('mailman.error') - -# We only care about the listname and the subq as in listname@ or -# listname-request@ -subqnames = ('admin', 'bounces', 'confirm', 'join', 'leave', - 'owner', 'request', 'subscribe', 'unsubscribe') - - -def getlistq(address): - localpart, domain = address.split('@', 1) - # TK: FIXME I only know configs of Postfix. - if config.POSTFIX_STYLE_VIRTUAL_DOMAINS: - p = localpart.split(config.POSTFIX_VIRTUAL_SEPARATOR, 1) - if len(p) == 2: - localpart, domain = p - l = localpart.split('-') - if l[-1] in subqnames: - listname = '-'.join(l[:-1]) - subq = l[-1] - else: - listname = localpart - subq = None - return listname, subq, domain - - - -class MaildirRunner(Runner): - # This class is much different than most runners because it pulls files - # of a different format than what scripts/post and friends leaves. The - # files this runner reads are just single message files as dropped into - # the directory by the MTA. This runner will read the file, and enqueue - # it in the expected qfiles directory for normal processing. - def __init__(self, slice=None, numslices=1): - # Don't call the base class constructor, but build enough of the - # underlying attributes to use the base class's implementation. - self._stop = 0 - self._dir = os.path.join(config.MAILDIR_DIR, 'new') - self._cur = os.path.join(config.MAILDIR_DIR, 'cur') - self._parser = Parser(Message) - - def _one_iteration(self): - # Refresh this each time through the list. - listnames = list(config.list_manager.names) - # Cruise through all the files currently in the new/ directory - try: - files = os.listdir(self._dir) - except OSError, e: - if e.errno <> errno.ENOENT: - raise - # Nothing's been delivered yet - return 0 - for file in files: - srcname = os.path.join(self._dir, file) - dstname = os.path.join(self._cur, file + ':1,P') - xdstname = os.path.join(self._cur, file + ':1,X') - try: - os.rename(srcname, dstname) - except OSError, e: - if e.errno == errno.ENOENT: - # Some other MaildirRunner beat us to it - continue - log.error('Could not rename maildir file: %s', srcname) - raise - # Now open, read, parse, and enqueue this message - try: - fp = open(dstname) - try: - msg = self._parser.parse(fp) - finally: - fp.close() - # Now we need to figure out which queue of which list this - # message was destined for. See get_verp() in - # mailman.app.bounces for why we do things this way. - vals = [] - for header in ('delivered-to', 'envelope-to', 'apparently-to'): - vals.extend(msg.get_all(header, [])) - for field in vals: - to = parseaddr(field)[1].lower() - if not to: - continue - listname, subq, domain = getlistq(to) - listname = listname + '@' + domain - if listname in listnames: - break - else: - # As far as we can tell, this message isn't destined for - # any list on the system. What to do? - log.error('Message apparently not for any list: %s', - xdstname) - os.rename(dstname, xdstname) - continue - # BAW: blech, hardcoded - msgdata = {'listname': listname} - # -admin is deprecated - if subq in ('bounces', 'admin'): - queue = Switchboard('bounces', config.BOUNCEQUEUE_DIR) - elif subq == 'confirm': - msgdata['toconfirm'] = 1 - queue = Switchboard('command', config.CMDQUEUE_DIR) - elif subq in ('join', 'subscribe'): - msgdata['tojoin'] = 1 - queue = Switchboard('command', config.CMDQUEUE_DIR) - elif subq in ('leave', 'unsubscribe'): - msgdata['toleave'] = 1 - queue = Switchboard('command', config.CMDQUEUE_DIR) - elif subq == 'owner': - msgdata.update({ - 'toowner': True, - 'envsender': config.SITE_OWNER_ADDRESS, - 'pipeline': config.OWNER_PIPELINE, - }) - queue = Switchboard('in', config.INQUEUE_DIR) - elif subq is None: - msgdata['tolist'] = 1 - queue = Switchboard('in', config.INQUEUE_DIR) - elif subq == 'request': - msgdata['torequest'] = 1 - queue = Switchboard('command', config.CMDQUEUE_DIR) - else: - log.error('Unknown sub-queue: %s', subq) - os.rename(dstname, xdstname) - continue - queue.enqueue(msg, msgdata) - os.unlink(dstname) - except Exception, e: - os.rename(dstname, xdstname) - log.error('%s', e) - - def _clean_up(self): - pass diff --git a/src/mailman/styles/docs/__init__.py b/src/mailman/styles/docs/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/styles/docs/__init__.py diff --git a/src/mailman/app/docs/styles.rst b/src/mailman/styles/docs/styles.rst index 63ec999bf..90a02227b 100644 --- a/src/mailman/app/docs/styles.rst +++ b/src/mailman/styles/docs/styles.rst @@ -2,9 +2,10 @@ List styles =========== -List styles are a way to name and apply a canned collection of attribute -settings. Every style has a name, which must be unique within the context of -a specific style manager. There is usually only one global style manager. +List styles are a way to name and apply a template of attribute settings to +new mailing lists. Every style has a name, which must be unique within the +context of a specific style manager. There is usually only one global style +manager. Styles also have a priority, which allows you to specify the order in which multiple styles will be applied. A style has a `match` function which is used @@ -22,26 +23,26 @@ Let's start with a vanilla mailing list and a default style manager. >>> from mailman.styles.manager import StyleManager >>> style_manager = StyleManager() >>> style_manager.populate() - >>> sorted(style.name for style in style_manager.styles) - ['default'] + >>> styles = sorted(style.name for style in style_manager.styles) + >>> len(styles) + 1 + >>> print styles[0] + default The default style ================= -There is a default style which implements the legacy application of list -defaults from previous versions of Mailman. This style only matching a -mailing list when no other styles match, and it has the lowest priority. The -low priority means that it is matched last and if it matches, it is applied -last. +There is a default style which implements a legacy style roughly corresponding +to discussion mailing lists. This style matches when no other styles match, +and it has the lowest priority. The low priority means that it is matched +last and if it matches, it is applied last. >>> default_style = style_manager.get('default') - >>> default_style.name - 'default' + >>> print default_style.name + default >>> default_style.priority 0 - >>> sorted(style.name for style in style_manager.styles) - ['default'] Given a mailing list, you can ask the style manager to find all the styles that match the list. The registered styles will be sorted by decreasing @@ -49,8 +50,11 @@ priority and each style's ``match()`` method will be called in turn. The sorted list of matching styles will be returned -- but not applied -- by the style manager's ``lookup()`` method. - >>> [style.name for style in style_manager.lookup(mlist)] - ['default'] + >>> matched_styles = [style.name for style in style_manager.lookup(mlist)] + >>> len(matched_styles) + 1 + >>> print matched_styles[0] + default Registering styles @@ -60,13 +64,13 @@ New styles must implement the ``IStyle`` interface. >>> from zope.interface import implements >>> from mailman.interfaces.styles import IStyle - >>> class TestStyle(object): + >>> class TestStyle: ... implements(IStyle) ... name = 'test' ... priority = 10 ... def apply(self, mailing_list): ... # Just does something very simple. - ... mailing_list.msg_footer = 'test footer' + ... mailing_list.style_thing = 'thing 1' ... def match(self, mailing_list, styles): ... # Applies to any test list ... if 'test' in mailing_list.fqdn_listname: @@ -76,16 +80,20 @@ You can register a new style with the style manager. >>> style_manager.register(TestStyle()) -And now if you lookup matching styles, you should find only the new test +And now if you look up matching styles, you should find only the new test style. This is because the default style only gets applied when no other styles match the mailing list. - >>> sorted(style.name for style in style_manager.lookup(mlist)) - [u'test'] + >>> matched_styles = sorted( + ... style.name for style in style_manager.lookup(mlist)) + >>> len(matched_styles) + 1 + >>> print matched_styles[0] + test >>> for style in style_manager.lookup(mlist): ... style.apply(mlist) - >>> print mlist.msg_footer - test footer + >>> print mlist.style_thing + thing 1 Style priority @@ -101,16 +109,16 @@ applied last. ... priority = 5 ... # Use the base class's match() method. ... def apply(self, mailing_list): - ... mailing_list.msg_footer = 'another footer' + ... mailing_list.style_thing = 'thing 2' - >>> mlist.msg_footer = '' - >>> mlist.msg_footer - u'' + >>> mlist.style_thing = 'thing 0' + >>> print mlist.style_thing + thing 0 >>> style_manager.register(AnotherTestStyle()) >>> for style in style_manager.lookup(mlist): ... style.apply(mlist) - >>> print mlist.msg_footer - another footer + >>> print mlist.style_thing + thing 2 You can change the priority of a style, and if you reapply the styles, they will take effect in the new priority order. @@ -121,8 +129,8 @@ will take effect in the new priority order. >>> style_2.priority = 10 >>> for style in style_manager.lookup(mlist): ... style.apply(mlist) - >>> print mlist.msg_footer - test footer + >>> print mlist.style_thing + thing 1 Unregistering styles @@ -131,32 +139,9 @@ Unregistering styles You can unregister a style, making it unavailable in the future. >>> style_manager.unregister(style_2) - >>> sorted(style.name for style in style_manager.lookup(mlist)) - [u'test'] - - -Corner cases -============ - -If you register a style with the same name as an already registered style, you -get an exception. - - >>> style_manager.register(TestStyle()) - Traceback (most recent call last): - ... - DuplicateStyleError: test - -If you try to register an object that isn't a style, you get an exception. - - >>> style_manager.register(object()) - Traceback (most recent call last): - ... - DoesNotImplement: An object does not implement interface - <InterfaceClass mailman.interfaces.styles.IStyle> - -If you try to unregister a style that isn't registered, you get an exception. - - >>> style_manager.unregister(style_2) - Traceback (most recent call last): - ... - KeyError: u'another' + >>> matched_styles = sorted( + ... style.name for style in style_manager.lookup(mlist)) + >>> len(matched_styles) + 1 + >>> print matched_styles[0] + test diff --git a/src/mailman/styles/tests/__init__.py b/src/mailman/styles/tests/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/styles/tests/__init__.py diff --git a/src/mailman/styles/tests/test_styles.py b/src/mailman/styles/tests/test_styles.py new file mode 100644 index 000000000..ce8b5064d --- /dev/null +++ b/src/mailman/styles/tests/test_styles.py @@ -0,0 +1,90 @@ +# Copyright (C) 2012 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test styles.""" + +from __future__ import absolute_import, print_function, unicode_literals + +__metaclass__ = type +__all__ = [ + 'TestStyle', + ] + + +import unittest + +from zope.component import getUtility +from zope.interface import implements +from zope.interface.exceptions import DoesNotImplement + +from mailman.interfaces.styles import ( + DuplicateStyleError, IStyle, IStyleManager) +from mailman.testing.layers import ConfigLayer + + + +class DummyStyle: + implements(IStyle) + + name = 'dummy' + priority = 1 + + def apply(self, mlist): + pass + + def match(self, mlist, styles): + styles.append(self) + + + +class TestStyle(unittest.TestCase): + """Test styles.""" + + layer = ConfigLayer + + def setUp(self): + self.manager = getUtility(IStyleManager) + + def test_register_style_again(self): + # Registering a style with the same name as a previous style raises an + # exception. + self.manager.register(DummyStyle()) + try: + self.manager.register(DummyStyle()) + except DuplicateStyleError: + pass + else: + raise AssertionError('DuplicateStyleError exception expected') + + def test_register_a_non_style(self): + # You can't register something that doesn't implement the IStyle + # interface. + try: + self.manager.register(object()) + except DoesNotImplement: + pass + else: + raise AssertionError('DoesNotImplement exception expected') + + def test_unregister_a_non_registered_style(self): + # You cannot unregister a style that hasn't yet been registered. + try: + self.manager.unregister(DummyStyle()) + except KeyError: + pass + else: + raise AssertionError('KeyError expected') diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg index 89d986a7c..526093572 100644 --- a/src/mailman/testing/testing.cfg +++ b/src/mailman/testing/testing.cfg @@ -45,9 +45,6 @@ max_restarts: 1 [runner.lmtp] max_restarts: 1 -[runner.maildir] -max_restarts: 1 - [runner.news] max_restarts: 1 |
