summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/Archiver/HyperArch.py5
-rw-r--r--src/mailman/Archiver/HyperDatabase.py6
-rw-r--r--src/mailman/app/docs/chains.txt6
-rw-r--r--src/mailman/app/membership.py2
-rw-r--r--src/mailman/bin/arch.py2
-rw-r--r--src/mailman/bin/gate_news.py12
-rw-r--r--src/mailman/bin/master.py94
-rw-r--r--src/mailman/bin/tests/__init__.py0
-rw-r--r--src/mailman/bin/tests/test_master.py81
-rw-r--r--src/mailman/bin/update.py2
-rw-r--r--src/mailman/chains/base.py2
-rw-r--r--src/mailman/chains/builtin.py2
-rw-r--r--src/mailman/chains/docs/__init__.py0
-rw-r--r--src/mailman/chains/docs/moderation.txt177
-rw-r--r--src/mailman/chains/member.py72
-rw-r--r--src/mailman/commands/cli_control.py13
-rw-r--r--src/mailman/commands/cli_info.py4
-rw-r--r--src/mailman/commands/cli_status.py68
-rw-r--r--src/mailman/commands/docs/info.txt4
-rw-r--r--src/mailman/commands/docs/status.txt37
-rw-r--r--src/mailman/core/chains.py32
-rw-r--r--src/mailman/core/logging.py2
-rw-r--r--src/mailman/database/mailman.sql2
-rw-r--r--src/mailman/database/stock.py2
-rw-r--r--src/mailman/docs/NEWS.txt4
-rw-r--r--src/mailman/interfaces/mailinglist.py10
-rw-r--r--src/mailman/model/docs/membership.txt24
-rw-r--r--src/mailman/model/mailinglist.py2
-rw-r--r--src/mailman/model/member.py5
-rw-r--r--src/mailman/mta/postfix.py2
-rw-r--r--src/mailman/pipeline/scrubber.py2
-rw-r--r--src/mailman/queue/archive.py2
-rw-r--r--src/mailman/queue/docs/incoming.txt137
-rw-r--r--src/mailman/queue/docs/rest.txt4
-rw-r--r--src/mailman/rest/configuration.py3
-rw-r--r--src/mailman/rest/docs/basic.txt8
-rw-r--r--src/mailman/rest/docs/configuration.txt24
-rw-r--r--src/mailman/rest/docs/domains.txt36
-rw-r--r--src/mailman/rest/docs/helpers.txt2
-rw-r--r--src/mailman/rest/docs/lists.txt24
-rw-r--r--src/mailman/rest/docs/membership.txt128
-rw-r--r--src/mailman/rest/members.py2
-rw-r--r--src/mailman/rules/docs/moderation.txt7
-rw-r--r--src/mailman/rules/docs/rules.txt2
-rw-r--r--src/mailman/rules/moderation.py10
-rw-r--r--src/mailman/testing/layers.py2
-rw-r--r--src/mailman/testing/testing.cfg4
-rw-r--r--src/mailman/tests/test_documentation.py12
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])