diff options
Diffstat (limited to 'src/mailman/chains')
| -rw-r--r-- | src/mailman/chains/base.py | 44 | ||||
| -rw-r--r-- | src/mailman/chains/builtin.py | 2 | ||||
| -rw-r--r-- | src/mailman/chains/dmarc.py | 39 | ||||
| -rw-r--r-- | src/mailman/chains/hold.py | 4 | ||||
| -rw-r--r-- | src/mailman/chains/moderation.py | 20 | ||||
| -rw-r--r-- | src/mailman/chains/reject.py | 19 | ||||
| -rw-r--r-- | src/mailman/chains/tests/test_accept.py | 2 | ||||
| -rw-r--r-- | src/mailman/chains/tests/test_dmarc.py | 82 |
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. +""") |
