diff options
Diffstat (limited to 'src')
48 files changed, 813 insertions, 270 deletions
diff --git a/src/mailman/Archiver/HyperArch.py b/src/mailman/Archiver/HyperArch.py index 11b28ae48..e851085b7 100644 --- a/src/mailman/Archiver/HyperArch.py +++ b/src/mailman/Archiver/HyperArch.py @@ -34,14 +34,13 @@ import time import errno import urllib import logging -import weakref import binascii from email.Charset import Charset from email.Errors import HeaderParseError from email.Header import decode_header, make_header +from flufl.lock import Lock, TimeOutError from lazr.config import as_boolean -from locknix.lockfile import Lock from string import Template from zope.component import getUtility @@ -751,7 +750,7 @@ class HyperArchive(pipermail.T): self.maillist.fqdn_listname + '-arch.lock')) try: self._lock_file.lock(timeout=0.5) - except lockfile.TimeOutError: + except TimeOutError: return 0 return 1 diff --git a/src/mailman/Archiver/HyperDatabase.py b/src/mailman/Archiver/HyperDatabase.py index f1884f019..2092ef507 100644 --- a/src/mailman/Archiver/HyperDatabase.py +++ b/src/mailman/Archiver/HyperDatabase.py @@ -27,7 +27,7 @@ import errno # package/project modules # import pipermail -from locknix import lockfile +from flufl.lock import Lock, NotLockedError CACHESIZE = pipermail.CACHESIZE @@ -58,7 +58,7 @@ class DumbBTree: def __init__(self, path): self.current_index = 0 self.path = path - self.lockfile = lockfile.Lock(self.path + ".lock") + self.lockfile = Lock(self.path + ".lock") self.lock() self.__dirty = 0 self.dict = {} @@ -80,7 +80,7 @@ class DumbBTree: def unlock(self): try: self.lockfile.unlock() - except lockfile.NotLockedError: + except NotLockedError: pass def __delitem__(self, item): diff --git a/src/mailman/app/docs/chains.txt b/src/mailman/app/docs/chains.txt index 18cd61443..3242f684e 100644 --- a/src/mailman/app/docs/chains.txt +++ b/src/mailman/app/docs/chains.txt @@ -317,9 +317,8 @@ all default rules. This message will end up in the `pipeline` queue. Message-ID: <first> X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW X-Mailman-Rule-Misses: approved; emergency; loop; administrivia; - implicit-dest; - max-recipients; max-size; news-moderation; no-subject; - suspicious-header + implicit-dest; max-recipients; max-size; news-moderation; no-subject; + suspicious-header; member-moderation <BLANKLINE> An important message. <BLANKLINE> @@ -338,6 +337,7 @@ hit and all rules that have missed. loop max-recipients max-size + member-moderation news-moderation no-subject suspicious-header diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py index 5682d17b2..187660728 100644 --- a/src/mailman/app/membership.py +++ b/src/mailman/app/membership.py @@ -115,8 +115,6 @@ def add_member(mlist, address, realname, password, delivery_mode, language): member = address_obj.subscribe(mlist, MemberRole.member) member.preferences.preferred_language = language member.preferences.delivery_mode = delivery_mode -## mlist.setMemberOption(email, config.Moderate, -## mlist.default_member_moderation) return member diff --git a/src/mailman/bin/arch.py b/src/mailman/bin/arch.py index 713af1013..c966f4e09 100644 --- a/src/mailman/bin/arch.py +++ b/src/mailman/bin/arch.py @@ -23,7 +23,7 @@ import errno import shutil import optparse -from locknix.lockfile import Lock +from flufl.lock import Lock from mailman.Archiver.HyperArch import HyperArchive from mailman.Defaults import hours diff --git a/src/mailman/bin/gate_news.py b/src/mailman/bin/gate_news.py index c10248c53..bb293a75f 100644 --- a/src/mailman/bin/gate_news.py +++ b/src/mailman/bin/gate_news.py @@ -25,7 +25,7 @@ import optparse import email.Errors from email.Parser import Parser -from locknix import lockfile +from flufl.lock import Lock, TimeOutError from mailman import MailList from mailman import Message @@ -209,7 +209,7 @@ def process_lists(glock): # loop over range, and this will not include the last # element in the list. poll_newsgroup(mlist, conn, start, last + 1, glock) - except lockfile.TimeOutError: + except TimeOutError: log.error('Could not acquire list lock: %s', listname) finally: if mlist.Locked(): @@ -230,12 +230,12 @@ def main(): log = logging.getLogger('mailman.fromusenet') try: - with lockfile.Lock(GATENEWS_LOCK_FILE, - # It's okay to hijack this - lifetime=LOCK_LIFETIME) as lock: + with Lock(GATENEWS_LOCK_FILE, + # It's okay to hijack this + lifetime=LOCK_LIFETIME) as lock: process_lists(lock) clearcache() - except lockfile.TimeOutError: + except TimeOutError: log.error('Could not acquire gate_news lock') diff --git a/src/mailman/bin/master.py b/src/mailman/bin/master.py index 0d0276fdb..2bc155325 100644 --- a/src/mailman/bin/master.py +++ b/src/mailman/bin/master.py @@ -34,8 +34,8 @@ import logging from datetime import timedelta from flufl.enum import Enum +from flufl.lock import Lock, NotLockedError, TimeOutError from lazr.config import as_boolean -from locknix import lockfile from mailman.config import config from mailman.core.i18n import _ @@ -109,26 +109,10 @@ instead of the default set. Multiple -r options may be given. The values for -def get_lock_data(): - """Get information from the master lock file. - - :return: A 3-tuple of the hostname, integer process id, and file name of - the lock file. - """ - with open(config.LOCK_FILE) as fp: - filename = os.path.split(fp.read().strip())[1] - parts = filename.split('.') - # Ignore the timestamp. - parts.pop() - pid = parts.pop() - hostname = parts.pop() - filename = DOT.join(reversed(parts)) - return hostname, int(pid), filename - - -# pylint: disable-msg=W0232 class WatcherState(Enum): """Enum for the state of the master process watcher.""" + # No lock has been acquired by any process. + none = 0 # Another master watcher is running. conflict = 1 # No conflicting process exists. @@ -137,47 +121,60 @@ class WatcherState(Enum): host_mismatch = 3 -def master_state(): +def master_state(lock_file=None): """Get the state of the master watcher. - :return: WatcherState describing the state of the lock file. + :param lock_file: Path to the lock file, otherwise `config.LOCK_FILE`. + :type lock_file: str + :return: 2-tuple of the WatcherState describing the state of the lock + file, and the lock object. """ - # pylint: disable-msg=W0612 - hostname, pid, tempfile = get_lock_data() - if hostname != socket.gethostname(): - return WatcherState.host_mismatch + if lock_file is None: + lock_file = config.LOCK_FILE + # We'll never acquire the lock, so the lifetime doesn't matter. + lock = Lock(lock_file) + try: + hostname, pid, tempfile = lock.details + except NotLockedError: + return WatcherState.none, lock + if hostname != socket.getfqdn(): + return WatcherState.host_mismatch, lock # Find out if the process exists by calling kill with a signal 0. try: os.kill(pid, 0) - return WatcherState.conflict + return WatcherState.conflict, lock except OSError as error: if error.errno == errno.ESRCH: # No matching process id. - return WatcherState.stale_lock + return WatcherState.stale_lock, lock # Some other error occurred. raise -def acquire_lock_1(force): +def acquire_lock_1(force, lock_file=None): """Try to acquire the master queue runner lock. :param force: Flag that controls whether to force acquisition of the lock. + :type force: bool + :param lock_file: Path to the lock file, otherwise `config.LOCK_FILE`. + :type lock_file: str :return: The master queue runner lock. :raises: `TimeOutError` if the lock could not be acquired. """ - lock = lockfile.Lock(config.LOCK_FILE, LOCK_LIFETIME) + if lock_file is None: + lock_file = config.LOCK_FILE + lock = Lock(lock_file, LOCK_LIFETIME) try: lock.lock(timedelta(seconds=0.1)) return lock - except lockfile.TimeOutError: + except TimeOutError: if not force: raise # Force removal of lock first. lock.disown() - # pylint: disable-msg=W0612 - hostname, pid, tempfile = get_lock_data() - os.unlink(config.LOCK_FILE) - os.unlink(os.path.join(config.LOCK_DIR, tempfile)) + hostname, pid, tempfile = lock.details + os.unlink(lock_file) + os.unlink(tempfile) return acquire_lock_1(force=False) @@ -191,26 +188,23 @@ def acquire_lock(force): try: lock = acquire_lock_1(force) return lock - except lockfile.TimeOutError: - status = master_state() - if status == WatcherState.conflict: + except TimeOutError: + status, lock = master_state() + if status is WatcherState.conflict: # Hostname matches and process exists. message = _("""\ The master queue runner lock could not be acquired because it appears as though another master is already running.""") - elif status == WatcherState.stale_lock: + elif status is WatcherState.stale_lock: # Hostname matches but the process does not exist. program = sys.argv[0] message = _("""\ The master queue runner lock could not be acquired. It appears as though there is a stale master lock. Try re-running $program with the --force flag.""") - else: + elif status is WatcherState.host_mismatch: # Hostname doesn't even match. - assert status == WatcherState.host_mismatch, ( - 'Invalid enum value: %s' % status) - # pylint: disable-msg=W0612 - hostname, pid, tempfile = get_lock_data() + hostname, pid, tempfile = lock.details message = _("""\ The master qrunner lock could not be acquired, because it appears as if some process on some other host may have acquired it. We can't @@ -221,6 +215,18 @@ Lock file: $config.LOCK_FILE Lock host: $hostname Exiting.""") + else: + assert status is WatcherState.none, ( + 'Invalid enum value: %s' % status) + hostname, pid, tempfile = lock.details + message = _("""\ +For unknown reasons, the master qrunner lock could not be acquired. + + +Lock file: $config.LOCK_FILE +Lock host: $hostname + +Exiting.""") config.options.parser.error(message) @@ -300,7 +306,6 @@ class Loop: # Set up our signal handlers. Also set up a SIGALRM handler to # refresh the lock once per day. The lock lifetime is 1 day + 6 hours # so this should be plenty. - # pylint: disable-msg=W0613,C0111 def sigalrm_handler(signum, frame): self._lock.refresh() signal.alarm(SECONDS_IN_A_DAY) @@ -490,7 +495,6 @@ qrunner %s reached maximum restart limit of %d, not restarting.""", # Wait for all the children to go away. while self._kids: try: - # pylint: disable-msg=W0612 pid, status = os.wait() self._kids.drop(pid) except OSError as error: diff --git a/src/mailman/bin/tests/__init__.py b/src/mailman/bin/tests/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/bin/tests/__init__.py diff --git a/src/mailman/bin/tests/test_master.py b/src/mailman/bin/tests/test_master.py new file mode 100644 index 000000000..cb15f08f9 --- /dev/null +++ b/src/mailman/bin/tests/test_master.py @@ -0,0 +1,81 @@ +# Copyright (C) 2010 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 master watcher utilities.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'test_suite', + ] + + +import os +import errno +import tempfile +import unittest + +from flufl.lock import Lock + +from mailman.bin import master + + + +class TestMasterLock(unittest.TestCase): + def setUp(self): + fd, self.lock_file = tempfile.mkstemp() + os.close(fd) + # The lock file should not exist before we try to acquire it. + os.remove(self.lock_file) + + def tearDown(self): + # Unlocking removes the lock file, but just to be safe (i.e. in case + # of errors). + try: + os.remove(self.lock_file) + except OSError as error: + if error.errno != errno.ENOENT: + raise + + def test_acquire_lock_1(self): + lock = master.acquire_lock_1(False, self.lock_file) + is_locked = lock.is_locked + lock.unlock() + self.failUnless(is_locked) + + def test_master_state(self): + my_lock = Lock(self.lock_file) + # Mailman is not running. + state, lock = master.master_state(self.lock_file) + self.assertEqual(state, master.WatcherState.none) + # Acquire the lock as if another process had already started the + # master qrunner. + my_lock.lock() + try: + state, lock = master.master_state(self.lock_file) + finally: + my_lock.unlock() + self.assertEqual(state, master.WatcherState.conflict) + # XXX test stale_lock and host_mismatch states. + + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestMasterLock)) + return suite diff --git a/src/mailman/bin/update.py b/src/mailman/bin/update.py index c4f7f0cf1..3ec6d7427 100644 --- a/src/mailman/bin/update.py +++ b/src/mailman/bin/update.py @@ -26,7 +26,7 @@ import cPickle import marshal import optparse -from locknix.lockfile import TimeOutError +from flufl.lock import TimeOutError from mailman import MailList from mailman import Message diff --git a/src/mailman/chains/base.py b/src/mailman/chains/base.py index e8b90537a..557dffd63 100644 --- a/src/mailman/chains/base.py +++ b/src/mailman/chains/base.py @@ -90,8 +90,6 @@ class Chain: self.name = name self.description = description self._links = [] - # Register the chain. - config.chains[name] = self def append_link(self, link): """See `IMutableChain`.""" diff --git a/src/mailman/chains/builtin.py b/src/mailman/chains/builtin.py index fc31085f3..c81f6700f 100644 --- a/src/mailman/chains/builtin.py +++ b/src/mailman/chains/builtin.py @@ -62,6 +62,8 @@ class BuiltInChain: ('suspicious-header', LinkAction.defer, None), # Now if any of the above hit, jump to the hold chain. ('any', LinkAction.jump, 'hold'), + # Hold the message if the sender is a moderated member. + ('member-moderation', LinkAction.jump, 'member-moderation'), # Take a detour through the header matching chain, which we'll create # later. ('truth', LinkAction.detour, 'header-match'), diff --git a/src/mailman/chains/docs/__init__.py b/src/mailman/chains/docs/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/chains/docs/__init__.py diff --git a/src/mailman/chains/docs/moderation.txt b/src/mailman/chains/docs/moderation.txt new file mode 100644 index 000000000..c95b8cac4 --- /dev/null +++ b/src/mailman/chains/docs/moderation.txt @@ -0,0 +1,177 @@ +========== +Moderation +========== + +Posts by members and non-members are subject to moderation checks during +incoming processing. Different situations can cause such posts to be held for +moderator approval. + + >>> mlist = create_list('test@example.com') + + +Member moderation +================= + +Posts by list members are moderated if the member's moderation flag is set. +The default setting for the moderation flag of new members is determined by +the mailing list's settings. By default, a mailing list is not set to +moderate new member postings. + + >>> from mailman.app.membership import add_member + >>> from mailman.interfaces.member import DeliveryMode + >>> member = add_member(mlist, 'anne@example.com', 'Anne', 'aaa', + ... DeliveryMode.regular, 'en') + >>> member + <Member: Anne <anne@example.com> on test@example.com as MemberRole.member> + >>> member.is_moderated + False + +In order to find out whether the message is held or accepted, we can subscribe +to Zope events that are triggered on each case. +:: + + >>> from mailman.chains.base import ChainNotification + >>> def on_chain(event): + ... if isinstance(event, ChainNotification): + ... print event + ... print event.chain + ... print 'Subject:', event.msg['subject'] + ... print 'Hits:' + ... for hit in event.msgdata.get('rule_hits', []): + ... print ' ', hit + ... print 'Misses:' + ... for miss in event.msgdata.get('rule_misses', []): + ... print ' ', miss + + >>> import zope.event + >>> zope.event.subscribers.append(on_chain) + +Anne's post to the mailing list runs through the incoming runner's default +built-in chain. No rules hit and so the message is accepted. +:: + + >>> msg = message_from_string("""\ + ... From: anne@example.com + ... To: test@example.com + ... Subject: aardvark + ... + ... This is a test. + ... """) + + >>> from mailman.core.chains import process + >>> process(mlist, msg, {}, 'built-in') + <mailman.chains.accept.AcceptNotification ...> + <mailman.chains.accept.AcceptChain ...> + Subject: aardvark + Hits: + Misses: + approved + emergency + loop + administrivia + implicit-dest + max-recipients + max-size + news-moderation + no-subject + suspicious-header + member-moderation + +However, when Anne's moderation flag is set, and the list's member moderation +action is set to `hold`, her post is held for moderation. +:: + + >>> msg = message_from_string("""\ + ... From: anne@example.com + ... To: test@example.com + ... Subject: badger + ... + ... This is a test. + ... """) + + >>> member.is_moderated = True + >>> print mlist.member_moderation_action + Action.hold + + >>> process(mlist, msg, {}, 'built-in') + <mailman.chains.hold.HoldNotification ...> + <mailman.chains.hold.HoldChain ...> + Subject: badger + Hits: + member-moderation + Misses: + approved + emergency + loop + administrivia + implicit-dest + max-recipients + max-size + news-moderation + no-subject + suspicious-header + +The list's member moderation action can also be set to `discard`... +:: + + >>> from mailman.interfaces.action import Action + >>> mlist.member_moderation_action = Action.discard + + >>> msg = message_from_string("""\ + ... From: anne@example.com + ... To: test@example.com + ... Subject: cougar + ... + ... This is a test. + ... """) + + >>> process(mlist, msg, {}, 'built-in') + <mailman.chains.discard.DiscardNotification ...> + <mailman.chains.discard.DiscardChain ...> + Subject: cougar + Hits: + member-moderation + Misses: + approved + emergency + loop + administrivia + implicit-dest + max-recipients + max-size + news-moderation + no-subject + suspicious-header + +... or `reject`. + + >>> mlist.member_moderation_action = Action.reject + + >>> msg = message_from_string("""\ + ... From: anne@example.com + ... To: test@example.com + ... Subject: dingo + ... + ... This is a test. + ... """) + + >>> process(mlist, msg, {}, 'built-in') + <mailman.chains.reject.RejectNotification ...> + <mailman.chains.reject.RejectChain ...> + Subject: dingo + Hits: + member-moderation + Misses: + approved + emergency + loop + administrivia + implicit-dest + max-recipients + max-size + news-moderation + no-subject + suspicious-header + +.. Clean up + >>> zope.event.subscribers.remove(on_chain) diff --git a/src/mailman/chains/member.py b/src/mailman/chains/member.py new file mode 100644 index 000000000..4a220b59d --- /dev/null +++ b/src/mailman/chains/member.py @@ -0,0 +1,72 @@ +# Copyright (C) 2010 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/>. + +"""Member moderation chain. + +When a member's moderation flag is set, the built-in chain jumps to this +chain, which just checks the mailing list's member moderation action. Based +on this value, one of the normal termination chains is jumped to. +""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'MemberModerationChain', + ] + + +from zope.interface import implements + +from mailman.chains.base import Link +from mailman.config import config +from mailman.core.i18n import _ +from mailman.interfaces.action import Action +from mailman.interfaces.chain import IChain, LinkAction + + + +class MemberModerationChain: + """Dynamically produce a link jumping to the appropriate terminal chain. + + The terminal chain will be one of the Accept, Hold, Discard, or Reject + chains, based on the mailing list's member moderation action setting. + """ + + implements(IChain) + + name = 'member-moderation' + description = _('Member moderation chain') + is_abstract = False + + def get_links(self, mlist, msg, msgdata): + """See `IChain`.""" + # defer and accept are not valid moderation actions. + jump_chains = { + Action.hold: 'hold', + Action.reject: 'reject', + Action.discard: 'discard', + } + chain_name = jump_chains.get(mlist.member_moderation_action) + assert chain_name is not None, ( + '{0}: Invalid member_moderation_action: {1}'.format( + mlist.fqdn_listname, mlist.member_moderation_action)) + truth = config.rules['truth'] + chain = config.chains[chain_name] + return iter([ + Link(truth, LinkAction.jump, chain), + ]) diff --git a/src/mailman/commands/cli_control.py b/src/mailman/commands/cli_control.py index 83432a6e8..2c693621d 100644 --- a/src/mailman/commands/cli_control.py +++ b/src/mailman/commands/cli_control.py @@ -36,6 +36,7 @@ import logging from zope.interface import implements +from mailman.bin.master import WatcherState, master_state from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.command import ICLISubCommand @@ -54,6 +55,7 @@ class Start: def add(self, parser, command_parser): """See `ICLISubCommand`.""" + self.parser = parser command_parser.add_argument( '-f', '--force', default=False, action='store_true', @@ -92,6 +94,15 @@ class Start: def process(self, args): """See `ICLISubCommand`.""" + # Although there's a potential race condition here, it's a better user + # experience for the parent process to refuse to start twice, rather + # than having it try to start the master, which will error exit. + status, lock = master_state() + if status is WatcherState.conflict: + self.parser.error(_('GNU Mailman is already running')) + elif status in (WatcherState.stale_lock, WatcherState.host_mismatch): + self.parser.error(_('A previous run of GNU Mailman did not exit ' + 'cleanly. Try using --force.')) def log(message): if not args.quiet: print message @@ -152,7 +163,7 @@ def kill_watcher(sig): class SignalCommand: """Common base class for simple, signal sending commands.""" - + implements(ICLISubCommand) name = None diff --git a/src/mailman/commands/cli_info.py b/src/mailman/commands/cli_info.py index 1a4ea75b4..22adbd384 100644 --- a/src/mailman/commands/cli_info.py +++ b/src/mailman/commands/cli_info.py @@ -32,6 +32,7 @@ from zope.interface import implements from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.command import ICLISubCommand +from mailman.rest.helpers import path_to from mailman.version import MAILMAN_VERSION_FULL @@ -68,6 +69,9 @@ class Info: print >> output, 'Python', sys.version print >> output, 'config file:', config.filename print >> output, 'db url:', config.db.url + print >> output, 'REST root url:', path_to('/') + print >> output, 'REST credentials: {0}:{1}'.format( + config.webservice.admin_user, config.webservice.admin_pass) if args.verbose: print >> output, 'File system paths:' longest = 0 diff --git a/src/mailman/commands/cli_status.py b/src/mailman/commands/cli_status.py new file mode 100644 index 000000000..abe8cfe3e --- /dev/null +++ b/src/mailman/commands/cli_status.py @@ -0,0 +1,68 @@ +# Copyright (C) 2010 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/>. + +"""Module stuff.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'Status', + ] + + +import socket + +from zope.interface import implements + +from mailman.bin.master import WatcherState, master_state +from mailman.core.i18n import _ +from mailman.interfaces.command import ICLISubCommand + + + +class Status: + """Status of the Mailman system.""" + + implements(ICLISubCommand) + + name = 'status' + + def add(self, parser, command_parser): + """See `ICLISubCommand`.""" + pass + + def process(self, args): + """See `ICLISubCommand`.""" + status, lock = master_state() + if status is WatcherState.none: + message = _('GNU Mailman is not running') + elif status is WatcherState.conflict: + hostname, pid, tempfile = lock.details + message = _('GNU Mailman is running (master pid: $pid)') + elif status is WatcherState.stale_lock: + hostname, pid, tempfile = lock.details + message =_('GNU Mailman is stopped (stale pid: $pid)') + else: + hostname, pid, tempfile = lock.details + fqdn_name = socket.getfqdn() + assert status is WatcherState.host_mismatch, ( + 'Invalid enum value: %s' % status) + message = _('GNU Mailman is in an unexpected state ' + '($hostname != $fqdn_name)') + print message + return int(status) diff --git a/src/mailman/commands/docs/info.txt b/src/mailman/commands/docs/info.txt index 85af9325b..bccb78fda 100644 --- a/src/mailman/commands/docs/info.txt +++ b/src/mailman/commands/docs/info.txt @@ -20,6 +20,8 @@ script ``mailman info``. By default, the info is printed to standard output. ... config file: .../test.cfg db url: sqlite:.../mailman.db + REST root url: http://localhost:9001/3.0/ + REST credentials: restadmin:restpass By passing in the ``-o/--output`` option, you can print the info to a file. @@ -35,6 +37,8 @@ By passing in the ``-o/--output`` option, you can print the info to a file. ... config file: .../test.cfg db url: sqlite:.../mailman.db + REST root url: http://localhost:9001/3.0/ + REST credentials: restadmin:restpass You can also get more verbose information, which contains a list of the file system paths that Mailman is using. diff --git a/src/mailman/commands/docs/status.txt b/src/mailman/commands/docs/status.txt new file mode 100644 index 000000000..6deb7fdc0 --- /dev/null +++ b/src/mailman/commands/docs/status.txt @@ -0,0 +1,37 @@ +============== +Getting status +============== + +The status of the Mailman master process can be queried from the command line. +It's clear at this point that nothing is running. +:: + + >>> from mailman.commands.cli_status import Status + >>> status = Status() + + >>> class FakeArgs: + ... pass + +The status is printed to stdout and a status code is returned. + + >>> status.process(FakeArgs) + GNU Mailman is not running + 0 + +We can simulate the master queue runner starting up by acquiring its lock. + + >>> from flufl.lock import Lock + >>> lock = Lock(config.LOCK_FILE) + >>> lock.lock() + +Getting the status confirms that the master queue runner is running. + + >>> status.process(FakeArgs) + GNU Mailman is running (master pid: ... + +We shutdown the master queue runner, and confirm the status. + + >>> lock.unlock() + >>> status.process(FakeArgs) + GNU Mailman is not running + 0 diff --git a/src/mailman/core/chains.py b/src/mailman/core/chains.py index 35e886d69..a81cbeedc 100644 --- a/src/mailman/core/chains.py +++ b/src/mailman/core/chains.py @@ -26,14 +26,12 @@ __all__ = [ ] -from mailman.chains.accept import AcceptChain -from mailman.chains.builtin import BuiltInChain -from mailman.chains.discard import DiscardChain -from mailman.chains.headers import HeaderMatchChain -from mailman.chains.hold import HoldChain -from mailman.chains.reject import RejectChain +from zope.interface.verify import verifyObject + +from mailman.app.finder import find_components +from mailman.chains.base import Chain, TerminalChainBase from mailman.config import config -from mailman.interfaces.chain import LinkAction +from mailman.interfaces.chain import LinkAction, IChain @@ -67,7 +65,6 @@ def process(mlist, msg, msgdata, start_chain='built-in'): return chain, chain_iter = chain_stack.pop() continue - # Process this link. if link.rule.check(mlist, msg, msgdata): if link.rule.record: hits.append(link.rule.name) @@ -103,16 +100,19 @@ def process(mlist, msg, msgdata, start_chain='built-in'): def initialize(): """Set up chains, both built-in and from the database.""" - for chain_class in (DiscardChain, HoldChain, RejectChain, AcceptChain): + for chain_class in find_components('mailman.chains', IChain): + # FIXME 2010-12-28 barry: We need a generic way to disable automatic + # instantiation of discovered classes. This is useful not just for + # chains, but also for rules, handlers, etc. Ideally it should be + # part of find_components(). For now, hard code the ones we do not + # want to instantiate. + if chain_class in (Chain, TerminalChainBase): + continue chain = chain_class() + verifyObject(IChain, chain) assert chain.name not in config.chains, ( - 'Duplicate chain name: {0}'.format(chain.name)) + 'Duplicate chain "{0}" found in {1} (previously: {2}'.format( + chain.name, chain_class, config.chains[chain.name])) config.chains[chain.name] = chain - # Set up a couple of other default chains. - chain = BuiltInChain() - config.chains[chain.name] = chain - # Create and initialize the header matching chain. - chain = HeaderMatchChain() - config.chains[chain.name] = chain # XXX Read chains from the database and initialize them. pass diff --git a/src/mailman/core/logging.py b/src/mailman/core/logging.py index edbacef31..e90367aae 100644 --- a/src/mailman/core/logging.py +++ b/src/mailman/core/logging.py @@ -55,10 +55,10 @@ class ReopenableFileHandler(logging.Handler): """A file handler that supports reopening.""" def __init__(self, name, filename): + logging.Handler.__init__(self) self.name = name self._filename = filename self._stream = self._open() - logging.Handler.__init__(self) def _open(self): return codecs.open(self._filename, 'a', 'utf-8') diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql index 702f357be..4ba71f8f0 100644 --- a/src/mailman/database/mailman.sql +++ b/src/mailman/database/mailman.sql @@ -156,7 +156,7 @@ CREATE TABLE mailinglist ( max_days_to_hold INTEGER, max_message_size INTEGER, max_num_recipients INTEGER, - member_moderation_action BOOLEAN, + member_moderation_action INTEGER, member_moderation_notice TEXT, mime_is_default_digest BOOLEAN, moderator_password TEXT, diff --git a/src/mailman/database/stock.py b/src/mailman/database/stock.py index e3c802455..92a344171 100644 --- a/src/mailman/database/stock.py +++ b/src/mailman/database/stock.py @@ -25,7 +25,7 @@ __all__ = [ import os import logging -from locknix.lockfile import Lock +from flufl.lock import Lock from lazr.config import as_boolean from pkg_resources import resource_string from storm.cache import GenerationalCache diff --git a/src/mailman/docs/NEWS.txt b/src/mailman/docs/NEWS.txt index 6ded4f2a9..0062ec7e6 100644 --- a/src/mailman/docs/NEWS.txt +++ b/src/mailman/docs/NEWS.txt @@ -20,6 +20,10 @@ REST ---- * Add Basic Auth support for REST API security. (given by Jimmy Bergman) +Build +----- + * Support Python 2.7. (LP: #667472) + 3.0 alpha 6 -- "Cut to the Chase" ================================= diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py index 13e026f8c..41eef81c8 100644 --- a/src/mailman/interfaces/mailinglist.py +++ b/src/mailman/interfaces/mailinglist.py @@ -457,6 +457,16 @@ class IMailingList(Interface): `pass_extensions` is non-empty. """) + # Moderation. + + default_member_moderation = Attribute( + """Default moderation flag for new mailing list members. + + When an address is subscribed to the mailing list, this boolean + attribute sets the initial moderation flag value. When a member's + posts are moderated, they must first be approved by the mailing list + owner or moderator. + """) diff --git a/src/mailman/model/docs/membership.txt b/src/mailman/model/docs/membership.txt index a3782eef6..1db2550f7 100644 --- a/src/mailman/model/docs/membership.txt +++ b/src/mailman/model/docs/membership.txt @@ -231,3 +231,27 @@ It is an error to subscribe someone to a list with the same role twice. ... AlreadySubscribedError: aperson@example.com is already a MemberRole.owner of mailing list _xtest@example.com + + +Moderation flag +=============== + +All members have a moderation flag which specifies whether postings from that +member must first be approved by the list owner or moderator. The default +value of this flag is inherited from the mailing lists configuration. +:: + + >>> mlist.default_member_moderation + False + >>> user_4 = user_manager.create_user('dperson@example.com', 'Dave') + >>> address_4 = list(user_4.addresses)[0] + >>> member_4 = address_4.subscribe(mlist, MemberRole.member) + >>> member_4.is_moderated + False + + >>> mlist.default_member_moderation = True + >>> user_5 = user_manager.create_user('eperson@example.com', 'Elly') + >>> address_5 = list(user_5.addresses)[0] + >>> member_5 = address_5.subscribe(mlist, MemberRole.member) + >>> member_5.is_moderated + True diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 5a6f26725..19116af6b 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -119,7 +119,7 @@ class MailingList(Model): bounce_unrecognized_goes_to_list_owner = Bool() # XXX bounce_you_are_disabled_warnings = Int() # XXX bounce_you_are_disabled_warnings_interval = TimeDelta() # XXX - default_member_moderation = Bool() # XXX + default_member_moderation = Bool() description = Unicode() digest_footer = Unicode() digest_header = Unicode() diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py index 429d9727d..48cd54b28 100644 --- a/src/mailman/model/member.py +++ b/src/mailman/model/member.py @@ -25,12 +25,14 @@ __all__ = [ ] from storm.locals import Bool, Int, Reference, Unicode +from zope.component import getUtility from zope.interface import implements from mailman.config import config from mailman.core.constants import system_preferences from mailman.database.model import Model from mailman.database.types import Enum +from mailman.interfaces.listmanager import IListManager from mailman.interfaces.member import IMember @@ -52,7 +54,8 @@ class Member(Model): self.role = role self.mailing_list = mailing_list self.address = address - self.is_moderated = False + self.is_moderated = getUtility(IListManager).get( + mailing_list).default_member_moderation def __repr__(self): return '<Member: {0} on {1} as {2}>'.format( diff --git a/src/mailman/mta/postfix.py b/src/mailman/mta/postfix.py index 0c4604ae5..59252f56d 100644 --- a/src/mailman/mta/postfix.py +++ b/src/mailman/mta/postfix.py @@ -29,7 +29,7 @@ import os import logging import datetime -from locknix.lockfile import Lock +from flufl.lock import Lock from zope.component import getUtility from zope.interface import implements diff --git a/src/mailman/pipeline/scrubber.py b/src/mailman/pipeline/scrubber.py index c9c7a3f6d..841936e3f 100644 --- a/src/mailman/pipeline/scrubber.py +++ b/src/mailman/pipeline/scrubber.py @@ -34,8 +34,8 @@ import binascii from email.charset import Charset from email.utils import make_msgid, parsedate +from flufl.lock import Lock from lazr.config import as_boolean -from locknix.lockfile import Lock from mimetypes import guess_all_extensions from string import Template from zope.interface import implements diff --git a/src/mailman/queue/archive.py b/src/mailman/queue/archive.py index 42a3d301a..542b5ad5c 100644 --- a/src/mailman/queue/archive.py +++ b/src/mailman/queue/archive.py @@ -28,8 +28,8 @@ import logging from datetime import datetime from email.Utils import parsedate_tz, mktime_tz, formatdate +from flufl.lock import Lock from lazr.config import as_timedelta -from locknix.lockfile import Lock from mailman.config import config from mailman.queue import Runner diff --git a/src/mailman/queue/docs/incoming.txt b/src/mailman/queue/docs/incoming.txt index 0e1ad1fca..24e7ecfd1 100644 --- a/src/mailman/queue/docs/incoming.txt +++ b/src/mailman/queue/docs/incoming.txt @@ -12,7 +12,7 @@ processing begins, with a global default. This chain is processed with the message eventually ending up in one of the four disposition states described above. - >>> mlist = create_list('_xtest@example.com') + >>> mlist = create_list('test@example.com') >>> print mlist.start_chain built-in @@ -26,15 +26,15 @@ pipeline queue. >>> msg = message_from_string("""\ ... From: aperson@example.com - ... To: _xtest@example.com + ... To: test@example.com ... Subject: My first post ... Message-ID: <first> ... ... First post! ... """) -Normally, the upstream mail server would drop the message in the incoming -queue, but this is an effective simulation. +Inject the message into the incoming queue, similar to the way the upstream +mail server normally would. >>> from mailman.inject import inject_message >>> inject_message(mlist, msg) @@ -58,14 +58,13 @@ And now the message is in the pipeline queue. >>> item = get_queue_messages('pipeline')[0] >>> print item.msg.as_string() From: aperson@example.com - To: _xtest@example.com + To: test@example.com Subject: My first post Message-ID: <first> Date: ... X-Mailman-Rule-Misses: approved; emergency; loop; administrivia; - implicit-dest; - max-recipients; max-size; news-moderation; no-subject; - suspicious-header + implicit-dest; max-recipients; max-size; news-moderation; no-subject; + suspicious-header; member-moderation <BLANKLINE> First post! <BLANKLINE> @@ -81,27 +80,28 @@ Held messages The list moderator sets the emergency flag on the mailing list. The built-in chain will now hold all posted messages, so nothing will show up in the pipeline queue. -::: +:: + + >>> from mailman.chains.base import ChainNotification + >>> def on_chain(event): + ... if isinstance(event, ChainNotification): + ... print event + ... print event.chain + ... print 'From: {0}\nTo: {1}\nMessage-ID: {2}'.format( + ... event.msg['from'], event.msg['to'], + ... event.msg['message-id']) - # XXX This checks the vette log file because there is no other evidence - # that this chain has done anything. - >>> import os - >>> fp = open(os.path.join(config.LOG_DIR, 'vette')) - >>> fp.seek(0, 2) + >>> import zope.event + >>> zope.event.subscribers.append(on_chain) >>> mlist.emergency = True >>> inject_message(mlist, msg) - >>> file_pos = fp.tell() >>> incoming.run() - >>> len(pipeline_queue.files) - 0 - >>> len(incoming_queue.files) - 0 - >>> fp.seek(file_pos) - >>> print 'LOG:', fp.read() - LOG: ... HOLD: _xtest@example.com post from aperson@example.com held, - message-id=<first>: n/a - <BLANKLINE> + <mailman.chains.hold.HoldNotification ...> + <mailman.chains.hold.HoldChain ...> + From: aperson@example.com + To: test@example.com + Message-ID: <first> >>> mlist.emergency = False @@ -116,26 +116,35 @@ new chain and set it as the mailing list's start chain. >>> from mailman.chains.base import Chain, Link >>> from mailman.interfaces.chain import LinkAction - >>> truth_rule = config.rules['truth'] - >>> discard_chain = config.chains['discard'] - >>> test_chain = Chain('always-discard', 'Testing discards') - >>> link = Link(truth_rule, LinkAction.jump, discard_chain) - >>> test_chain.append_link(link) - >>> mlist.start_chain = 'always-discard' + >>> def make_chain(name, target_chain): + ... truth_rule = config.rules['truth'] + ... target_chain = config.chains[target_chain] + ... test_chain = Chain(name, 'Testing {0}'.format(target_chain)) + ... config.chains[test_chain.name] = test_chain + ... link = Link(truth_rule, LinkAction.jump, target_chain) + ... test_chain.append_link(link) + ... return test_chain + + >>> test_chain = make_chain('always-discard', 'discard') + >>> mlist.start_chain = test_chain.name + >>> msg.replace_header('message-id', '<second>') >>> inject_message(mlist, msg) - >>> file_pos = fp.tell() >>> incoming.run() - >>> len(pipeline_queue.files) - 0 - >>> len(incoming_queue.files) - 0 - >>> fp.seek(file_pos) - >>> print 'LOG:', fp.read() - LOG: ... DISCARD: <first> - <BLANKLINE> + <mailman.chains.discard.DiscardNotification ...> + <mailman.chains.discard.DiscardChain ...> + From: aperson@example.com + To: test@example.com + Message-ID: <second> + + >>> del config.chains[test_chain.name] - >>> del config.chains['always-discard'] +.. + The virgin queue needs to be cleared out due to artifacts from the + previous tests above. + + >>> virgin_queue = config.switchboards['virgin'] + >>> ignore = get_queue_messages('virgin') Rejected messages @@ -145,33 +154,27 @@ Similar to discarded messages, a message can be rejected, or bounced back to the original sender. Again, the built-in chain doesn't support this so we'll just create a new chain that does. - >>> reject_chain = config.chains['reject'] - >>> test_chain = Chain('always-reject', 'Testing rejections') - >>> link = Link(truth_rule, LinkAction.jump, reject_chain) - >>> test_chain.append_link(link) - >>> mlist.start_chain = 'always-reject' - -The virgin queue needs to be cleared out due to artifacts from the previous -tests above. -:: - - >>> virgin_queue = config.switchboards['virgin'] - >>> ignore = get_queue_messages('virgin') + >>> test_chain = make_chain('always-reject', 'reject') + >>> mlist.start_chain = test_chain.name + >>> msg.replace_header('message-id', '<third>') >>> inject_message(mlist, msg) - >>> file_pos = fp.tell() >>> incoming.run() - >>> len(pipeline_queue.files) - 0 - >>> len(incoming_queue.files) - 0 + <mailman.chains.reject.RejectNotification ...> + <mailman.chains.reject.RejectChain ...> + From: aperson@example.com + To: test@example.com + Message-ID: <third> + +The rejection message is sitting in the virgin queue waiting to be delivered +to the original sender. >>> len(virgin_queue.files) 1 >>> item = get_queue_messages('virgin')[0] >>> print item.msg.as_string() Subject: My first post - From: _xtest-owner@example.com + From: test-owner@example.com To: aperson@example.com ... <BLANKLINE> @@ -186,24 +189,18 @@ tests above. MIME-Version: 1.0 <BLANKLINE> From: aperson@example.com - To: _xtest@example.com + To: test@example.com Subject: My first post - Message-ID: <first> + Message-ID: <third> Date: ... <BLANKLINE> First post! <BLANKLINE> --===============... - >>> dump_msgdata(item.msgdata) - _parsemsg : False - ... - recipients : [u'aperson@example.com'] - ... + >>> del config.chains['always-reject'] - >>> fp.seek(file_pos) - >>> print 'LOG:', fp.read() - LOG: ... REJECT: <first> - <BLANKLINE> +.. + Clean up. - >>> del config.chains['always-reject'] + >>> zope.event.subscribers.remove(on_chain) diff --git a/src/mailman/queue/docs/rest.txt b/src/mailman/queue/docs/rest.txt index a71915a49..9e8851eca 100644 --- a/src/mailman/queue/docs/rest.txt +++ b/src/mailman/queue/docs/rest.txt @@ -10,11 +10,11 @@ Mailman is controllable through an administrative `REST`_ HTTP server. The RESTful server can be used to access basic version information. - >>> dump_json('http://localhost:8001/3.0/system') + >>> dump_json('http://localhost:9001/3.0/system') http_etag: "..." mailman_version: GNU Mailman 3.0... (...) python_version: ... - self_link: http://localhost:8001/3.0/system + self_link: http://localhost:9001/3.0/system Clean up diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py index 36167720d..7b7c127c2 100644 --- a/src/mailman/rest/configuration.py +++ b/src/mailman/rest/configuration.py @@ -31,8 +31,7 @@ from restish import http, resource from mailman.config import config from mailman.interfaces.action import Action from mailman.interfaces.autorespond import ResponseAction -from mailman.interfaces.mailinglist import IAcceptableAliasSet -from mailman.interfaces.mailinglist import ReplyToMunging +from mailman.interfaces.mailinglist import IAcceptableAliasSet, ReplyToMunging from mailman.rest.helpers import PATCH, etag from mailman.rest.validator import Validator, enum_validator diff --git a/src/mailman/rest/docs/basic.txt b/src/mailman/rest/docs/basic.txt index 614b074ba..cf02fa4ec 100644 --- a/src/mailman/rest/docs/basic.txt +++ b/src/mailman/rest/docs/basic.txt @@ -20,11 +20,11 @@ The Mailman major and minor version numbers are in the URL. System information can be retrieved from the server. By default JSON is returned. - >>> dump_json('http://localhost:8001/3.0/system') + >>> dump_json('http://localhost:9001/3.0/system') http_etag: "..." mailman_version: GNU Mailman 3.0... (...) python_version: ... - self_link: http://localhost:8001/3.0/system + self_link: http://localhost:9001/3.0/system Non-existent links @@ -33,7 +33,7 @@ Non-existent links When you try to access a link that doesn't exist, you get the appropriate HTTP 404 Not Found error. - >>> dump_json('http://localhost:8001/3.0/does-not-exist') + >>> dump_json('http://localhost:9001/3.0/does-not-exist') Traceback (most recent call last): ... HTTPError: HTTP Error 404: 404 Not Found @@ -49,7 +49,7 @@ an appropriate HTTP 401 Unauthorized error. >>> from base64 import b64encode >>> auth = b64encode('baduser:badpass') - >>> url = 'http://localhost:8001/3.0/system' + >>> url = 'http://localhost:9001/3.0/system' >>> headers = { ... 'Content-Type': 'application/x-www-form-urlencode', ... 'Authorization': 'Basic ' + auth, diff --git a/src/mailman/rest/docs/configuration.txt b/src/mailman/rest/docs/configuration.txt index 1260a9e1b..7cf1ce540 100644 --- a/src/mailman/rest/docs/configuration.txt +++ b/src/mailman/rest/docs/configuration.txt @@ -13,7 +13,7 @@ Reading a configuration All readable attributes for a list are available on a sub-resource. - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config') acceptable_aliases: [] admin_immed_notify: True @@ -72,7 +72,7 @@ Not all of the readable attributes can be set through the web interface. The ones that can, can either be set via ``PUT`` or ``PATCH``. ``PUT`` changes all the writable attributes in one request. - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config', ... dict( ... acceptable_aliases=['one@example.com', 'two@example.com'], @@ -112,7 +112,7 @@ all the writable attributes in one request. These values are changed permanently. - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config') acceptable_aliases: [u'one@example.com', u'two@example.com'] admin_immed_notify: False @@ -150,7 +150,7 @@ These values are changed permanently. If you use ``PUT`` to change a list's configuration, all writable attributes must be included. It is an error to leave one or more out... - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config', ... dict( ... #acceptable_aliases=['one', 'two'], @@ -189,7 +189,7 @@ must be included. It is an error to leave one or more out... ...or to add an unknown one. - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config', ... dict( ... a_mailing_list_attribute=False, @@ -223,7 +223,7 @@ must be included. It is an error to leave one or more out... It is also an error to spell an attribute value incorrectly... - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config', ... dict( ... admin_immed_notify='Nope', @@ -256,7 +256,7 @@ It is also an error to spell an attribute value incorrectly... ...or to name a pipeline that doesn't exist... - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config', ... dict( ... acceptable_aliases=['one', 'two'], @@ -288,7 +288,7 @@ It is also an error to spell an attribute value incorrectly... ...or to name an invalid auto-response enumeration value. - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config', ... dict( ... acceptable_aliases=['one', 'two'], @@ -324,7 +324,7 @@ Changing a partial configuration Using ``PATCH``, you can change just one attribute. - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config', ... dict(real_name='My List'), ... 'PATCH') @@ -358,7 +358,7 @@ emails. By default, a mailing list has no acceptable aliases. >>> from mailman.interfaces.mailinglist import IAcceptableAliasSet >>> IAcceptableAliasSet(mlist).clear() >>> transaction.commit() - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config/acceptable_aliases') acceptable_aliases: [] http_etag: "..." @@ -366,7 +366,7 @@ emails. By default, a mailing list has no acceptable aliases. We can add a few by ``PUT``-ing them on the sub-resource. The keys in the dictionary are ignored. - >>> dump_json('http://localhost:8001/3.0/lists/' + >>> dump_json('http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config/acceptable_aliases', ... dict(acceptable_aliases=['foo@example.com', ... 'bar@example.net']), @@ -379,7 +379,7 @@ dictionary are ignored. Aliases are returned as a list on the ``aliases`` key. >>> response = call_http( - ... 'http://localhost:8001/3.0/lists/' + ... 'http://localhost:9001/3.0/lists/' ... 'test-one@example.com/config/acceptable_aliases') >>> for alias in response['acceptable_aliases']: ... print alias diff --git a/src/mailman/rest/docs/domains.txt b/src/mailman/rest/docs/domains.txt index b9b7dc3d6..29592fadb 100644 --- a/src/mailman/rest/docs/domains.txt +++ b/src/mailman/rest/docs/domains.txt @@ -18,7 +18,7 @@ Domains The REST API can be queried for the set of known domains, of which there are initially none. - >>> dump_json('http://localhost:8001/3.0/domains') + >>> dump_json('http://localhost:9001/3.0/domains') http_etag: "..." start: 0 total_size: 0 @@ -33,14 +33,14 @@ Once a domain is added, it is accessible through the API. contact_address: postmaster@example.com> >>> transaction.commit() - >>> dump_json('http://localhost:8001/3.0/domains') + >>> dump_json('http://localhost:9001/3.0/domains') entry 0: base_url: http://lists.example.com contact_address: postmaster@example.com description: An example domain email_host: example.com http_etag: "..." - self_link: http://localhost:8001/3.0/domains/example.com + self_link: http://localhost:9001/3.0/domains/example.com url_host: lists.example.com http_etag: "..." start: 0 @@ -65,14 +65,14 @@ At the top level, all domains are returned as separate entries. contact_address: porkmaster@example.net> >>> transaction.commit() - >>> dump_json('http://localhost:8001/3.0/domains') + >>> dump_json('http://localhost:9001/3.0/domains') entry 0: base_url: http://lists.example.com contact_address: postmaster@example.com description: An example domain email_host: example.com http_etag: "..." - self_link: http://localhost:8001/3.0/domains/example.com + self_link: http://localhost:9001/3.0/domains/example.com url_host: lists.example.com entry 1: base_url: http://mail.example.org @@ -80,7 +80,7 @@ At the top level, all domains are returned as separate entries. description: None email_host: example.org http_etag: "..." - self_link: http://localhost:8001/3.0/domains/example.org + self_link: http://localhost:9001/3.0/domains/example.org url_host: mail.example.org entry 2: base_url: http://example.net @@ -88,7 +88,7 @@ At the top level, all domains are returned as separate entries. description: Porkmasters email_host: lists.example.net http_etag: "..." - self_link: http://localhost:8001/3.0/domains/lists.example.net + self_link: http://localhost:9001/3.0/domains/lists.example.net url_host: example.net http_etag: "..." start: 0 @@ -101,18 +101,18 @@ Individual domains The information for a single domain is available by following one of the ``self_links`` from the above collection. - >>> dump_json('http://localhost:8001/3.0/domains/lists.example.net') + >>> dump_json('http://localhost:9001/3.0/domains/lists.example.net') base_url: http://example.net contact_address: porkmaster@example.net description: Porkmasters email_host: lists.example.net http_etag: "..." - self_link: http://localhost:8001/3.0/domains/lists.example.net + self_link: http://localhost:9001/3.0/domains/lists.example.net url_host: example.net But we get a 404 for a non-existent domain. - >>> dump_json('http://localhost:8001/3.0/domains/does-not-exist') + >>> dump_json('http://localhost:9001/3.0/domains/does-not-exist') Traceback (most recent call last): ... HTTPError: HTTP Error 404: 404 Not Found @@ -123,23 +123,23 @@ Creating new domains New domains can be created by posting to the ``domains`` url. - >>> dump_json('http://localhost:8001/3.0/domains', { + >>> dump_json('http://localhost:9001/3.0/domains', { ... 'email_host': 'lists.example.com', ... }) content-length: 0 date: ... - location: http://localhost:8001/3.0/domains/lists.example.com + location: http://localhost:9001/3.0/domains/lists.example.com ... Now the web service knows about our new domain. - >>> dump_json('http://localhost:8001/3.0/domains/lists.example.com') + >>> dump_json('http://localhost:9001/3.0/domains/lists.example.com') base_url: http://lists.example.com contact_address: postmaster@lists.example.com description: None email_host: lists.example.com http_etag: "..." - self_link: http://localhost:8001/3.0/domains/lists.example.com + self_link: http://localhost:9001/3.0/domains/lists.example.com url_host: lists.example.com And the new domain is in our database. @@ -157,7 +157,7 @@ You can also create a new domain with a description, a base url, and a contact address. :: - >>> dump_json('http://localhost:8001/3.0/domains', { + >>> dump_json('http://localhost:9001/3.0/domains', { ... 'email_host': 'my.example.com', ... 'description': 'My new domain', ... 'base_url': 'http://allmy.example.com', @@ -165,16 +165,16 @@ address. ... }) content-length: 0 date: ... - location: http://localhost:8001/3.0/domains/my.example.com + location: http://localhost:9001/3.0/domains/my.example.com ... - >>> dump_json('http://localhost:8001/3.0/domains/my.example.com') + >>> dump_json('http://localhost:9001/3.0/domains/my.example.com') base_url: http://allmy.example.com contact_address: helpme@example.com description: My new domain email_host: my.example.com http_etag: "..." - self_link: http://localhost:8001/3.0/domains/my.example.com + self_link: http://localhost:9001/3.0/domains/my.example.com url_host: allmy.example.com >>> domain_manager['my.example.com'] diff --git a/src/mailman/rest/docs/helpers.txt b/src/mailman/rest/docs/helpers.txt index eec72da84..4f0b1c804 100644 --- a/src/mailman/rest/docs/helpers.txt +++ b/src/mailman/rest/docs/helpers.txt @@ -14,7 +14,7 @@ function can return them the full path to the resource. >>> from mailman.rest.helpers import path_to >>> print path_to('system') - http://localhost:8001/3.0/system + http://localhost:9001/3.0/system Parameters like the ``scheme``, ``host``, ``port``, and API version number can be set in the configuration file. diff --git a/src/mailman/rest/docs/lists.txt b/src/mailman/rest/docs/lists.txt index 4cb00e378..32cca5fb7 100644 --- a/src/mailman/rest/docs/lists.txt +++ b/src/mailman/rest/docs/lists.txt @@ -6,7 +6,7 @@ The REST API can be queried for the set of known mailing lists. There is a top level collection that can return all the mailing lists. There aren't any yet though. - >>> dump_json('http://localhost:8001/3.0/lists') + >>> dump_json('http://localhost:9001/3.0/lists') http_etag: "..." start: 0 total_size: 0 @@ -18,14 +18,14 @@ Create a mailing list in a domain and it's accessible via the API. <mailing list "test-one@example.com" at ...> >>> transaction.commit() - >>> dump_json('http://localhost:8001/3.0/lists') + >>> dump_json('http://localhost:9001/3.0/lists') entry 0: fqdn_listname: test-one@example.com host_name: example.com http_etag: "..." list_name: test-one real_name: Test-one - self_link: http://localhost:8001/3.0/lists/test-one@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com http_etag: "..." start: 0 total_size: 1 @@ -37,12 +37,12 @@ Creating lists via the API New mailing lists can also be created through the API, by posting to the ``lists`` URL. - >>> dump_json('http://localhost:8001/3.0/lists', { + >>> dump_json('http://localhost:9001/3.0/lists', { ... 'fqdn_listname': 'test-two@example.com', ... }) content-length: 0 date: ... - location: http://localhost:8001/3.0/lists/test-two@example.com + location: http://localhost:9001/3.0/lists/test-two@example.com ... The mailing list exists in the database. @@ -61,18 +61,18 @@ The mailing list exists in the database. It is also available via the location given in the response. - >>> dump_json('http://localhost:8001/3.0/lists/test-two@example.com') + >>> dump_json('http://localhost:9001/3.0/lists/test-two@example.com') fqdn_listname: test-two@example.com host_name: example.com http_etag: "..." list_name: test-two real_name: Test-two - self_link: http://localhost:8001/3.0/lists/test-two@example.com + self_link: http://localhost:9001/3.0/lists/test-two@example.com However, you are not allowed to create a mailing list in a domain that does not exist. - >>> dump_json('http://localhost:8001/3.0/lists', { + >>> dump_json('http://localhost:9001/3.0/lists', { ... 'fqdn_listname': 'test-three@example.org', ... }) Traceback (most recent call last): @@ -81,7 +81,7 @@ not exist. Nor can you create a mailing list that already exists. - >>> dump_json('http://localhost:8001/3.0/lists', { + >>> dump_json('http://localhost:9001/3.0/lists', { ... 'fqdn_listname': 'test-one@example.com', ... }) Traceback (most recent call last): @@ -96,7 +96,7 @@ Existing mailing lists can be deleted through the API, by doing an HTTP ``DELETE`` on the mailing list URL. :: - >>> dump_json('http://localhost:8001/3.0/lists/test-two@example.com', + >>> dump_json('http://localhost:9001/3.0/lists/test-two@example.com', ... method='DELETE') content-length: 0 date: ... @@ -116,13 +116,13 @@ You cannot delete a mailing list that does not exist or has already been deleted. :: - >>> dump_json('http://localhost:8001/3.0/lists/test-two@example.com', + >>> dump_json('http://localhost:9001/3.0/lists/test-two@example.com', ... method='DELETE') Traceback (most recent call last): ... HTTPError: HTTP Error 404: 404 Not Found - >>> dump_json('http://localhost:8001/3.0/lists/test-ten@example.com', + >>> dump_json('http://localhost:9001/3.0/lists/test-ten@example.com', ... method='DELETE') Traceback (most recent call last): ... diff --git a/src/mailman/rest/docs/membership.txt b/src/mailman/rest/docs/membership.txt index d34794447..553f6fde9 100644 --- a/src/mailman/rest/docs/membership.txt +++ b/src/mailman/rest/docs/membership.txt @@ -8,7 +8,7 @@ returns all the members of all known mailing lists. There are no mailing lists and no members yet. - >>> dump_json('http://localhost:8001/3.0/members') + >>> dump_json('http://localhost:9001/3.0/members') http_etag: "..." start: 0 total_size: 0 @@ -19,7 +19,7 @@ We create a mailing list, which starts out with no members. >>> mlist_one = create_list('test-one@example.com') >>> transaction.commit() - >>> dump_json('http://localhost:8001/3.0/members') + >>> dump_json('http://localhost:9001/3.0/members') http_etag: "..." start: 0 total_size: 0 @@ -39,10 +39,12 @@ the REST interface. >>> from mailman.testing.helpers import subscribe >>> subscribe(mlist_one, 'Bart') - >>> dump_json('http://localhost:8001/3.0/members') + >>> dump_json('http://localhost:9001/3.0/members') entry 0: + address: bperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com http_etag: "..." start: 0 total_size: 1 @@ -51,13 +53,17 @@ When Cris also joins the mailing list, her subscription is also available via the REST interface. >>> subscribe(mlist_one, 'Cris') - >>> dump_json('http://localhost:8001/3.0/members') + >>> dump_json('http://localhost:9001/3.0/members') entry 0: + address: bperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com entry 1: + address: cperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/cperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/cperson@example.com http_etag: "..." start: 0 total_size: 2 @@ -68,16 +74,22 @@ subscribes, she is returned first. >>> subscribe(mlist_one, 'Anna') - >>> dump_json('http://localhost:8001/3.0/members') + >>> dump_json('http://localhost:9001/3.0/members') entry 0: + address: aperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/aperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/aperson@example.com entry 1: + address: bperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com entry 2: + address: cperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/cperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/cperson@example.com http_etag: "..." start: 0 total_size: 3 @@ -90,22 +102,32 @@ address. Anna and Cris subscribe to this new mailing list. >>> subscribe(mlist_two, 'Anna') >>> subscribe(mlist_two, 'Cris') - >>> dump_json('http://localhost:8001/3.0/members') + >>> dump_json('http://localhost:9001/3.0/members') entry 0: + address: aperson@example.com + fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/aperson@example.com + self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/aperson@example.com entry 1: + address: cperson@example.com + fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/cperson@example.com + self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/cperson@example.com entry 2: + address: aperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/aperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/aperson@example.com entry 3: + address: bperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com entry 4: + address: cperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/cperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/cperson@example.com http_etag: "..." start: 0 total_size: 5 @@ -113,13 +135,17 @@ address. Anna and Cris subscribe to this new mailing list. We can also get just the members of a single mailing list. >>> dump_json( - ... 'http://localhost:8001/3.0/lists/alpha@example.com/roster/members') + ... 'http://localhost:9001/3.0/lists/alpha@example.com/roster/members') entry 0: + address: aperson@example.com + fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/aperson@example.com + self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/aperson@example.com entry 1: + address: cperson@example.com + fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/cperson@example.com + self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/cperson@example.com http_etag: ... start: 0 total_size: 2 @@ -136,28 +162,42 @@ test-one mailing list. >>> subscribe(mlist_one, 'Cris', MemberRole.owner) >>> subscribe(mlist_two, 'Dave', MemberRole.moderator) - >>> dump_json('http://localhost:8001/3.0/members') + >>> dump_json('http://localhost:9001/3.0/members') entry 0: + address: dperson@example.com + fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/alpha@example.com/moderator/dperson@example.com + self_link: http://localhost:9001/3.0/lists/alpha@example.com/moderator/dperson@example.com entry 1: + address: aperson@example.com + fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/aperson@example.com + self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/aperson@example.com entry 2: + address: cperson@example.com + fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/cperson@example.com + self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/cperson@example.com entry 3: + address: cperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/owner/cperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/owner/cperson@example.com entry 4: + address: aperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/aperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/aperson@example.com entry 5: + address: bperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com entry 6: + address: cperson@example.com + fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/cperson@example.com + self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/cperson@example.com http_etag: "..." start: 0 total_size: 7 @@ -174,14 +214,14 @@ Elly subscribes to the alpha mailing list. By default, get gets a regular delivery. Since Elly's email address is not yet known to Mailman, a user is created for her. - >>> dump_json('http://localhost:8001/3.0/members', { + >>> dump_json('http://localhost:9001/3.0/members', { ... 'fqdn_listname': 'alpha@example.com', ... 'address': 'eperson@example.com', ... 'real_name': 'Elly Person', ... }) content-length: 0 date: ... - location: http://localhost:8001/3.0/lists/alpha@example.com/member/eperson@example.com + location: http://localhost:9001/3.0/lists/alpha@example.com/member/eperson@example.com ... Elly is now a member of the mailing list. @@ -194,12 +234,14 @@ Elly is now a member of the mailing list. >>> set(member.mailing_list for member in elly.memberships.members) set([u'alpha@example.com']) - >>> dump_json('http://localhost:8001/3.0/members') + >>> dump_json('http://localhost:9001/3.0/members') entry 0: ... entry 3: + address: eperson@example.com + fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/eperson@example.com + self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/eperson@example.com ... @@ -212,7 +254,7 @@ so she leaves from the mailing list. # Ensure our previous reads don't keep the database lock. >>> transaction.abort() - >>> dump_json('http://localhost:8001/3.0/lists/alpha@example.com' + >>> dump_json('http://localhost:9001/3.0/lists/alpha@example.com' ... '/member/eperson@example.com', ... method='DELETE') content-length: 0 @@ -232,7 +274,7 @@ Fred joins the alpha mailing list but wants MIME digest delivery. :: >>> transaction.abort() - >>> dump_json('http://localhost:8001/3.0/members', { + >>> dump_json('http://localhost:9001/3.0/members', { ... 'fqdn_listname': 'alpha@example.com', ... 'address': 'fperson@example.com', ... 'real_name': 'Fred Person', @@ -240,7 +282,7 @@ Fred joins the alpha mailing list but wants MIME digest delivery. ... }) content-length: 0 ... - location: http://localhost:8001/3.0/lists/alpha@example.com/member/fperson@example.com + location: http://localhost:9001/3.0/lists/alpha@example.com/member/fperson@example.com ... status: 201 @@ -258,7 +300,7 @@ Corner cases For some reason Elly tries to join a mailing list that does not exist. - >>> dump_json('http://localhost:8001/3.0/members', { + >>> dump_json('http://localhost:9001/3.0/members', { ... 'fqdn_listname': 'beta@example.com', ... 'address': 'eperson@example.com', ... 'real_name': 'Elly Person', @@ -269,7 +311,7 @@ For some reason Elly tries to join a mailing list that does not exist. Then, she tries to leave a mailing list that does not exist. - >>> dump_json('http://localhost:8001/3.0/lists/beta@example.com' + >>> dump_json('http://localhost:9001/3.0/lists/beta@example.com' ... '/members/eperson@example.com', ... method='DELETE') Traceback (most recent call last): @@ -278,7 +320,7 @@ Then, she tries to leave a mailing list that does not exist. She then tries to leave a mailing list with a bogus address. - >>> dump_json('http://localhost:8001/3.0/lists/alpha@example.com' + >>> dump_json('http://localhost:9001/3.0/lists/alpha@example.com' ... '/members/elly', ... method='DELETE') Traceback (most recent call last): @@ -288,7 +330,7 @@ She then tries to leave a mailing list with a bogus address. For some reason, Elly tries to leave the mailing list again, but she's already been unsubscribed. - >>> dump_json('http://localhost:8001/3.0/lists/alpha@example.com' + >>> dump_json('http://localhost:9001/3.0/lists/alpha@example.com' ... '/members/eperson@example.com', ... method='DELETE') Traceback (most recent call last): @@ -297,7 +339,7 @@ been unsubscribed. Anna tries to join a mailing list she's already a member of. - >>> dump_json('http://localhost:8001/3.0/members', { + >>> dump_json('http://localhost:9001/3.0/members', { ... 'fqdn_listname': 'alpha@example.com', ... 'address': 'aperson@example.com', ... }) @@ -307,7 +349,7 @@ Anna tries to join a mailing list she's already a member of. Gwen tries to join the alpha mailing list using an invalid delivery mode. - >>> dump_json('http://localhost:8001/3.0/members', { + >>> dump_json('http://localhost:9001/3.0/members', { ... 'fqdn_listname': 'alpha@example.com', ... 'address': 'gperson@example.com', ... 'real_name': 'Gwen Person', @@ -320,12 +362,12 @@ Gwen tries to join the alpha mailing list using an invalid delivery mode. Even using an address with "funny" characters Hugh can join the mailing list. >>> transaction.abort() - >>> dump_json('http://localhost:8001/3.0/members', { + >>> dump_json('http://localhost:9001/3.0/members', { ... 'fqdn_listname': 'alpha@example.com', ... 'address': 'hugh/person@example.com', ... 'real_name': 'Hugh Person', ... }) content-length: 0 date: ... - location: http://localhost:8001/3.0/lists/alpha@example.com/member/hugh%2Fperson@example.com + location: http://localhost:9001/3.0/lists/alpha@example.com/member/hugh%2Fperson@example.com ... diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py index 3fdc7dbeb..1b3023865 100644 --- a/src/mailman/rest/members.py +++ b/src/mailman/rest/members.py @@ -50,6 +50,8 @@ class _MemberBase(resource.Resource, CollectionMixin): """See `CollectionMixin`.""" enum, dot, role = str(member.role).partition('.') return dict( + fqdn_listname=member.mailing_list, + address=member.address.address, self_link=path_to('lists/{0}/{1}/{2}'.format( member.mailing_list, role, member.address.address)), ) diff --git a/src/mailman/rules/docs/moderation.txt b/src/mailman/rules/docs/moderation.txt index 73a6b111d..96ad8b6c3 100644 --- a/src/mailman/rules/docs/moderation.txt +++ b/src/mailman/rules/docs/moderation.txt @@ -8,9 +8,9 @@ email the list without having those messages be held for approval. The 'moderation' rule determines whether the message should be moderated or not. >>> mlist = create_list('_xtest@example.com') - >>> rule = config.rules['moderation'] + >>> rule = config.rules['member-moderation'] >>> print rule.name - moderation + member-moderation In the simplest case, the sender is not a member of the mailing list, so the moderation rule can't match. @@ -25,13 +25,14 @@ moderation rule can't match. False Let's add the message author as a non-moderated member. -:: >>> from mailman.interfaces.usermanager import IUserManager >>> from zope.component import getUtility >>> user = getUtility(IUserManager).create_user( ... 'aperson@example.org', 'Anne Person') +Because the member is not moderated, the rule does not match. + >>> address = list(user.addresses)[0] >>> from mailman.interfaces.member import MemberRole >>> member = address.subscribe(mlist, MemberRole.member) diff --git a/src/mailman/rules/docs/rules.txt b/src/mailman/rules/docs/rules.txt index 2d59203f8..056e39cab 100644 --- a/src/mailman/rules/docs/rules.txt +++ b/src/mailman/rules/docs/rules.txt @@ -26,7 +26,7 @@ names to rule objects. loop True max-recipients True max-size True - moderation True + member-moderation True news-moderation True no-subject True non-member True diff --git a/src/mailman/rules/moderation.py b/src/mailman/rules/moderation.py index 1e2b46529..4bf6ba1c8 100644 --- a/src/mailman/rules/moderation.py +++ b/src/mailman/rules/moderation.py @@ -21,8 +21,8 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ - 'Moderation', - 'NonMember', + 'MemberModeration', + 'NonMemberModeration', ] @@ -33,11 +33,11 @@ from mailman.interfaces.rules import IRule -class Moderation: +class MemberModeration: """The member moderation rule.""" implements(IRule) - name = 'moderation' + name = 'member-moderation' description = _('Match messages sent by moderated members.') record = True @@ -51,7 +51,7 @@ class Moderation: -class NonMember: +class NonMemberModeration: """The non-membership rule.""" implements(IRule) diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py index fbb2dfa8a..b26c93830 100644 --- a/src/mailman/testing/layers.py +++ b/src/mailman/testing/layers.py @@ -273,7 +273,7 @@ class RESTLayer(SMTPLayer): until = datetime.datetime.now() + TEST_TIMEOUT while datetime.datetime.now() < until: try: - request = Request('http://localhost:8001/3.0/system') + request = Request('http://localhost:9001/3.0/system') basic_auth = '{0}:{1}'.format(config.webservice.admin_user, config.webservice.admin_pass) request.add_header('Authorization', diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg index 8c56d2135..e3f642d71 100644 --- a/src/mailman/testing/testing.cfg +++ b/src/mailman/testing/testing.cfg @@ -19,8 +19,12 @@ [mta] smtp_port: 9025 +lmtp_port: 9024 incoming: mailman.testing.mta.FakeMTA +[webservice] +port: 9001 + [qrunner.archive] max_restarts: 1 diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py index f4ecd924c..f4b382714 100644 --- a/src/mailman/tests/test_documentation.py +++ b/src/mailman/tests/test_documentation.py @@ -151,6 +151,8 @@ def call_http(url, data=None, method=None, username=None, password=None): for header in sorted(response): print '{0}: {1}'.format(header, response[header]) return None + # XXX Workaround http://bugs.python.org/issue10038 + content = unicode(content) return json.loads(content) @@ -170,18 +172,18 @@ def dump_json(url, data=None, method=None, username=None, password=None): from the configuration. :type username: str """ - data = call_http(url, data, method, username, password) - if data is None: + results = call_http(url, data, method, username, password) + if results is None: return - for key in sorted(data): + for key in sorted(results): if key == 'entries': - for i, entry in enumerate(data[key]): + for i, entry in enumerate(results[key]): # entry is a dictionary. print 'entry %d:' % i for entry_key in sorted(entry): print ' {0}: {1}'.format(entry_key, entry[entry_key]) else: - print '{0}: {1}'.format(key, data[key]) + print '{0}: {1}'.format(key, results[key]) |
