diff options
| author | Barry Warsaw | 2017-08-04 01:13:04 +0000 |
|---|---|---|
| committer | Barry Warsaw | 2017-08-04 01:13:04 +0000 |
| commit | 324226f1f859f6be5e932dc9abe638aba268d154 (patch) | |
| tree | f021166b8c82bb02feff82a9360ba61a44b804ee | |
| parent | e6326533b78290514ede917ed1cb95804759a45a (diff) | |
| parent | 9cdcffbc1189a19bc2963cf3d5c86a3d4f1f24a6 (diff) | |
| download | mailman-324226f1f859f6be5e932dc9abe638aba268d154.tar.gz mailman-324226f1f859f6be5e932dc9abe638aba268d154.tar.zst mailman-324226f1f859f6be5e932dc9abe638aba268d154.zip | |
Merge branch 'rename-metadata-key' into 'master'
Rename metadata key for clarity
Compose bounce messages so that they can be properly translated
See merge request !304
Diffstat (limited to '')
27 files changed, 355 insertions, 115 deletions
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py index 1239c916c..0d9a6ccb9 100644 --- a/src/mailman/app/bounces.py +++ b/src/mailman/app/bounces.py @@ -45,6 +45,7 @@ elog = logging.getLogger('mailman.error') blog = logging.getLogger('mailman.bounce') DOT = '.' +NL = '\n' @public @@ -56,8 +57,12 @@ def bounce_message(mlist, msg, error=None): :param msg: The original message. :type msg: `email.message.Message` :param error: Optional exception causing the bounce. The exception - instance must have a `.message` attribute. - :type error: Exception + instance must have a `.message` attribute. The exception *may* have a + non-None `.reasons` attribute which would be a list of reasons for the + rejection, and it may have a non-None `.substitutions` attribute. The + latter, along with the formatted reasons will be interpolated into the + message (`.reasons` gets put into the `$reasons` placeholder). + :type error: RejectMessage """ # Bounce a message back to the sender, with an error message if provided # in the exception argument. .sender might be None or the empty string. @@ -67,10 +72,9 @@ def bounce_message(mlist, msg, error=None): return subject = msg.get('subject', _('(no subject)')) subject = oneline(subject, mlist.preferred_language.charset) - if error is None: - notice = _('[No bounce details are available]') - else: - notice = _(error.message) + notice = (_('[No bounce details are available]') + if error is None + else str(error)) # Currently we always craft bounces as MIME messages. bmsg = UserNotification(msg.sender, mlist.owner_address, subject, lang=mlist.preferred_language) diff --git a/src/mailman/app/docs/bounces.rst b/src/mailman/app/docs/bounces.rst index 99eae8b2c..b25381031 100644 --- a/src/mailman/app/docs/bounces.rst +++ b/src/mailman/app/docs/bounces.rst @@ -12,12 +12,12 @@ Mailman can bounce messages back to the original sender. This is essentially equivalent to rejecting the message with notification. Mailing lists can bounce a message with an optional error message. - >>> mlist = create_list('text@example.com') + >>> mlist = create_list('ant@example.com') Any message can be bounced. >>> msg = message_from_string("""\ - ... To: text@example.com + ... To: ant@example.com ... From: aperson@example.com ... Subject: Something important ... @@ -36,7 +36,7 @@ to the original message author. 1 >>> print(items[0].msg.as_string()) Subject: Something important - From: text-owner@example.com + From: ant-owner@example.com To: aperson@example.com MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="..." @@ -54,7 +54,7 @@ to the original message author. Content-Type: message/rfc822 MIME-Version: 1.0 <BLANKLINE> - To: text@example.com + To: ant@example.com From: aperson@example.com Subject: Something important <BLANKLINE> @@ -63,18 +63,16 @@ to the original message author. --...-- An error message can be given when the message is bounced, and this will be -included in the payload of the text/plain part. The error message must be +included in the payload of the ``text/plain`` part. The error message must be passed in as an instance of a ``RejectMessage`` exception. >>> from mailman.interfaces.pipeline import RejectMessage >>> error = RejectMessage("This wasn't very important after all.") >>> bounce_message(mlist, msg, error) - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 + >>> items = get_queue_messages('virgin', expected_count=1) >>> print(items[0].msg.as_string()) Subject: Something important - From: text-owner@example.com + From: ant-owner@example.com To: aperson@example.com MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="..." @@ -92,10 +90,56 @@ passed in as an instance of a ``RejectMessage`` exception. Content-Type: message/rfc822 MIME-Version: 1.0 <BLANKLINE> - To: text@example.com + To: ant@example.com From: aperson@example.com Subject: Something important <BLANKLINE> I sometimes say something important. <BLANKLINE> --...-- + +The ``RejectMessage`` exception can also include a set of reasons, which will +be interpolated into the message using the ``{reasons}`` placeholder. + + >>> error = RejectMessage("""This message is rejected because: + ... + ... $reasons + ... """, [ + ... 'I am not happy', + ... 'You are not happy', + ... 'We are not happy']) + >>> bounce_message(mlist, msg, error) + >>> items = get_queue_messages('virgin', expected_count=1) + >>> print(items[0].msg.as_string()) + Subject: Something important + From: ant-owner@example.com + To: aperson@example.com + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="..." + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + --... + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + <BLANKLINE> + This message is rejected because: + <BLANKLINE> + I am not happy + You are not happy + We are not happy + <BLANKLINE> + --... + Content-Type: message/rfc822 + MIME-Version: 1.0 + <BLANKLINE> + To: ant@example.com + From: aperson@example.com + Subject: Something important + <BLANKLINE> + I sometimes say something important. + <BLANKLINE> + --... + <BLANKLINE> 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. +""") diff --git a/src/mailman/commands/cli_withlist.py b/src/mailman/commands/cli_withlist.py index b93d9b153..d2b706e34 100644 --- a/src/mailman/commands/cli_withlist.py +++ b/src/mailman/commands/cli_withlist.py @@ -80,7 +80,7 @@ def start_python(overrides, banner): # Set the tab completion. with ExitStack() as resources: try: # pragma: nocover - import readline, rlcompleter # noqa: F401, E401 + import readline, rlcompleter # noqa: F401,E401 except ImportError: # pragma: nocover print(_('readline not available'), file=sys.stderr) pass diff --git a/src/mailman/core/i18n.py b/src/mailman/core/i18n.py index 69abe3b9d..d5aa06b5c 100644 --- a/src/mailman/core/i18n.py +++ b/src/mailman/core/i18n.py @@ -47,3 +47,21 @@ def initialize(application=None): def handle_ConfigurationUpdatedEvent(event): if isinstance(event, ConfigurationUpdatedEvent): _.default = event.config.mailman.default_language + + +@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 diff --git a/src/mailman/core/pipelines.py b/src/mailman/core/pipelines.py index df07bc608..4265d24d1 100644 --- a/src/mailman/core/pipelines.py +++ b/src/mailman/core/pipelines.py @@ -55,7 +55,7 @@ def process(mlist, msg, msgdata, pipeline_name='built-in'): except RejectMessage as error: vlog.info( '{} rejected by "{}" pipeline handler "{}": {}'.format( - message_id, pipeline_name, handler.name, error.message)) + message_id, pipeline_name, handler.name, str(error))) bounce_message(mlist, msg, error) diff --git a/src/mailman/core/tests/test_pipelines.py b/src/mailman/core/tests/test_pipelines.py index 4ec55798f..3d4ae0ad2 100644 --- a/src/mailman/core/tests/test_pipelines.py +++ b/src/mailman/core/tests/test_pipelines.py @@ -48,8 +48,11 @@ class DiscardingHandler: class RejectHandler: name = 'rejecting' + def __init__(self, message): + self.message = message + def process(self, mlist, msg, msgdata): - raise RejectMessage('by test handler') + raise RejectMessage(self.message) @implementer(IPipeline) @@ -66,8 +69,11 @@ class RejectingPipeline: name = 'test-rejecting' description = 'Rejectinging test pipeline' + def __init__(self): + self.message = 'by test handler' + def __iter__(self): - yield RejectHandler() + yield RejectHandler(self.message) class TestPostingPipeline(unittest.TestCase): @@ -109,18 +115,49 @@ testing '"discarding": by test handler')) def test_rejecting_pipeline(self): - # If a handler in the pipeline raises DiscardMessage, the message will - # be thrown away, but with a log message. + # If a handler in the pipeline raises RejectMessage, the post will + # be bounced with a log message. mark = LogFileMark('mailman.vette') process(self._mlist, self._msg, {}, 'test-rejecting') line = mark.readline()[:-1] - self.assertTrue(line.endswith( + self.assertEqual( + line[-80:], + '<ant> rejected by "test-rejecting" pipeline handler ' + '"rejecting": by test handler', + line) + # In the rejection case, the original message will also be in the + # virgin queue. + items = get_queue_messages('virgin', expected_count=1) + self.assertEqual( + str(items[0].msg.get_payload(1).get_payload(0)['subject']), + 'a test') + # The first payload contains the rejection reason. + payload = items[0].msg.get_payload(0).get_payload() + self.assertEqual(payload, 'by test handler') + + def test_rejecting_pipeline_without_message(self): + # Similar to above, but without a rejection message. + pipeline = config.pipelines['test-rejecting'] + message = pipeline.message + self.addCleanup(setattr, pipeline, 'message', message) + pipeline.message = None + mark = LogFileMark('mailman.vette') + process(self._mlist, self._msg, {}, 'test-rejecting') + line = mark.readline()[:-1] + self.assertEqual( + line[-91:], '<ant> rejected by "test-rejecting" pipeline handler ' - '"rejecting": by test handler')) + '"rejecting": [No details are available]', + line) # In the rejection case, the original message will also be in the # virgin queue. items = get_queue_messages('virgin', expected_count=1) - self.assertEqual(str(items[0].msg['subject']), 'a test') + self.assertEqual( + str(items[0].msg.get_payload(1).get_payload(0)['subject']), + 'a test') + # The first payload contains the rejection reason. + payload = items[0].msg.get_payload(0).get_payload() + self.assertEqual(payload, '[No details are available]') def test_decorate_bulk(self): # Ensure that bulk postings get decorated with the footer. diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index 420e0fbe2..4000c135b 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -23,6 +23,7 @@ Bugs discarded. (Closes #369) * Various message holds and rejects that gave 'N/A' as a reason now give an appropriate reason. (Closes #368) +* Bounce messages are now composed for proper translations. Command line ------------ @@ -39,6 +40,8 @@ Interfaces both ``List-ID``s and fully qualified list names, since that's the most common use case. There's now a separate ``.get_by_fqdn()`` which only accepts the latter and mirrors the already existing ``.get_by_list_id()``. +* A new template ``list:user:notice:rejected`` has been added for customizing + the bounce message rejection notice. Other ----- diff --git a/src/mailman/interfaces/pipeline.py b/src/mailman/interfaces/pipeline.py index fac89c442..faccc52d2 100644 --- a/src/mailman/interfaces/pipeline.py +++ b/src/mailman/interfaces/pipeline.py @@ -17,13 +17,12 @@ """Interface for describing pipelines.""" +from mailman.core.i18n import _, format_reasons from public import public from zope.interface import Attribute, Interface -# For i18n extraction. -def _(s): - return s +NL = '\n' # These are thrown but they aren't exceptions so don't inherit from @@ -44,11 +43,20 @@ class DiscardMessage(BaseException): class RejectMessage(BaseException): """The message will be bounced back to the sender""" - def __init__(self, message=None): + def __init__(self, message=None, reasons=None, substitutions=None): self.message = message + self.reasons = reasons + self.substitutions = ({} if substitutions is None else substitutions) def __str__(self): - return self.message + if self.message is None: + return _('[No details are available]') + reasons = (_('[No reasons given]') + if self.reasons is None + else NL.join(format_reasons(self.reasons))) + substitutions = self.substitutions.copy() + substitutions['reasons'] = reasons + return _(self.message, substitutions) @public diff --git a/src/mailman/interfaces/template.py b/src/mailman/interfaces/template.py index 388e95942..ee2466f86 100644 --- a/src/mailman/interfaces/template.py +++ b/src/mailman/interfaces/template.py @@ -179,6 +179,7 @@ ALL_TEMPLATES = { 'list:user:notice:post', 'list:user:notice:probe', 'list:user:notice:refuse', + 'list:user:notice:rejected', 'list:user:notice:welcome', } } diff --git a/src/mailman/mta/deliver.py b/src/mailman/mta/deliver.py index db069bd36..e614d5bd1 100644 --- a/src/mailman/mta/deliver.py +++ b/src/mailman/mta/deliver.py @@ -90,15 +90,15 @@ def deliver(mlist, msg, msgdata): if size is None: size = len(msg.as_string()) substitutions = dict( - msgid = msg.get('message-id', 'n/a'), # noqa: E221, E251 - listname = mlist.fqdn_listname, # noqa: E221, E251 - sender = original_sender, # noqa: E221, E251 - recip = len(original_recipients), # noqa: E221, E251 - size = size, # noqa: E221, E251 - time = t1 - t0, # noqa: E221, E251 - refused = len(refused), # noqa: E221, E251 - smtpcode = 'n/a', # noqa: E221, E251 - smtpmsg = 'n/a', # noqa: E221, E251 + msgid = msg.get('message-id', 'n/a'), # noqa: E221,E251 + listname = mlist.fqdn_listname, # noqa: E221,E251 + sender = original_sender, # noqa: E221,E251 + recip = len(original_recipients), # noqa: E221,E251 + size = size, # noqa: E221,E251 + time = t1 - t0, # noqa: E221,E251 + refused = len(refused), # noqa: E221,E251 + smtpcode = 'n/a', # noqa: E221,E251 + smtpmsg = 'n/a', # noqa: E221,E251 ) template = config.logging.smtp.every if template.lower() != 'no': @@ -141,9 +141,9 @@ def deliver(mlist, msg, msgdata): template = config.logging.smtp.failure if template.lower() != 'no': substitutions.update( - recip = recipient, # noqa: E221, E251 - smtpcode = code, # noqa: E221, E251 - smtpmsg = smtp_message, # noqa: E221, E251 + recip = recipient, # noqa: E221,E251 + smtpcode = code, # noqa: E221,E251 + smtpmsg = smtp_message, # noqa: E221,E251 ) log.info('%s', expand(template, mlist, substitutions)) # Return the results diff --git a/src/mailman/rest/tests/test_domains.py b/src/mailman/rest/tests/test_domains.py index 3bacf7a72..bd6668a5b 100644 --- a/src/mailman/rest/tests/test_domains.py +++ b/src/mailman/rest/tests/test_domains.py @@ -346,6 +346,7 @@ class TestDomainTemplates(unittest.TestCase): 'list:user:notice:post': '', 'list:user:notice:probe': '', 'list:user:notice:refuse': '', + 'list:user:notice:rejected': '', 'list:user:notice:welcome': 'http://example.org/welcome', 'password': 'some password', 'username': 'anne.person', diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py index 75a81f3cb..0f3b99393 100644 --- a/src/mailman/rest/tests/test_lists.py +++ b/src/mailman/rest/tests/test_lists.py @@ -705,6 +705,7 @@ class TestListTemplates(unittest.TestCase): 'list:user:notice:post': '', 'list:user:notice:probe': '', 'list:user:notice:refuse': '', + 'list:user:notice:rejected': '', 'list:user:notice:welcome': 'http://example.org/welcome', 'password': 'some password', 'username': 'anne.person', diff --git a/src/mailman/rest/tests/test_root.py b/src/mailman/rest/tests/test_root.py index cb16caeeb..45adc6075 100644 --- a/src/mailman/rest/tests/test_root.py +++ b/src/mailman/rest/tests/test_root.py @@ -283,6 +283,7 @@ class TestSiteTemplates(unittest.TestCase): 'list:user:notice:post': '', 'list:user:notice:probe': '', 'list:user:notice:refuse': '', + 'list:user:notice:rejected': '', 'list:user:notice:welcome': 'http://example.org/welcome', 'password': 'some password', 'username': 'anne.person', diff --git a/src/mailman/rules/dmarc.py b/src/mailman/rules/dmarc.py index 9f9b11673..2741eb1df 100644 --- a/src/mailman/rules/dmarc.py +++ b/src/mailman/rules/dmarc.py @@ -298,15 +298,15 @@ class DMARCMitigation: if mlist.dmarc_mitigate_action is DMARCMitigateAction.no_mitigation: # Don't bother to check if we're not going to do anything. return False - dn, addr = parseaddr(msg.get('from')) - if maybe_mitigate(mlist, addr): + display_name, address = parseaddr(msg.get('from')) + if maybe_mitigate(mlist, address): # If dmarc_mitigate_action is discard or reject, this rule fires # and jumps to the 'moderation' chain to do the actual discard. # Otherwise, the rule misses but sets a flag for the dmarc handler # to do the appropriate action. msgdata['dmarc'] = True if mlist.dmarc_mitigate_action is DMARCMitigateAction.discard: - msgdata['moderation_action'] = 'discard' + msgdata['dmarc_action'] = 'discard' with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( @@ -324,9 +324,9 @@ class DMARCMitigation: 'contact the mailing list owner at ${listowner}.')) msgdata.setdefault('moderation_reasons', []).append( wrap(reason)) - msgdata['moderation_action'] = 'reject' + msgdata['dmarc_action'] = 'reject' else: return False - msgdata['moderation_sender'] = addr + msgdata['moderation_sender'] = address return True return False diff --git a/src/mailman/rules/docs/dmarc-mitigation.rst b/src/mailman/rules/docs/dmarc-mitigation.rst index bee7b3a55..cf7ceeb74 100644 --- a/src/mailman/rules/docs/dmarc-mitigation.rst +++ b/src/mailman/rules/docs/dmarc-mitigation.rst @@ -102,7 +102,7 @@ message. True >>> dump_msgdata(msgdata) dmarc : True - moderation_action : discard + dmarc_action : discard moderation_reasons: ['DMARC moderation'] moderation_sender : aperson@example.biz @@ -121,7 +121,7 @@ We can reject the message with a default reason. True >>> dump_msgdata(msgdata) dmarc : True - moderation_action : reject + dmarc_action : reject moderation_reasons: ['You are not allowed to post to this mailing list... moderation_sender : aperson@example.biz @@ -140,6 +140,6 @@ And, we can reject with a custom message. True >>> dump_msgdata(msgdata) dmarc : True - moderation_action : reject + dmarc_action : reject moderation_reasons: ['A silly reason'] moderation_sender : aperson@example.biz diff --git a/src/mailman/rules/docs/moderation.rst b/src/mailman/rules/docs/moderation.rst index d1cc5fd67..8bdcd2f1a 100644 --- a/src/mailman/rules/docs/moderation.rst +++ b/src/mailman/rules/docs/moderation.rst @@ -60,9 +60,9 @@ the eventual moderation chain. >>> member_rule.check(mlist, member_msg, msgdata) True >>> dump_msgdata(msgdata) - moderation_action : hold - moderation_reasons: ['The message comes from a moderated member'] - moderation_sender : aperson@example.com + member_moderation_action: hold + moderation_reasons : ['The message comes from a moderated member'] + moderation_sender : aperson@example.com Nonmembers @@ -107,9 +107,9 @@ rule matches and the message metadata again carries some useful information. >>> nonmember_rule.check(mlist, nonmember_msg, msgdata) True >>> dump_msgdata(msgdata) - moderation_action : hold - moderation_reasons: ['The message is not from a list member'] - moderation_sender : bperson@example.com + member_moderation_action: hold + moderation_reasons : ['The message is not from a list member'] + moderation_sender : bperson@example.com Of course, the nonmember action can be set to defer the decision, in which case the rule does not match. @@ -161,9 +161,9 @@ nonmember of the list. The rule also matches. >>> nonmember_rule.check(mlist, msg, msgdata) True >>> dump_msgdata(msgdata) - moderation_action : hold - moderation_reasons: ['The message is not from a list member'] - moderation_sender : cperson@example.com + member_moderation_action: hold + moderation_reasons : ['The message is not from a list member'] + moderation_sender : cperson@example.com >>> dump_list(mlist.members.members, key=memberkey) <Member: Anne Person <aperson@example.com> diff --git a/src/mailman/rules/moderation.py b/src/mailman/rules/moderation.py index 7f78d3ce7..26f04d632 100644 --- a/src/mailman/rules/moderation.py +++ b/src/mailman/rules/moderation.py @@ -82,7 +82,7 @@ class MemberModeration: elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. - msgdata['moderation_action'] = action.name + msgdata['member_moderation_action'] = action.name msgdata['moderation_sender'] = sender with _.defer_translation(): # This will be translated at the point of use. @@ -94,7 +94,7 @@ class MemberModeration: def _record_action(msgdata, action, sender, reason): - msgdata['moderation_action'] = action + msgdata['member_moderation_action'] = action msgdata['moderation_sender'] = sender msgdata.setdefault('moderation_reasons', []).append(reason) diff --git a/src/mailman/rules/tests/test_moderation.py b/src/mailman/rules/tests/test_moderation.py index e08bba480..46d848b42 100644 --- a/src/mailman/rules/tests/test_moderation.py +++ b/src/mailman/rules/tests/test_moderation.py @@ -142,9 +142,9 @@ A message body. msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result, 'NonmemberModeration rule should hit') - self.assertIn('moderation_action', msgdata) + self.assertIn('member_moderation_action', msgdata) self.assertEqual( - msgdata['moderation_action'], action_name, + msgdata['member_moderation_action'], action_name, 'Wrong action for {}: {}'.format(address, action_name)) def test_nonmember_fallback_to_list_defaults(self): @@ -166,7 +166,7 @@ A message body. msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) - self.assertEqual(msgdata['moderation_action'], 'hold') + self.assertEqual(msgdata['member_moderation_action'], 'hold') # As a side-effect, Anne has been added as a nonmember with a # moderation action that falls back to the list's default. anne = self._mlist.nonmembers.get_member('anne@example.com') @@ -177,7 +177,7 @@ A message body. # This time, the message should be discarded. result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) - self.assertEqual(msgdata.get('moderation_action'), 'discard') + self.assertEqual(msgdata.get('member_moderation_action'), 'discard') def test_member_fallback_to_list_defaults(self): # https://gitlab.com/mailman/mailman/issues/189 @@ -201,14 +201,14 @@ A message body. msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) - self.assertEqual(msgdata.get('moderation_action'), 'accept') + self.assertEqual(msgdata.get('member_moderation_action'), 'accept') # Then the list's default member action is changed. self._mlist.default_member_action = Action.hold msg.replace_header('Message-ID', '<bee>') # This time, the message is held. result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) - self.assertEqual(msgdata.get('moderation_action'), 'hold') + self.assertEqual(msgdata.get('member_moderation_action'), 'hold') def test_linked_address_nonmembermoderation_misses(self): # Anne subscribes to a mailing list as a user with her preferred diff --git a/src/mailman/templates/en/list:user:notice:rejected.txt b/src/mailman/templates/en/list:user:notice:rejected.txt new file mode 100644 index 000000000..b8e5aab53 --- /dev/null +++ b/src/mailman/templates/en/list:user:notice:rejected.txt @@ -0,0 +1,6 @@ +Your message to the $listname mailing-list was rejected for the following +reasons: + +$reasons + +The original message as received by Mailman is attached. |
