summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2012-03-23 19:25:27 -0400
committerBarry Warsaw2012-03-23 19:25:27 -0400
commit25a392bf5c1a8d4d2bc63d51697350fc7dbd48bc (patch)
treef51fe7931545e23058cdb65595c7caed9b43bb0e /src
parentaa2d0ad067adfd2515ed3c256cd0bca296058479 (diff)
parente005e1b12fa0bd82d2e126df476b5505b440ce36 (diff)
downloadmailman-25a392bf5c1a8d4d2bc63d51697350fc7dbd48bc.tar.gz
mailman-25a392bf5c1a8d4d2bc63d51697350fc7dbd48bc.tar.zst
mailman-25a392bf5c1a8d4d2bc63d51697350fc7dbd48bc.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/chains/accept.py3
-rw-r--r--src/mailman/chains/owner.py56
-rw-r--r--src/mailman/chains/tests/test_owner.py75
-rw-r--r--src/mailman/core/pipelines.py33
-rw-r--r--src/mailman/core/tests/test_pipelines.py46
-rw-r--r--src/mailman/database/schema/postgres.sql2
-rw-r--r--src/mailman/database/schema/sqlite.sql2
-rw-r--r--src/mailman/docs/NEWS.rst2
-rw-r--r--src/mailman/handlers/__init__.py (renamed from src/mailman/pipeline/__init__.py)0
-rw-r--r--src/mailman/handlers/acknowledge.py (renamed from src/mailman/pipeline/acknowledge.py)0
-rw-r--r--src/mailman/handlers/after_delivery.py (renamed from src/mailman/pipeline/after_delivery.py)0
-rw-r--r--src/mailman/handlers/avoid_duplicates.py (renamed from src/mailman/pipeline/avoid_duplicates.py)0
-rw-r--r--src/mailman/handlers/cleanse.py (renamed from src/mailman/pipeline/cleanse.py)2
-rw-r--r--src/mailman/handlers/cleanse_dkim.py (renamed from src/mailman/pipeline/cleanse_dkim.py)0
-rw-r--r--src/mailman/handlers/cook_headers.py (renamed from src/mailman/pipeline/cook_headers.py)0
-rw-r--r--src/mailman/handlers/decorate.py (renamed from src/mailman/pipeline/decorate.py)0
-rw-r--r--src/mailman/handlers/docs/ack-headers.rst (renamed from src/mailman/pipeline/docs/ack-headers.rst)2
-rw-r--r--src/mailman/handlers/docs/acknowledge.rst (renamed from src/mailman/pipeline/docs/acknowledge.rst)0
-rw-r--r--src/mailman/handlers/docs/after-delivery.rst (renamed from src/mailman/pipeline/docs/after-delivery.rst)0
-rw-r--r--src/mailman/handlers/docs/archives.rst (renamed from src/mailman/pipeline/docs/archives.rst)0
-rw-r--r--src/mailman/handlers/docs/avoid-duplicates.rst (renamed from src/mailman/pipeline/docs/avoid-duplicates.rst)0
-rw-r--r--src/mailman/handlers/docs/cleanse.rst (renamed from src/mailman/pipeline/docs/cleanse.rst)0
-rw-r--r--src/mailman/handlers/docs/cook-headers.rst (renamed from src/mailman/pipeline/docs/cook-headers.rst)2
-rw-r--r--src/mailman/handlers/docs/decorate.rst (renamed from src/mailman/pipeline/docs/decorate.rst)2
-rw-r--r--src/mailman/handlers/docs/digests.rst (renamed from src/mailman/pipeline/docs/digests.rst)0
-rw-r--r--src/mailman/handlers/docs/file-recips.rst (renamed from src/mailman/pipeline/docs/file-recips.rst)0
-rw-r--r--src/mailman/handlers/docs/filtering.rst (renamed from src/mailman/pipeline/docs/filtering.rst)0
-rw-r--r--src/mailman/handlers/docs/member-recips.rst (renamed from src/mailman/pipeline/docs/calc-recips.rst)35
-rw-r--r--src/mailman/handlers/docs/nntp.rst (renamed from src/mailman/pipeline/docs/nntp.rst)0
-rw-r--r--src/mailman/handlers/docs/owner-recips.rst63
-rw-r--r--src/mailman/handlers/docs/reply-to.rst (renamed from src/mailman/pipeline/docs/reply-to.rst)2
-rw-r--r--src/mailman/handlers/docs/replybot.rst (renamed from src/mailman/pipeline/docs/replybot.rst)0
-rw-r--r--src/mailman/handlers/docs/rfc-2369.rst (renamed from src/mailman/pipeline/docs/rfc-2369.rst)2
-rw-r--r--src/mailman/handlers/docs/subject-munging.rst (renamed from src/mailman/pipeline/docs/subject-munging.rst)2
-rw-r--r--src/mailman/handlers/docs/tagger.rst (renamed from src/mailman/pipeline/docs/tagger.rst)2
-rw-r--r--src/mailman/handlers/docs/to-outgoing.rst (renamed from src/mailman/pipeline/docs/to-outgoing.rst)0
-rw-r--r--src/mailman/handlers/file_recipients.py (renamed from src/mailman/pipeline/file_recipients.py)0
-rw-r--r--src/mailman/handlers/member_recipients.py (renamed from src/mailman/pipeline/calculate_recipients.py)14
-rw-r--r--src/mailman/handlers/mime_delete.py (renamed from src/mailman/pipeline/mime_delete.py)0
-rw-r--r--src/mailman/handlers/owner_recipients.py67
-rw-r--r--src/mailman/handlers/replybot.py (renamed from src/mailman/pipeline/replybot.py)0
-rw-r--r--src/mailman/handlers/rfc_2369.py (renamed from src/mailman/pipeline/rfc_2369.py)3
-rw-r--r--src/mailman/handlers/tagger.py (renamed from src/mailman/pipeline/tagger.py)0
-rw-r--r--src/mailman/handlers/tests/__init__.py (renamed from src/mailman/pipeline/tests/__init__.py)0
-rw-r--r--src/mailman/handlers/tests/test_mimedel.py (renamed from src/mailman/pipeline/tests/test_mimedel.py)2
-rw-r--r--src/mailman/handlers/tests/test_recipients.py200
-rw-r--r--src/mailman/handlers/to_archive.py (renamed from src/mailman/pipeline/to_archive.py)0
-rw-r--r--src/mailman/handlers/to_digest.py (renamed from src/mailman/pipeline/to_digest.py)0
-rw-r--r--src/mailman/handlers/to_outgoing.py (renamed from src/mailman/pipeline/to_outgoing.py)0
-rw-r--r--src/mailman/handlers/to_usenet.py (renamed from src/mailman/pipeline/to_usenet.py)0
-rw-r--r--src/mailman/interfaces/mailinglist.py20
-rw-r--r--src/mailman/model/docs/requests.rst8
-rw-r--r--src/mailman/model/mailinglist.py2
-rw-r--r--src/mailman/pipeline/owner_recipients.py35
-rw-r--r--src/mailman/runners/digest.py3
-rw-r--r--src/mailman/runners/docs/lmtp.rst2
-rw-r--r--src/mailman/runners/docs/outgoing.rst4
-rw-r--r--src/mailman/runners/incoming.py5
-rw-r--r--src/mailman/runners/pipeline.py5
-rw-r--r--src/mailman/runners/tests/test_incoming.py94
-rw-r--r--src/mailman/runners/tests/test_owner.py142
-rw-r--r--src/mailman/runners/tests/test_pipeline.py115
-rw-r--r--src/mailman/styles/default.py5
-rw-r--r--src/mailman/testing/layers.py8
-rw-r--r--src/mailman/testing/testing.cfg3
65 files changed, 969 insertions, 101 deletions
diff --git a/src/mailman/chains/accept.py b/src/mailman/chains/accept.py
index b1e4b1cf0..4b326142b 100644
--- a/src/mailman/chains/accept.py
+++ b/src/mailman/chains/accept.py
@@ -62,7 +62,6 @@ class AcceptChain(TerminalChainBase):
rule_misses = msgdata.get('rule_misses')
if rule_misses:
msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
- accept_queue = config.switchboards['pipeline']
- accept_queue.enqueue(msg, msgdata)
+ config.switchboards['pipeline'].enqueue(msg, msgdata)
log.info('ACCEPT: %s', msg.get('message-id', 'n/a'))
notify(AcceptNotification(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/owner.py b/src/mailman/chains/owner.py
new file mode 100644
index 000000000..ad0a04cea
--- /dev/null
+++ b/src/mailman/chains/owner.py
@@ -0,0 +1,56 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""The standard -owner posting chain."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'BuiltInOwnerChain',
+ ]
+
+
+import logging
+
+from zope.event import notify
+
+from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.config import config
+from mailman.core.i18n import _
+
+
+log = logging.getLogger('mailman.vette')
+
+
+
+class OwnerNotification(ChainNotification):
+ """An event signaling that a message is accepted to the -owner address."""
+
+
+
+class BuiltInOwnerChain(TerminalChainBase):
+ """Default built-in -owner address chain."""
+
+ name = 'default-owner-chain'
+ description = _('The built-in -owner posting chain.')
+
+ def _process(self, mlist, msg, msgdata):
+ # At least for now, everything posted to -owners goes through.
+ config.switchboards['pipeline'].enqueue(msg, msgdata)
+ log.info('OWNER: %s', msg.get('message-id', 'n/a'))
+ notify(OwnerNotification(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/tests/test_owner.py b/src/mailman/chains/tests/test_owner.py
new file mode 100644
index 000000000..db85d4967
--- /dev/null
+++ b/src/mailman/chains/tests/test_owner.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test the owner chain."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestOwnerChain',
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.chains.owner import BuiltInOwnerChain, OwnerNotification
+from mailman.core.chains import process
+from mailman.testing.helpers import (
+ event_subscribers,
+ get_queue_messages,
+ specialized_message_from_string as mfs)
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestOwnerChain(unittest.TestCase):
+ """Test the owner chain."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+Message-ID: <ant>
+
+""")
+
+ def test_owner_pipeline(self):
+ # Messages processed through the default owners chain end up in the
+ # pipeline queue, and an event gets sent.
+ #
+ # This event subscriber records the event that occurs when the message
+ # is processed by the owner chain.
+ events = []
+ def catch_event(event):
+ events.append(event)
+ with event_subscribers(catch_event):
+ process(self._mlist, self._msg, {}, 'default-owner-chain')
+ self.assertEqual(len(events), 1)
+ event = events[0]
+ self.assertTrue(isinstance(event, OwnerNotification))
+ self.assertEqual(event.mlist, self._mlist)
+ self.assertEqual(event.msg['message-id'], '<ant>')
+ self.assertTrue(isinstance(event.chain, BuiltInOwnerChain))
+ messages = get_queue_messages('pipeline')
+ self.assertEqual(len(messages), 1)
+ message = messages[0].msg
+ self.assertEqual(message['message-id'], '<ant>')
diff --git a/src/mailman/core/pipelines.py b/src/mailman/core/pipelines.py
index d5cee588b..25bb68030 100644
--- a/src/mailman/core/pipelines.py
+++ b/src/mailman/core/pipelines.py
@@ -15,12 +15,16 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Pipeline processor."""
+"""Built-in pipelines."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'BasePipeline',
+ 'OwnerPipeline',
+ 'PostingPipeline',
+ 'VirginPipeline',
'initialize',
'process',
]
@@ -89,16 +93,29 @@ class BasePipeline:
yield handler
-class BuiltInPipeline(BasePipeline):
- """The built-in pipeline."""
+
+class OwnerPipeline(BasePipeline):
+ """The built-in owner pipeline."""
+
+ name = 'default-owner-pipeline'
+ description = _('The built-in owner pipeline.')
+
+ _default_handlers = (
+ 'owner-recipients',
+ 'to-outgoing',
+ )
+
+
+class PostingPipeline(BasePipeline):
+ """The built-in posting pipeline."""
name = 'default-posting-pipeline'
- description = _('The built-in pipeline.')
+ description = _('The built-in posting pipeline.')
_default_handlers = (
'mime-delete',
'tagger',
- 'calculate-recipients',
+ 'member-recipients',
'avoid-duplicates',
'cleanse',
'cleanse-dkim',
@@ -131,7 +148,7 @@ class VirginPipeline(BasePipeline):
def initialize():
"""Initialize the pipelines."""
# Find all handlers in the registered plugins.
- for handler_class in find_components('mailman.pipeline', IHandler):
+ for handler_class in find_components('mailman.handlers', IHandler):
handler = handler_class()
verifyObject(IHandler, handler)
assert handler.name not in config.handlers, (
@@ -139,6 +156,6 @@ def initialize():
handler.name, handler_class))
config.handlers[handler.name] = handler
# Set up some pipelines.
- for pipeline_class in (BuiltInPipeline, VirginPipeline):
+ for pipeline_class in (OwnerPipeline, PostingPipeline, VirginPipeline):
pipeline = pipeline_class()
config.pipelines[pipeline.name] = pipeline
diff --git a/src/mailman/core/tests/test_pipelines.py b/src/mailman/core/tests/test_pipelines.py
index 0cf3732c9..8f851de95 100644
--- a/src/mailman/core/tests/test_pipelines.py
+++ b/src/mailman/core/tests/test_pipelines.py
@@ -21,11 +21,14 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestOwnerPipeline',
+ 'TestPostingPipeline',
]
import unittest
+from zope.component import getUtility
from zope.interface import implements
from mailman.app.lifecycle import create_list
@@ -33,7 +36,9 @@ from mailman.config import config
from mailman.core.errors import DiscardMessage, RejectMessage
from mailman.core.pipelines import process
from mailman.interfaces.handler import IHandler
+from mailman.interfaces.member import MemberRole
from mailman.interfaces.pipeline import IPipeline
+from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
LogFileMark,
get_queue_messages,
@@ -78,7 +83,7 @@ class RejectingPipeline:
-class TestBuiltinPipeline(unittest.TestCase):
+class TestPostingPipeline(unittest.TestCase):
"""Test various aspects of the built-in postings pipeline."""
layer = ConfigLayer
@@ -133,3 +138,42 @@ testing
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0].msg['subject']), 'a test')
+
+
+
+class TestOwnerPipeline(unittest.TestCase):
+ """Test various aspects of the built-in owner pipeline."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ user_manager = getUtility(IUserManager)
+ anne = user_manager.create_address('anne@example.com')
+ bart = user_manager.create_address('bart@example.com')
+ self._mlist.subscribe(anne, MemberRole.owner)
+ self._mlist.subscribe(bart, MemberRole.moderator)
+ self._msg = mfs("""\
+From: Anne Person <anne@example.org>
+To: test-owner@example.com
+
+""")
+
+ def test_calculate_recipients(self):
+ # Recipients are the administrators of the mailing list.
+ msgdata = dict(listname='test@example.com',
+ to_owner=True)
+ process(self._mlist, self._msg, msgdata,
+ pipeline_name='default-owner-pipeline')
+ self.assertEqual(msgdata['recipients'], set(('anne@example.com',
+ 'bart@example.com')))
+
+ def test_to_outgoing(self):
+ # The message, with the calculated recipients, gets put in the
+ # outgoing queue.
+ process(self._mlist, self._msg, {},
+ pipeline_name='default-owner-pipeline')
+ messages = get_queue_messages('out', sort_on='to')
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0].msgdata['recipients'],
+ set(('anne@example.com', 'bart@example.com')))
diff --git a/src/mailman/database/schema/postgres.sql b/src/mailman/database/schema/postgres.sql
index bd7ef3f6b..2e9ba249f 100644
--- a/src/mailman/database/schema/postgres.sql
+++ b/src/mailman/database/schema/postgres.sql
@@ -84,6 +84,8 @@ CREATE TABLE mailinglist (
nondigestable BOOLEAN,
nonmember_rejection_notice TEXT,
obscure_addresses BOOLEAN,
+ owner_chain TEXT,
+ owner_pipeline TEXT,
personalize INTEGER,
post_id INTEGER,
posting_chain TEXT,
diff --git a/src/mailman/database/schema/sqlite.sql b/src/mailman/database/schema/sqlite.sql
index 37b6ed8f2..e6211bf53 100644
--- a/src/mailman/database/schema/sqlite.sql
+++ b/src/mailman/database/schema/sqlite.sql
@@ -180,6 +180,8 @@ CREATE TABLE mailinglist (
nondigestable BOOLEAN,
nonmember_rejection_notice TEXT,
obscure_addresses BOOLEAN,
+ owner_chain TEXT,
+ owner_pipeline TEXT,
personalize INTEGER,
post_id INTEGER,
posting_chain TEXT,
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index b92a3b618..fa5d1cb90 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -75,6 +75,8 @@ Database
- real_name -> display_name (mailinglist, user, address)
* Schema additions:
- mailinglist.filter_action
+ - mailinglist.owner_chain
+ - mailinglist.owner_pipeline
REST
----
diff --git a/src/mailman/pipeline/__init__.py b/src/mailman/handlers/__init__.py
index e69de29bb..e69de29bb 100644
--- a/src/mailman/pipeline/__init__.py
+++ b/src/mailman/handlers/__init__.py
diff --git a/src/mailman/pipeline/acknowledge.py b/src/mailman/handlers/acknowledge.py
index 0e0916337..0e0916337 100644
--- a/src/mailman/pipeline/acknowledge.py
+++ b/src/mailman/handlers/acknowledge.py
diff --git a/src/mailman/pipeline/after_delivery.py b/src/mailman/handlers/after_delivery.py
index 46007092b..46007092b 100644
--- a/src/mailman/pipeline/after_delivery.py
+++ b/src/mailman/handlers/after_delivery.py
diff --git a/src/mailman/pipeline/avoid_duplicates.py b/src/mailman/handlers/avoid_duplicates.py
index ffbc80c85..ffbc80c85 100644
--- a/src/mailman/pipeline/avoid_duplicates.py
+++ b/src/mailman/handlers/avoid_duplicates.py
diff --git a/src/mailman/pipeline/cleanse.py b/src/mailman/handlers/cleanse.py
index 90f2a892a..605b843d0 100644
--- a/src/mailman/pipeline/cleanse.py
+++ b/src/mailman/handlers/cleanse.py
@@ -31,8 +31,8 @@ from email.utils import formataddr
from zope.interface import implements
from mailman.core.i18n import _
+from mailman.handlers.cook_headers import uheader
from mailman.interfaces.handler import IHandler
-from mailman.pipeline.cook_headers import uheader
log = logging.getLogger('mailman.post')
diff --git a/src/mailman/pipeline/cleanse_dkim.py b/src/mailman/handlers/cleanse_dkim.py
index d2cd32636..d2cd32636 100644
--- a/src/mailman/pipeline/cleanse_dkim.py
+++ b/src/mailman/handlers/cleanse_dkim.py
diff --git a/src/mailman/pipeline/cook_headers.py b/src/mailman/handlers/cook_headers.py
index 2d117429c..2d117429c 100644
--- a/src/mailman/pipeline/cook_headers.py
+++ b/src/mailman/handlers/cook_headers.py
diff --git a/src/mailman/pipeline/decorate.py b/src/mailman/handlers/decorate.py
index d6d156048..d6d156048 100644
--- a/src/mailman/pipeline/decorate.py
+++ b/src/mailman/handlers/decorate.py
diff --git a/src/mailman/pipeline/docs/ack-headers.rst b/src/mailman/handlers/docs/ack-headers.rst
index dba2169e2..e700e2fd1 100644
--- a/src/mailman/pipeline/docs/ack-headers.rst
+++ b/src/mailman/handlers/docs/ack-headers.rst
@@ -21,7 +21,7 @@ added.
... A message of great import.
... """)
- >>> from mailman.pipeline.cook_headers import process
+ >>> from mailman.handlers.cook_headers import process
>>> process(mlist, msg, dict(noack=True))
>>> print msg.as_string()
From: aperson@example.com
diff --git a/src/mailman/pipeline/docs/acknowledge.rst b/src/mailman/handlers/docs/acknowledge.rst
index 479aa4ea6..479aa4ea6 100644
--- a/src/mailman/pipeline/docs/acknowledge.rst
+++ b/src/mailman/handlers/docs/acknowledge.rst
diff --git a/src/mailman/pipeline/docs/after-delivery.rst b/src/mailman/handlers/docs/after-delivery.rst
index c3e393cf2..c3e393cf2 100644
--- a/src/mailman/pipeline/docs/after-delivery.rst
+++ b/src/mailman/handlers/docs/after-delivery.rst
diff --git a/src/mailman/pipeline/docs/archives.rst b/src/mailman/handlers/docs/archives.rst
index 323d121e8..323d121e8 100644
--- a/src/mailman/pipeline/docs/archives.rst
+++ b/src/mailman/handlers/docs/archives.rst
diff --git a/src/mailman/pipeline/docs/avoid-duplicates.rst b/src/mailman/handlers/docs/avoid-duplicates.rst
index 1e46793c2..1e46793c2 100644
--- a/src/mailman/pipeline/docs/avoid-duplicates.rst
+++ b/src/mailman/handlers/docs/avoid-duplicates.rst
diff --git a/src/mailman/pipeline/docs/cleanse.rst b/src/mailman/handlers/docs/cleanse.rst
index 61dfa8f52..61dfa8f52 100644
--- a/src/mailman/pipeline/docs/cleanse.rst
+++ b/src/mailman/handlers/docs/cleanse.rst
diff --git a/src/mailman/pipeline/docs/cook-headers.rst b/src/mailman/handlers/docs/cook-headers.rst
index e0313f53a..948628d54 100644
--- a/src/mailman/pipeline/docs/cook-headers.rst
+++ b/src/mailman/handlers/docs/cook-headers.rst
@@ -26,7 +26,7 @@ will place the sender in the message metadata for safe keeping.
... """)
>>> msgdata = {}
- >>> from mailman.pipeline.cook_headers import process
+ >>> from mailman.handlers.cook_headers import process
>>> process(mlist, msg, msgdata)
>>> print msgdata['original_sender']
aperson@example.com
diff --git a/src/mailman/pipeline/docs/decorate.rst b/src/mailman/handlers/docs/decorate.rst
index 6fa8212ac..eae8ea904 100644
--- a/src/mailman/pipeline/docs/decorate.rst
+++ b/src/mailman/handlers/docs/decorate.rst
@@ -22,7 +22,7 @@ Digest messages get decorated during the digest creation phase so no extra
decorations are added for digest messages.
::
- >>> from mailman.pipeline.decorate import process
+ >>> from mailman.handlers.decorate import process
>>> process(mlist, msg, dict(isdigest=True))
>>> print msg.as_string()
From: aperson@example.org
diff --git a/src/mailman/pipeline/docs/digests.rst b/src/mailman/handlers/docs/digests.rst
index d4d563180..d4d563180 100644
--- a/src/mailman/pipeline/docs/digests.rst
+++ b/src/mailman/handlers/docs/digests.rst
diff --git a/src/mailman/pipeline/docs/file-recips.rst b/src/mailman/handlers/docs/file-recips.rst
index 7d157ccc5..7d157ccc5 100644
--- a/src/mailman/pipeline/docs/file-recips.rst
+++ b/src/mailman/handlers/docs/file-recips.rst
diff --git a/src/mailman/pipeline/docs/filtering.rst b/src/mailman/handlers/docs/filtering.rst
index fd0b33d3b..fd0b33d3b 100644
--- a/src/mailman/pipeline/docs/filtering.rst
+++ b/src/mailman/handlers/docs/filtering.rst
diff --git a/src/mailman/pipeline/docs/calc-recips.rst b/src/mailman/handlers/docs/member-recips.rst
index 6dca85816..1439e978f 100644
--- a/src/mailman/pipeline/docs/calc-recips.rst
+++ b/src/mailman/handlers/docs/member-recips.rst
@@ -6,10 +6,10 @@ Every message that makes it through to the list membership gets sent to a set
of recipient addresses. These addresses are calculated by one of the handler
modules and depends on a host of factors.
- >>> mlist = create_list('_xtest@example.com')
+ >>> mlist = create_list('test@example.com')
-Recipients are calculate from the list members, so add a bunch of members to
-start out with. First, create a bunch of addresses...
+Recipients are calculate from the list membership, so first some people
+subscribe to the mailing list...
::
>>> from mailman.interfaces.usermanager import IUserManager
@@ -35,42 +35,25 @@ start out with. First, create a bunch of addresses...
...then make some of the members digest members.
- >>> from mailman.core.constants import DeliveryMode
+ >>> from mailman.interfaces.member import DeliveryMode
>>> member_d.preferences.delivery_mode = DeliveryMode.plaintext_digests
>>> member_e.preferences.delivery_mode = DeliveryMode.mime_digests
>>> member_f.preferences.delivery_mode = DeliveryMode.summary_digests
-Short-circuiting
-================
+Regular delivery recipients
+===========================
-Sometimes, the list of recipients already exists in the message metadata.
-This can happen for example, when a message was previously delivered to some
-but not all of the recipients.
-::
+Regular delivery recipients are those people who get messages from the list as
+soon as they are posted. In other words, these folks are not digest members.
>>> msg = message_from_string("""\
... From: Xavier Person <xperson@example.com>
...
... Something of great import.
... """)
- >>> recipients = set(('qperson@example.com', 'zperson@example.com'))
- >>> msgdata = dict(recipients=recipients)
-
- >>> handler = config.handlers['calculate-recipients']
- >>> handler.process(mlist, msg, msgdata)
- >>> dump_list(msgdata['recipients'])
- qperson@example.com
- zperson@example.com
-
-
-Regular delivery recipients
-===========================
-
-Regular delivery recipients are those people who get messages from the list as
-soon as they are posted. In other words, these folks are not digest members.
-
>>> msgdata = {}
+ >>> handler = config.handlers['member-recipients']
>>> handler.process(mlist, msg, msgdata)
>>> dump_list(msgdata['recipients'])
aperson@example.com
diff --git a/src/mailman/pipeline/docs/nntp.rst b/src/mailman/handlers/docs/nntp.rst
index 874712397..874712397 100644
--- a/src/mailman/pipeline/docs/nntp.rst
+++ b/src/mailman/handlers/docs/nntp.rst
diff --git a/src/mailman/handlers/docs/owner-recips.rst b/src/mailman/handlers/docs/owner-recips.rst
new file mode 100644
index 000000000..e62551ba6
--- /dev/null
+++ b/src/mailman/handlers/docs/owner-recips.rst
@@ -0,0 +1,63 @@
+=====================
+List owner recipients
+=====================
+
+When a message is posted to a mailing list's `-owners` address, all of the
+list's administrators will receive a copy. The administrators are defined as
+the set of owners and moderators.
+
+ >>> mlist_1 = create_list('alpha@example.com')
+
+Anne is the owner of the list and Bart is a moderator of the list.
+
+ >>> from mailman.interfaces.usermanager import IUserManager
+ >>> from zope.component import getUtility
+ >>> user_manager = getUtility(IUserManager)
+ >>> anne_addr = user_manager.create_address('anne@example.com')
+ >>> bart_addr = user_manager.create_address('bart@example.com')
+ >>> from mailman.interfaces.member import MemberRole
+ >>> anne = mlist_1.subscribe(anne_addr, MemberRole.owner)
+ >>> bart = mlist_1.subscribe(bart_addr, MemberRole.moderator)
+
+The recipients list for the `-owners` address includes both Anne and Bart.
+
+ >>> msg = message_from_string("""\
+ ... From: Xavier Person <xperson@example.com>
+ ... To: alpha@example.com
+ ...
+ ... """)
+ >>> msgdata = {}
+ >>> handler = config.handlers['owner-recipients']
+ >>> handler.process(mlist_1, msg, msgdata)
+ >>> dump_list(msgdata['recipients'])
+ anne@example.com
+ bart@example.com
+
+Anne disables her owner delivery, so she will not receive `-owner` emails.
+
+ >>> from mailman.interfaces.member import DeliveryStatus
+ >>> anne.preferences.delivery_status = DeliveryStatus.by_user
+ >>> msgdata = {}
+ >>> handler.process(mlist_1, msg, msgdata)
+ >>> dump_list(msgdata['recipients'])
+ bart@example.com
+
+If Bart also disables his owner delivery, then no one could contact the list's
+owners. Since this is unacceptable, the site owner is used as a fallback.
+
+ >>> bart.preferences.delivery_status = DeliveryStatus.by_user
+ >>> msgdata = {}
+ >>> handler.process(mlist_1, msg, msgdata)
+ >>> dump_list(msgdata['recipients'])
+ noreply@example.com
+
+For mailing lists which have no owners at all, the site owner is also used as
+a fallback.
+
+ >>> mlist_2 = create_list('beta@example.com')
+ >>> mlist_2.administrators.member_count
+ 0
+ >>> msgdata = {}
+ >>> handler.process(mlist_2, msg, msgdata)
+ >>> dump_list(msgdata['recipients'])
+ noreply@example.com
diff --git a/src/mailman/pipeline/docs/reply-to.rst b/src/mailman/handlers/docs/reply-to.rst
index e08fea81d..d421e2dc5 100644
--- a/src/mailman/pipeline/docs/reply-to.rst
+++ b/src/mailman/handlers/docs/reply-to.rst
@@ -46,7 +46,7 @@ original message, the list's posting address simply gets inserted.
...
... """)
- >>> from mailman.pipeline.cook_headers import process
+ >>> from mailman.handlers.cook_headers import process
>>> process(mlist, msg, {})
>>> len(msg.get_all('reply-to'))
1
diff --git a/src/mailman/pipeline/docs/replybot.rst b/src/mailman/handlers/docs/replybot.rst
index 7cdd7c928..7cdd7c928 100644
--- a/src/mailman/pipeline/docs/replybot.rst
+++ b/src/mailman/handlers/docs/replybot.rst
diff --git a/src/mailman/pipeline/docs/rfc-2369.rst b/src/mailman/handlers/docs/rfc-2369.rst
index 1b89f2354..0461f27ba 100644
--- a/src/mailman/pipeline/docs/rfc-2369.rst
+++ b/src/mailman/handlers/docs/rfc-2369.rst
@@ -28,7 +28,7 @@ headers generally start with the `List-` prefix.
The `rfc-2369` handler adds the `List-` headers. `List-Id` is always added.
- >>> from mailman.pipeline.rfc_2369 import process
+ >>> from mailman.handlers.rfc_2369 import process
>>> msg = message_from_string("""\
... From: aperson@example.com
...
diff --git a/src/mailman/pipeline/docs/subject-munging.rst b/src/mailman/handlers/docs/subject-munging.rst
index e7a6553ce..48cee8e2b 100644
--- a/src/mailman/pipeline/docs/subject-munging.rst
+++ b/src/mailman/handlers/docs/subject-munging.rst
@@ -29,7 +29,7 @@ subject munging, a mailing list must have a preferred language.
... """)
>>> msgdata = {}
- >>> from mailman.pipeline.cook_headers import process
+ >>> from mailman.handlers.cook_headers import process
>>> process(mlist, msg, msgdata)
The original subject header is stored in the message metadata. We must print
diff --git a/src/mailman/pipeline/docs/tagger.rst b/src/mailman/handlers/docs/tagger.rst
index 80e682119..b64b05c54 100644
--- a/src/mailman/pipeline/docs/tagger.rst
+++ b/src/mailman/handlers/docs/tagger.rst
@@ -26,7 +26,7 @@ are defined.
... """)
>>> msgdata = {}
- >>> from mailman.pipeline.tagger import process
+ >>> from mailman.handlers.tagger import process
>>> process(mlist, msg, msgdata)
>>> print msg.as_string()
Subject: foobar
diff --git a/src/mailman/pipeline/docs/to-outgoing.rst b/src/mailman/handlers/docs/to-outgoing.rst
index 816aa4ca6..816aa4ca6 100644
--- a/src/mailman/pipeline/docs/to-outgoing.rst
+++ b/src/mailman/handlers/docs/to-outgoing.rst
diff --git a/src/mailman/pipeline/file_recipients.py b/src/mailman/handlers/file_recipients.py
index d087ff2bb..d087ff2bb 100644
--- a/src/mailman/pipeline/file_recipients.py
+++ b/src/mailman/handlers/file_recipients.py
diff --git a/src/mailman/pipeline/calculate_recipients.py b/src/mailman/handlers/member_recipients.py
index 15be07ecd..956ea6adc 100644
--- a/src/mailman/pipeline/calculate_recipients.py
+++ b/src/mailman/handlers/member_recipients.py
@@ -23,13 +23,14 @@ on the `recipients' attribute of the message. This attribute is used by the
SendmailDeliver and BulkDeliver modules.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
- 'CalculateRecipients',
+ 'MemberRecipients',
]
+
from zope.interface import implements
from mailman.config import config
@@ -41,12 +42,12 @@ from mailman.utilities.string import wrap
-class CalculateRecipients:
+class MemberRecipients:
"""Calculate the regular (i.e. non-digest) recipients of the message."""
implements(IHandler)
- name = 'calculate-recipients'
+ name = 'member-recipients'
description = _('Calculate the regular recipients of the message.')
def process(self, mlist, msg, msgdata):
@@ -82,10 +83,9 @@ class CalculateRecipients:
# Bad Urgent: password, so reject it instead of passing it on.
# I think it's better that the sender know they screwed up
# than to deliver it normally.
- listname = mlist.display_name
text = _("""\
-Your urgent message to the $listname mailing list was not authorized for
-delivery. The original message as received by Mailman is attached.
+Your urgent message to the $mlist.display_name mailing list was not authorized
+for delivery. The original message as received by Mailman is attached.
""")
raise errors.RejectMessage(wrap(text))
# Calculate the regular recipients of the message
diff --git a/src/mailman/pipeline/mime_delete.py b/src/mailman/handlers/mime_delete.py
index c9c1eb408..c9c1eb408 100644
--- a/src/mailman/pipeline/mime_delete.py
+++ b/src/mailman/handlers/mime_delete.py
diff --git a/src/mailman/handlers/owner_recipients.py b/src/mailman/handlers/owner_recipients.py
new file mode 100644
index 000000000..e431d00cf
--- /dev/null
+++ b/src/mailman/handlers/owner_recipients.py
@@ -0,0 +1,67 @@
+# Copyright (C) 2001-2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Calculate the list owner recipients (includes moderators)."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'OwnerRecipients',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.config import config
+from mailman.core.i18n import _
+from mailman.interfaces.handler import IHandler
+from mailman.interfaces.member import DeliveryStatus
+
+
+
+class OwnerRecipients:
+ """Calculate the owner (and moderator) recipients for -owner postings."""
+
+ implements(IHandler)
+
+ name = 'owner-recipients'
+ description = _('Calculate the owner and moderator recipients.')
+
+ def process(self, mlist, msg, msgdata):
+ """See `IHandler`."""
+ # Short circuit if we've already calculated the recipients list,
+ # regardless of whether the list is empty or not.
+ if 'recipients' in msgdata:
+ return
+ # -owner messages go to both the owners and moderators, which is most
+ # conveniently accessed via the administrators roster.
+ recipients = set(admin.address.email
+ for admin in mlist.administrators.members
+ if admin.delivery_status == DeliveryStatus.enabled)
+ # To prevent -owner messages from going into a black hole, if there
+ # are no administrators available, the message goes to the site owner.
+ if len(recipients) == 0:
+ msgdata['recipients'] = set((config.mailman.site_owner,))
+ else:
+ msgdata['recipients'] = recipients
+ # Don't decorate these messages with the header/footers. Eventually
+ # we should support unique decorations for owner emails.
+ msgdata['nodecorate'] = True
+ # We should probably always VERP deliveries to the owners. We
+ # *really* want to know if they are bouncing.
+ msgdata['verp'] = True
diff --git a/src/mailman/pipeline/replybot.py b/src/mailman/handlers/replybot.py
index 83aa40214..83aa40214 100644
--- a/src/mailman/pipeline/replybot.py
+++ b/src/mailman/handlers/replybot.py
diff --git a/src/mailman/pipeline/rfc_2369.py b/src/mailman/handlers/rfc_2369.py
index 26bfe094c..ece4e83cb 100644
--- a/src/mailman/pipeline/rfc_2369.py
+++ b/src/mailman/handlers/rfc_2369.py
@@ -30,8 +30,9 @@ from zope.interface import implements
from mailman.config import config
from mailman.core.i18n import _
+from mailman.handlers.cook_headers import uheader
from mailman.interfaces.handler import IHandler
-from mailman.pipeline.cook_headers import uheader
+
CONTINUATION = ',\n\t'
diff --git a/src/mailman/pipeline/tagger.py b/src/mailman/handlers/tagger.py
index 49e004a12..49e004a12 100644
--- a/src/mailman/pipeline/tagger.py
+++ b/src/mailman/handlers/tagger.py
diff --git a/src/mailman/pipeline/tests/__init__.py b/src/mailman/handlers/tests/__init__.py
index e69de29bb..e69de29bb 100644
--- a/src/mailman/pipeline/tests/__init__.py
+++ b/src/mailman/handlers/tests/__init__.py
diff --git a/src/mailman/pipeline/tests/test_mimedel.py b/src/mailman/handlers/tests/test_mimedel.py
index 566c1a40c..6ca34b17b 100644
--- a/src/mailman/pipeline/tests/test_mimedel.py
+++ b/src/mailman/handlers/tests/test_mimedel.py
@@ -32,10 +32,10 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.core import errors
+from mailman.handlers import mime_delete
from mailman.interfaces.action import FilterAction
from mailman.interfaces.member import MemberRole
from mailman.interfaces.usermanager import IUserManager
-from mailman.pipeline import mime_delete
from mailman.testing.helpers import (
LogFileMark,
get_queue_messages,
diff --git a/src/mailman/handlers/tests/test_recipients.py b/src/mailman/handlers/tests/test_recipients.py
new file mode 100644
index 000000000..29cd5b64a
--- /dev/null
+++ b/src/mailman/handlers/tests/test_recipients.py
@@ -0,0 +1,200 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Testing various recipients stuff."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestMemberRecipients',
+ 'TestOwnerRecipients',
+ ]
+
+
+import unittest
+
+from zope.component import getUtility
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole
+from mailman.interfaces.usermanager import IUserManager
+from mailman.testing.helpers import specialized_message_from_string as mfs
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestMemberRecipients(unittest.TestCase):
+ """Test regular member recipient calculation."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._manager = getUtility(IUserManager)
+ anne = self._manager.create_address('anne@example.com')
+ bart = self._manager.create_address('bart@example.com')
+ cris = self._manager.create_address('cris@example.com')
+ dave = self._manager.create_address('dave@example.com')
+ self._anne = self._mlist.subscribe(anne, MemberRole.member)
+ self._bart = self._mlist.subscribe(bart, MemberRole.member)
+ self._cris = self._mlist.subscribe(cris, MemberRole.member)
+ self._dave = self._mlist.subscribe(dave, MemberRole.member)
+ self._process = config.handlers['member-recipients'].process
+ self._msg = mfs("""\
+From: Elle Person <elle@example.com>
+To: test@example.com
+
+""")
+
+ def test_shortcircuit(self):
+ # When there are already recipients in the message metadata, those are
+ # used instead of calculating them from the list membership.
+ recipients = set(('zperson@example.com', 'yperson@example.com'))
+ msgdata = dict(recipients=recipients)
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], recipients)
+
+ def test_calculate_recipients(self):
+ # The normal path just adds the list's regular members.
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('anne@example.com',
+ 'bart@example.com',
+ 'cris@example.com',
+ 'dave@example.com')))
+
+ def test_digest_members_not_included(self):
+ # Digest members are not included in the recipients calculated by this
+ # handler.
+ self._cris.preferences.delivery_mode = DeliveryMode.mime_digests
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('anne@example.com',
+ 'bart@example.com',
+ 'dave@example.com')))
+
+
+
+class TestOwnerRecipients(unittest.TestCase):
+ """Test owner recipient calculation."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._manager = getUtility(IUserManager)
+ anne = self._manager.create_address('anne@example.com')
+ bart = self._manager.create_address('bart@example.com')
+ cris = self._manager.create_address('cris@example.com')
+ dave = self._manager.create_address('dave@example.com')
+ # Make Cris and Dave owners of the mailing list.
+ self._anne = self._mlist.subscribe(anne, MemberRole.member)
+ self._bart = self._mlist.subscribe(bart, MemberRole.member)
+ self._cris = self._mlist.subscribe(cris, MemberRole.owner)
+ self._dave = self._mlist.subscribe(dave, MemberRole.owner)
+ self._process = config.handlers['owner-recipients'].process
+ self._msg = mfs("""\
+From: Elle Person <elle@example.com>
+To: test-owner@example.com
+
+""")
+
+ def test_shortcircuit(self):
+ # When there are already recipients in the message metadata, those are
+ # used instead of calculating them from the owner membership.
+ recipients = set(('zperson@example.com', 'yperson@example.com'))
+ msgdata = dict(recipients=recipients)
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], recipients)
+
+ def test_calculate_recipients(self):
+ # The normal path just adds the list's owners.
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('cris@example.com',
+ 'dave@example.com')))
+
+ def test_with_moderators(self):
+ # Moderators are included in the owner recipient list.
+ elle = self._manager.create_address('elle@example.com')
+ fred = self._manager.create_address('fred@example.com')
+ gwen = self._manager.create_address('gwen@example.com')
+ self._mlist.subscribe(elle, MemberRole.moderator)
+ self._mlist.subscribe(fred, MemberRole.moderator)
+ self._mlist.subscribe(gwen, MemberRole.owner)
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('cris@example.com',
+ 'dave@example.com',
+ 'elle@example.com',
+ 'fred@example.com',
+ 'gwen@example.com')))
+
+ def test_dont_decorate(self):
+ # Messages to the administrators don't get decorated.
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertTrue(msgdata['nodecorate'])
+
+ def test_omit_disabled_owners(self):
+ # Owner memberships can be disabled, and these folks will not get the
+ # messages.
+ self._dave.preferences.delivery_status = DeliveryStatus.by_user
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('cris@example.com',)))
+
+ def test_include_membership_disabled_owner_enabled(self):
+ # If an address is subscribed to a mailing list as both an owner and a
+ # member, and their membership is disabled but their ownership
+ # subscription is not, they still get owner email.
+ dave = self._manager.get_address('dave@example.com')
+ member = self._mlist.subscribe(dave, MemberRole.member)
+ member.preferences.delivery_status = DeliveryStatus.by_user
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('cris@example.com',
+ 'dave@example.com')))
+ # Dave disables his owner membership but re-enables his list
+ # membership. He will not get the owner emails now.
+ member.preferences.delivery_status = DeliveryStatus.enabled
+ self._dave.preferences.delivery_status = DeliveryStatus.by_user
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('cris@example.com',)))
+
+ def test_all_owners_disabled(self):
+ # If all the owners are disabled, then the site owner gets the
+ # message. This prevents a list's -owner address from going into a
+ # black hole.
+ self._cris.preferences.delivery_status = DeliveryStatus.by_user
+ self._dave.preferences.delivery_status = DeliveryStatus.by_user
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('noreply@example.com',)))
+
+ def test_no_owners(self):
+ # If a list has no owners or moderators, then the site owner gets the
+ # message. This prevents a list's -owner address from going into a
+ # black hole.
+ self._cris.unsubscribe()
+ self._dave.unsubscribe()
+ self.assertEqual(self._mlist.administrators.member_count, 0)
+ msgdata = {}
+ self._process(self._mlist, self._msg, msgdata)
+ self.assertEqual(msgdata['recipients'], set(('noreply@example.com',)))
diff --git a/src/mailman/pipeline/to_archive.py b/src/mailman/handlers/to_archive.py
index fd5259a14..fd5259a14 100644
--- a/src/mailman/pipeline/to_archive.py
+++ b/src/mailman/handlers/to_archive.py
diff --git a/src/mailman/pipeline/to_digest.py b/src/mailman/handlers/to_digest.py
index 698f16e1e..698f16e1e 100644
--- a/src/mailman/pipeline/to_digest.py
+++ b/src/mailman/handlers/to_digest.py
diff --git a/src/mailman/pipeline/to_outgoing.py b/src/mailman/handlers/to_outgoing.py
index 971f87757..971f87757 100644
--- a/src/mailman/pipeline/to_outgoing.py
+++ b/src/mailman/handlers/to_outgoing.py
diff --git a/src/mailman/pipeline/to_usenet.py b/src/mailman/handlers/to_usenet.py
index 26a383c64..26a383c64 100644
--- a/src/mailman/pipeline/to_usenet.py
+++ b/src/mailman/handlers/to_usenet.py
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index d92bae464..bced070d3 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -382,7 +382,7 @@ class IMailingList(Interface):
# Processing.
posting_chain = Attribute(
- """This mailing list's moderation chain.
+ """This mailing list's posting moderation chain.
When messages are posted to a mailing list, it first goes through a
moderation chain to determine whether the message will be accepted.
@@ -397,6 +397,24 @@ class IMailingList(Interface):
This attribute names a pipeline for postings, which must exist.
""")
+ owner_chain = Attribute(
+ """This mailing list's owner moderation chain.
+
+ When messages are posted to the owners of a mailing list, it first
+ goes through a moderation chain to determine whether the message will
+ be accepted. This attribute names a chain for postings, which must
+ exist.
+ """)
+
+ owner_pipeline = Attribute(
+ """This mailing list's owner posting pipeline.
+
+ Every mailing list has a processing pipeline that messages flow
+ through once they've been accepted for posting to the owners of a
+ mailing list. This attribute names a pipeline for postings, which
+ must exist.
+ """)
+
data_path = Attribute(
"""The file system path to list-specific data.
diff --git a/src/mailman/model/docs/requests.rst b/src/mailman/model/docs/requests.rst
index 1e461e36c..068cc84f6 100644
--- a/src/mailman/model/docs/requests.rst
+++ b/src/mailman/model/docs/requests.rst
@@ -664,7 +664,7 @@ The admin message is sent to the moderators.
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Subject: A Test List subscription notification
- From: changeme@example.com
+ From: noreply@example.com
To: alist-owner@example.com
Message-ID: ...
Date: ...
@@ -676,7 +676,7 @@ The admin message is sent to the moderators.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- envsender : changeme@example.com
+ envsender : noreply@example.com
listname : alist@example.com
nodecorate : True
recipients : set([])
@@ -888,7 +888,7 @@ The goodbye message...
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Subject: A Test List unsubscription notification
- From: changeme@example.com
+ From: noreply@example.com
To: alist-owner@example.com
Message-ID: ...
Date: ...
@@ -899,7 +899,7 @@ The goodbye message...
>>> dump_msgdata(messages[1].msgdata)
_parsemsg : False
- envsender : changeme@example.com
+ envsender : noreply@example.com
listname : alist@example.com
nodecorate : True
recipients : set([])
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index 76f88caa7..e397d59d6 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -168,6 +168,8 @@ class MailingList(Model):
nondigestable = Bool()
nonmember_rejection_notice = Unicode()
obscure_addresses = Bool()
+ owner_chain = Unicode()
+ owner_pipeline = Unicode()
personalize = Enum(Personalization)
post_id = Int()
posting_chain = Unicode()
diff --git a/src/mailman/pipeline/owner_recipients.py b/src/mailman/pipeline/owner_recipients.py
deleted file mode 100644
index 9e4bbf174..000000000
--- a/src/mailman/pipeline/owner_recipients.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright (C) 2001-2012 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Calculate the list owner recipients (includes moderators)."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'process',
- ]
-
-
-
-def process(mlist, msg, msgdata):
- """Add owner recipients."""
- # The recipients are the owner and the moderator
- msgdata['recipients'] = mlist.owner + mlist.moderator
- # Don't decorate these messages with the header/footers
- msgdata['nodecorate'] = True
- msgdata['personalize'] = False
diff --git a/src/mailman/runners/digest.py b/src/mailman/runners/digest.py
index 513a20322..afc11f732 100644
--- a/src/mailman/runners/digest.py
+++ b/src/mailman/runners/digest.py
@@ -41,11 +41,10 @@ from email.utils import formatdate, getaddresses, make_msgid
from urllib2 import URLError
from mailman.config import config
-from mailman.core.errors import DiscardMessage
from mailman.core.i18n import _
from mailman.core.runner import Runner
+from mailman.handlers.decorate import decorate
from mailman.interfaces.member import DeliveryMode, DeliveryStatus
-from mailman.pipeline.decorate import decorate
from mailman.utilities.i18n import make
from mailman.utilities.mailbox import Mailbox
from mailman.utilities.string import oneline, wrap
diff --git a/src/mailman/runners/docs/lmtp.rst b/src/mailman/runners/docs/lmtp.rst
index 2b6c4b42b..3ce145907 100644
--- a/src/mailman/runners/docs/lmtp.rst
+++ b/src/mailman/runners/docs/lmtp.rst
@@ -306,7 +306,7 @@ Messages to the `-owner` address also go to the incoming processor.
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
- envsender : changeme@example.com
+ envsender : noreply@example.com
listname : mylist@example.com
original_size: ...
subaddress : owner
diff --git a/src/mailman/runners/docs/outgoing.rst b/src/mailman/runners/docs/outgoing.rst
index b1ffb06a0..d8963ae00 100644
--- a/src/mailman/runners/docs/outgoing.rst
+++ b/src/mailman/runners/docs/outgoing.rst
@@ -33,7 +33,7 @@ move messages to the 'retry queue' for handling delivery failures.
Normally, messages would show up in the outgoing queue after the message has
been processed by the rule set and pipeline. But we can simulate that here by
injecting a message directly into the outgoing queue. First though, we must
-call the ``calculate-recipients`` handler so that the message metadata will be
+call the ``member-recipients`` handler so that the message metadata will be
populated with the list of addresses to deliver the message to.
::
@@ -47,7 +47,7 @@ populated with the list of addresses to deliver the message to.
... """)
>>> msgdata = {}
- >>> handler = config.handlers['calculate-recipients']
+ >>> handler = config.handlers['member-recipients']
>>> handler.process(mlist, msg, msgdata)
>>> outgoing_queue = config.switchboards['out']
diff --git a/src/mailman/runners/incoming.py b/src/mailman/runners/incoming.py
index 7072f9bcc..d8db926c7 100644
--- a/src/mailman/runners/incoming.py
+++ b/src/mailman/runners/incoming.py
@@ -61,6 +61,9 @@ class IncomingRunner(Runner):
pass
config.db.commit()
# Process the message through the mailing list's start chain.
- process(mlist, msg, msgdata, mlist.posting_chain)
+ start_chain = (mlist.owner_chain
+ if msgdata.get('to_owner', False)
+ else mlist.posting_chain)
+ process(mlist, msg, msgdata, start_chain)
# Do not keep this message queued.
return False
diff --git a/src/mailman/runners/pipeline.py b/src/mailman/runners/pipeline.py
index 8bee2c4cb..b031662b3 100644
--- a/src/mailman/runners/pipeline.py
+++ b/src/mailman/runners/pipeline.py
@@ -30,6 +30,9 @@ from mailman.core.runner import Runner
class PipelineRunner(Runner):
def _dispose(self, mlist, msg, msgdata):
# Process the message through the mailing list's pipeline.
- process(mlist, msg, msgdata, mlist.posting_pipeline)
+ pipeline = (mlist.owner_pipeline
+ if msgdata.get('to_owner', False)
+ else mlist.posting_pipeline)
+ process(mlist, msg, msgdata, pipeline)
# Do not keep this message queued.
return False
diff --git a/src/mailman/runners/tests/test_incoming.py b/src/mailman/runners/tests/test_incoming.py
new file mode 100644
index 000000000..5a0d82765
--- /dev/null
+++ b/src/mailman/runners/tests/test_incoming.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test the incoming queue runner."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestIncoming',
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.chains.base import TerminalChainBase
+from mailman.config import config
+from mailman.runners.incoming import IncomingRunner
+from mailman.testing.helpers import (
+ get_queue_messages,
+ make_testable_runner,
+ specialized_message_from_string as mfs)
+from mailman.testing.layers import ConfigLayer
+
+
+
+class Chain(TerminalChainBase):
+ name = 'test'
+ description = 'a test chain'
+
+ def __init__(self, marker):
+ self._marker = marker
+
+ def _process(self, mlist, msg, msgdata):
+ msgdata['marker'] = self._marker
+ config.switchboards['out'].enqueue(msg, msgdata)
+
+
+
+class TestIncoming(unittest.TestCase):
+ """Test the incoming queue runner."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._mlist.posting_chain = 'test posting'
+ self._mlist.owner_chain = 'test owner'
+ config.chains['test posting'] = Chain('posting')
+ config.chains['test owner'] = Chain('owner')
+ self._in = make_testable_runner(IncomingRunner, 'in')
+ self._msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+
+""")
+
+ def tearDown(self):
+ del config.chains['test posting']
+ del config.chains['test owner']
+
+ def test_posting(self):
+ # A message posted to the list goes through the posting chain.
+ msgdata = dict(listname='test@example.com')
+ config.switchboards['in'].enqueue(self._msg, msgdata)
+ self._in.run()
+ messages = get_queue_messages('out')
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0].msgdata.get('marker'), 'posting')
+
+ def test_owner(self):
+ # A message posted to the list goes through the posting chain.
+ msgdata = dict(listname='test@example.com',
+ to_owner=True)
+ config.switchboards['in'].enqueue(self._msg, msgdata)
+ self._in.run()
+ messages = get_queue_messages('out')
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0].msgdata.get('marker'), 'owner')
diff --git a/src/mailman/runners/tests/test_owner.py b/src/mailman/runners/tests/test_owner.py
new file mode 100644
index 000000000..8264ef0d8
--- /dev/null
+++ b/src/mailman/runners/tests/test_owner.py
@@ -0,0 +1,142 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test posting to a mailing list's -owner address."""
+
+# XXX 2012-03-23 BAW: This is not necessarily the best place for this test.
+# We really need a better place to collect these sort of end-to-end posting
+# tests. They're not exactly integration tests, but they do touch lots of
+# parts of the system.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestEmailToOwner',
+ ]
+
+
+import unittest
+
+from operator import itemgetter
+from zope.component import getUtility
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.interfaces.member import MemberRole
+from mailman.interfaces.usermanager import IUserManager
+from mailman.testing.helpers import (
+ TestableMaster,
+ get_lmtp_client,
+ make_testable_runner)
+from mailman.runners.incoming import IncomingRunner
+from mailman.runners.outgoing import OutgoingRunner
+from mailman.runners.pipeline import PipelineRunner
+from mailman.testing.layers import SMTPLayer
+
+
+
+class TestEmailToOwner(unittest.TestCase):
+ """Test emailing a mailing list's -owner address."""
+
+ layer = SMTPLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ # Add some owners, moderators, and members
+ manager = getUtility(IUserManager)
+ anne = manager.create_address('anne@example.com')
+ bart = manager.create_address('bart@example.com')
+ cris = manager.create_address('cris@example.com')
+ dave = manager.create_address('dave@example.com')
+ self._mlist.subscribe(anne, MemberRole.member)
+ self._mlist.subscribe(anne, MemberRole.owner)
+ self._mlist.subscribe(bart, MemberRole.moderator)
+ self._mlist.subscribe(bart, MemberRole.owner)
+ self._mlist.subscribe(cris, MemberRole.moderator)
+ self._mlist.subscribe(dave, MemberRole.member)
+ config.db.commit()
+ self._inq = make_testable_runner(IncomingRunner, 'in')
+ self._pipelineq = make_testable_runner(PipelineRunner, 'pipeline')
+ self._outq = make_testable_runner(OutgoingRunner, 'out')
+ # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
+ self.maxDiff = None
+ self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
+
+ def test_owners_get_email(self):
+ # XXX 2012-03-23 BAW: We can't use a layer here because we need both
+ # the SMTPLayer and LMTPLayer and these are incompatible. There's no
+ # way to make zope.test* happy without causing errors or worse. Live
+ # with this hack until we can rip all that layer crap out and use
+ # something like testresources.
+ def wait():
+ get_lmtp_client(quiet=True)
+ lmtpd = TestableMaster(wait)
+ lmtpd.start()
+ # Post a message to the list's -owner address, and all the owners will
+ # get a copy of the message.
+ lmtp = get_lmtp_client(quiet=True)
+ lmtp.lhlo('remote.example.org')
+ lmtp.sendmail('zuzu@example.org', ['test-owner@example.com'], """\
+From: Zuzu Person <zuzu@example.org>
+To: test-owner@example.com
+Message-ID: <ant>
+
+Can you help me?
+""")
+ lmtpd.stop()
+ # There should now be one message sitting in the incoming queue.
+ # Check that, then process it. Don't use get_queue_messages() since
+ # that will empty the queue.
+ self.assertEqual(len(config.switchboards['in'].files), 1)
+ self._inq.run()
+ # There should now be one message sitting in the pipeline queue.
+ # Process that one too.
+ self.assertEqual(len(config.switchboards['pipeline'].files), 1)
+ self._pipelineq.run()
+ # The message has made its way to the outgoing queue. Again, check
+ # and process that one.
+ self.assertEqual(len(config.switchboards['out'].files), 1)
+ self._outq.run()
+ # The SMTP server has now received three messages, one for each of the
+ # owners and moderators. Of course, Bart is both an owner and a
+ # moderator, so he'll get only one copy of the message. Dave does not
+ # get a copy of the message.
+ messages = sorted(SMTPLayer.smtpd.messages, key=itemgetter('x-rcptto'))
+ self.assertEqual(len(messages), 3)
+ self.assertEqual(messages[0]['x-rcptto'], 'anne@example.com')
+ self.assertEqual(messages[1]['x-rcptto'], 'bart@example.com')
+ self.assertEqual(messages[2]['x-rcptto'], 'cris@example.com')
+ # And yet, all three messages are addressed to the -owner address.
+ for message in messages:
+ self.assertEqual(message['to'], 'test-owner@example.com')
+ # All three messages will have two X-MailFrom headers. One is added
+ # by the LMTP server accepting Zuzu's original message, and will
+ # contain her posting address, i.e. zuzu@example.com. The second one
+ # is added by the lazr.smtptest server that accepts Mailman's VERP'd
+ # message to the individual recipient. By verifying both, we prove
+ # that Zuzu sent the original message, and that Mailman is VERP'ing
+ # the copy to all the owners.
+ self.assertEqual(
+ messages[0].get_all('x-mailfrom'),
+ ['zuzu@example.org', 'test-bounces+anne=example.com@example.com'])
+ self.assertEqual(
+ messages[1].get_all('x-mailfrom'),
+ ['zuzu@example.org', 'test-bounces+bart=example.com@example.com'])
+ self.assertEqual(
+ messages[2].get_all('x-mailfrom'),
+ ['zuzu@example.org', 'test-bounces+cris=example.com@example.com'])
diff --git a/src/mailman/runners/tests/test_pipeline.py b/src/mailman/runners/tests/test_pipeline.py
new file mode 100644
index 000000000..8776bf844
--- /dev/null
+++ b/src/mailman/runners/tests/test_pipeline.py
@@ -0,0 +1,115 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test the pipeline runner."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestPipelineRunner',
+ ]
+
+
+import unittest
+
+from zope.interface import implements
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.interfaces.handler import IHandler
+from mailman.interfaces.pipeline import IPipeline
+from mailman.runners.pipeline import PipelineRunner
+from mailman.testing.helpers import (
+ make_testable_runner,
+ specialized_message_from_string as mfs)
+from mailman.testing.layers import ConfigLayer
+
+
+
+class MyTestHandler:
+ implements(IHandler)
+ name = 'test handler'
+ description = 'A test handler'
+
+ def __init__(self, marker, test):
+ self._marker = marker
+ self._test = test
+
+ def process(self, mlist, msg, msgdata):
+ self._test.mark(self._marker)
+
+
+class MyTestPipeline:
+ implements(IPipeline)
+ name = 'test'
+ description = 'a test pipeline'
+
+ def __init__(self, marker, test):
+ self._marker = marker
+ self._test = test
+
+ def __iter__(self):
+ yield MyTestHandler(self._marker, self._test)
+
+
+
+class TestPipelineRunner(unittest.TestCase):
+ """Test the pipeline runner."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._mlist.posting_pipeline = 'test posting'
+ self._mlist.owner_pipeline = 'test owner'
+ config.pipelines['test posting'] = MyTestPipeline('posting', self)
+ config.pipelines['test owner'] = MyTestPipeline('owner', self)
+ self._pipeline = make_testable_runner(PipelineRunner, 'pipeline')
+ self._markers = []
+ self._msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+
+""")
+
+ def tearDown(self):
+ del config.pipelines['test posting']
+ del config.pipelines['test owner']
+
+ def mark(self, marker):
+ # Record a marker seen by a handler.
+ self._markers.append(marker)
+
+ def test_posting(self):
+ # A message accepted for posting gets processed through the posting
+ # pipeline.
+ msgdata = dict(listname='test@example.com')
+ config.switchboards['pipeline'].enqueue(self._msg, msgdata)
+ self._pipeline.run()
+ self.assertEqual(len(self._markers), 1)
+ self.assertEqual(self._markers[0], 'posting')
+
+ def test_owner(self):
+ # A message accepted for posting to a list's owners gets processed
+ # through the owner pipeline.
+ msgdata = dict(listname='test@example.com',
+ to_owner=True)
+ config.switchboards['pipeline'].enqueue(self._msg, msgdata)
+ self._pipeline.run()
+ self.assertEqual(len(self._markers), 1)
+ self.assertEqual(self._markers[0], 'owner')
diff --git a/src/mailman/styles/default.py b/src/mailman/styles/default.py
index e64bbe40b..18a145d3c 100644
--- a/src/mailman/styles/default.py
+++ b/src/mailman/styles/default.py
@@ -218,6 +218,11 @@ from: .*@uplinkpro.com
# The default pipeline to send accepted messages through to the
# mailing list's members.
mlist.posting_pipeline = 'default-posting-pipeline'
+ # The processing chain that messages posted to this mailing list's
+ # -owner address gets processed by.
+ mlist.owner_chain = 'default-owner-chain'
+ # The default pipeline to send -owner email through.
+ mlist.owner_pipeline = 'default-owner-pipeline'
def match(self, mailing_list, styles):
"""See `IStyle`."""
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index 04ab8f91f..41ef86935 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -17,6 +17,14 @@
"""Mailman test layers."""
+# XXX 2012-03-23 BAW: Layers really really suck. For example, the
+# test_owners_get_email() test requires that both the SMTPLayer and LMTPLayer
+# be set up, but there's apparently no way to do that and make zope.testing
+# happy. This causes no tests failures, but it does cause errors at the end
+# of the full test run. For now, I'll ignore that, but I do want to
+# eventually get rid of the zope.test* dependencies and use something like
+# testresources or some such.
+
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg
index d503247de..b7e80ff02 100644
--- a/src/mailman/testing/testing.cfg
+++ b/src/mailman/testing/testing.cfg
@@ -22,6 +22,9 @@
#class: mailman.database.postgresql.PostgreSQLDatabase
#url: postgres://barry:barry@localhost/mailman
+[mailman]
+site_owner: noreply@example.com
+
[mta]
smtp_port: 9025
lmtp_port: 9024