summaryrefslogtreecommitdiff
path: root/src/mailman/chains
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/chains')
-rw-r--r--src/mailman/chains/base.py44
-rw-r--r--src/mailman/chains/builtin.py2
-rw-r--r--src/mailman/chains/dmarc.py39
-rw-r--r--src/mailman/chains/hold.py4
-rw-r--r--src/mailman/chains/moderation.py20
-rw-r--r--src/mailman/chains/reject.py19
-rw-r--r--src/mailman/chains/tests/test_accept.py2
-rw-r--r--src/mailman/chains/tests/test_dmarc.py82
8 files changed, 164 insertions, 48 deletions
diff --git a/src/mailman/chains/base.py b/src/mailman/chains/base.py
index 9507d8fbc..45301abc3 100644
--- a/src/mailman/chains/base.py
+++ b/src/mailman/chains/base.py
@@ -18,7 +18,6 @@
"""Base class for terminal chains."""
from mailman.config import config
-from mailman.core.i18n import _
from mailman.interfaces.chain import (
IChain, IChainIterator, IChainLink, IMutableChain, LinkAction)
from mailman.interfaces.rules import IRule
@@ -28,24 +27,6 @@ from zope.interface import implementer
@public
-def format_reasons(reasons):
- """Translate and format hold and rejection reasons.
-
- :param reasons: A list of reasons from the rules that hit. Each reason is
- a string to be translated or a tuple consisting of a string with {}
- replacements and one or more replacement values.
- :returns: A list of the translated and formatted strings.
- """
- new_reasons = []
- for reason in reasons:
- if isinstance(reason, tuple):
- new_reasons.append(_(reason[0]).format(*reason[1:]))
- else:
- new_reasons.append(_(reason))
- return new_reasons
-
-
-@public
@implementer(IChainLink)
class Link:
"""A chain link."""
@@ -107,6 +88,31 @@ class TerminalChainBase:
@public
@abstract_component
+@implementer(IChain)
+class JumpChainBase:
+ """A base chain that simplifies jumping to another chain."""
+ def jump_to(self, mlist, msg, msgsdata):
+ """Return the chain to jump to.
+
+ This must be overridden by subclasses.
+
+ :param mlist: The mailing list.
+ :param msg: The message.
+ :param msgdata: The message metadata.
+ :return: The name of the chain to jump to.
+ :rtype: str
+ """
+ raise NotImplementedError
+
+ def get_links(self, mlist, msg, msgdata):
+ jump_chain = self.jump_to(mlist, msg, msgdata)
+ return iter([
+ Link('truth', LinkAction.jump, jump_chain),
+ ])
+
+
+@public
+@abstract_component
@implementer(IMutableChain)
class Chain:
"""Generic chain base class."""
diff --git a/src/mailman/chains/builtin.py b/src/mailman/chains/builtin.py
index 9b97fef38..ad016201f 100644
--- a/src/mailman/chains/builtin.py
+++ b/src/mailman/chains/builtin.py
@@ -41,7 +41,7 @@ class BuiltInChain:
# First check DMARC. For a reject or discard, the rule hits and we
# jump to the moderation chain to do the action. Otherwise, the rule
# misses buts sets msgdata['dmarc'] for the handler.
- ('dmarc-mitigation', LinkAction.jump, 'moderation'),
+ ('dmarc-mitigation', LinkAction.jump, 'dmarc'),
# Discard emails with no valid senders.
('no-senders', LinkAction.jump, 'discard'),
('approved', LinkAction.jump, 'accept'),
diff --git a/src/mailman/chains/dmarc.py b/src/mailman/chains/dmarc.py
new file mode 100644
index 000000000..6a8d4d188
--- /dev/null
+++ b/src/mailman/chains/dmarc.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2017 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/>.
+
+"""DMARC mitigation chain."""
+
+from mailman.chains.base import JumpChainBase
+from mailman.core.i18n import _
+from public import public
+
+
+@public
+class DMARCMitigationChain(JumpChainBase):
+ """Perform DMARC mitigation."""
+
+ name = 'dmarc'
+ description = _('Process DMARC reject or discard mitigations')
+
+ def jump_to(self, mlist, msg, msgdata):
+ # Which action should be taken?
+ jump_chain = msgdata['dmarc_action']
+ assert jump_chain in ('discard', 'reject'), (
+ '{}: Invalid DMARC action: {} for sender: {}'.format(
+ mlist.list_id, jump_chain,
+ msgdata.get('moderation_sender', '(unknown)')))
+ return jump_chain
diff --git a/src/mailman/chains/hold.py b/src/mailman/chains/hold.py
index edc80da3c..ea25dbc0d 100644
--- a/src/mailman/chains/hold.py
+++ b/src/mailman/chains/hold.py
@@ -24,9 +24,9 @@ from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from mailman.app.moderator import hold_message
from mailman.app.replybot import can_acknowledge
-from mailman.chains.base import TerminalChainBase, format_reasons
+from mailman.chains.base import TerminalChainBase
from mailman.config import config
-from mailman.core.i18n import _
+from mailman.core.i18n import _, format_reasons
from mailman.email.message import UserNotification
from mailman.interfaces.autorespond import IAutoResponseSet, Response
from mailman.interfaces.chain import HoldEvent
diff --git a/src/mailman/chains/moderation.py b/src/mailman/chains/moderation.py
index 7cfc0a01e..d8f8fb7e7 100644
--- a/src/mailman/chains/moderation.py
+++ b/src/mailman/chains/moderation.py
@@ -34,17 +34,14 @@ made as to the disposition of the message. `defer` is the default for
members, while `hold` is the default for nonmembers.
"""
-from mailman.chains.base import Link
+from mailman.chains.base import JumpChainBase
from mailman.core.i18n import _
from mailman.interfaces.action import Action
-from mailman.interfaces.chain import IChain, LinkAction
from public import public
-from zope.interface import implementer
@public
-@implementer(IChain)
-class ModerationChain:
+class ModerationChain(JumpChainBase):
"""Dynamically produce a link jumping to the appropriate terminal chain.
The terminal chain will be one of the Accept, Hold, Discard, or Reject
@@ -53,13 +50,12 @@ class ModerationChain:
name = 'moderation'
description = _('Moderation chain')
- def get_links(self, mlist, msg, msgdata):
- """See `IChain`."""
+ def jump_to(self, mlist, msg, msgdata):
# Get the moderation action from the message metadata. It can only be
# one of the expected values (i.e. not Action.defer). See the
# moderation.py rule for details. This is stored in the metadata as a
# string so that it can be stored in the pending table.
- action = Action[msgdata.get('moderation_action')]
+ action = Action[msgdata.get('member_moderation_action')]
# defer is not a valid moderation action.
jump_chain = {
Action.accept: 'accept',
@@ -68,9 +64,7 @@ class ModerationChain:
Action.reject: 'reject',
}.get(action)
assert jump_chain is not None, (
- '{0}: Invalid moderation action: {1} for sender: {2}'.format(
- mlist.fqdn_listname, action,
+ '{}: Invalid moderation action: {} for sender: {}'.format(
+ mlist.list_id, action,
msgdata.get('moderation_sender', '(unknown)')))
- return iter([
- Link('truth', LinkAction.jump, jump_chain),
- ])
+ return jump_chain
diff --git a/src/mailman/chains/reject.py b/src/mailman/chains/reject.py
index 31f66c8fa..6696e1900 100644
--- a/src/mailman/chains/reject.py
+++ b/src/mailman/chains/reject.py
@@ -20,11 +20,13 @@
import logging
from mailman.app.bounces import bounce_message
-from mailman.chains.base import TerminalChainBase, format_reasons
+from mailman.chains.base import TerminalChainBase
from mailman.core.i18n import _
from mailman.interfaces.chain import RejectEvent
from mailman.interfaces.pipeline import RejectMessage
+from mailman.interfaces.template import ITemplateLoader
from public import public
+from zope.component import getUtility
from zope.event import notify
@@ -56,17 +58,10 @@ class RejectChain(TerminalChainBase):
if reasons is None:
error = None
else:
- error = RejectMessage(_("""
-Your message to the {list_name} mailing-list was rejected for the following
-reasons:
-
-{reasons}
-
-The original message as received by Mailman is attached.
-""").format(
- list_name=mlist.display_name, # noqa: E122
- reasons=NEWLINE.join(format_reasons(reasons))
- ))
+ template = getUtility(ITemplateLoader).get(
+ 'list:user:notice:rejected', mlist)
+ error = RejectMessage(
+ template, reasons, dict(listname=mlist.display_name))
bounce_message(mlist, msg, error)
log.info('REJECT: %s', msg.get('message-id', 'n/a'))
notify(RejectEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/tests/test_accept.py b/src/mailman/chains/tests/test_accept.py
index 65b18d0b4..bf8bad541 100644
--- a/src/mailman/chains/tests/test_accept.py
+++ b/src/mailman/chains/tests/test_accept.py
@@ -51,7 +51,7 @@ class TestAccept(unittest.TestCase):
self._mlist = create_list('ant@example.com')
self._msg = mfs("""\
From: anne@example.com
-To: test@example.com
+To: ant@example.com
Subject: Ignore
""")
diff --git a/src/mailman/chains/tests/test_dmarc.py b/src/mailman/chains/tests/test_dmarc.py
new file mode 100644
index 000000000..8d0072254
--- /dev/null
+++ b/src/mailman/chains/tests/test_dmarc.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2017 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 for the DMARC chain."""
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.core.chains import process as process_chain
+from mailman.interfaces.chain import DiscardEvent, RejectEvent
+from mailman.testing.helpers import (
+ event_subscribers, get_queue_messages,
+ specialized_message_from_string as mfs)
+from mailman.testing.layers import ConfigLayer
+
+
+class TestDMARC(unittest.TestCase):
+ layer = ConfigLayer
+ maxDiff = None
+
+ def setUp(self):
+ self._mlist = create_list('ant@example.com')
+ self._msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+Subject: Ignore
+
+""")
+
+ def test_discard(self):
+ msgdata = dict(dmarc_action='discard')
+ # When a message is discarded, the only artifacts are a log message
+ # and an event. Catch the event to prove it happened.
+ events = []
+ def handler(event): # noqa: E306
+ if isinstance(event, DiscardEvent):
+ events.append(event)
+ with event_subscribers(handler):
+ process_chain(self._mlist, self._msg, msgdata, start_chain='dmarc')
+ self.assertEqual(len(events), 1)
+ self.assertIs(events[0].msg, self._msg)
+
+ def test_reject(self):
+ msgdata = dict(
+ dmarc_action='reject',
+ moderation_reasons=['DMARC violation'],
+ )
+ # When a message is reject, an event will be triggered and the message
+ # will be bounced.
+ events = []
+ def handler(event): # noqa: E306
+ if isinstance(event, RejectEvent):
+ events.append(event)
+ with event_subscribers(handler):
+ process_chain(self._mlist, self._msg, msgdata, start_chain='dmarc')
+ self.assertEqual(len(events), 1)
+ self.assertIs(events[0].msg, self._msg)
+ items = get_queue_messages('virgin', expected_count=1)
+ # Unpack the rejection message.
+ rejection = items[0].msg.get_payload(0).get_payload()
+ self.assertEqual(rejection, """\
+Your message to the Ant mailing-list was rejected for the following
+reasons:
+
+DMARC violation
+
+The original message as received by Mailman is attached.
+""")