summaryrefslogtreecommitdiff
path: root/src/mailman/chains
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/chains')
-rw-r--r--src/mailman/chains/base.py19
-rw-r--r--src/mailman/chains/headers.py5
-rw-r--r--src/mailman/chains/hold.py11
-rw-r--r--src/mailman/chains/reject.py4
-rw-r--r--src/mailman/chains/tests/test_headers.py55
-rw-r--r--src/mailman/chains/tests/test_hold.py7
-rw-r--r--src/mailman/chains/tests/test_reject.py2
7 files changed, 94 insertions, 9 deletions
diff --git a/src/mailman/chains/base.py b/src/mailman/chains/base.py
index 59125ba69..9507d8fbc 100644
--- a/src/mailman/chains/base.py
+++ b/src/mailman/chains/base.py
@@ -18,6 +18,7 @@
"""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
@@ -27,6 +28,24 @@ 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."""
diff --git a/src/mailman/chains/headers.py b/src/mailman/chains/headers.py
index 6fc061fe4..01ba96971 100644
--- a/src/mailman/chains/headers.py
+++ b/src/mailman/chains/headers.py
@@ -105,6 +105,11 @@ class HeaderMatchRule:
if isinstance(value, Header):
value = value.encode()
if re.search(self.pattern, value, re.IGNORECASE):
+ msgdata['moderation_sender'] = msg.sender
+ with _.defer_translation():
+ # This will be translated at the point of use.
+ msgdata.setdefault('moderation_reasons', []).append(
+ (_('Header "{}" matched a header rule'), str(value)))
return True
return False
diff --git a/src/mailman/chains/hold.py b/src/mailman/chains/hold.py
index 4e72b685a..edc80da3c 100644
--- a/src/mailman/chains/hold.py
+++ b/src/mailman/chains/hold.py
@@ -24,7 +24,7 @@ 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
+from mailman.chains.base import TerminalChainBase, format_reasons
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import UserNotification
@@ -57,8 +57,8 @@ def _compose_reasons(msgdata, column=66):
# Rules can add reasons to the metadata.
reasons = msgdata.get('moderation_reasons', [_('N/A')])
return NL.join(
- [(SPACE * 4) + wrap(_(reason), column=column)
- for reason in reasons])
+ [(SPACE * 4) + wrap(reason, column=column)
+ for reason in format_reasons(reasons)])
def autorespond_to_sender(mlist, sender, language=None):
@@ -142,7 +142,7 @@ class HoldChain(TerminalChainBase):
rule_misses = msgdata.get('rule_misses')
if rule_misses:
msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
- reasons = msgdata.get('moderation_reasons', ['n/a'])
+ reasons = format_reasons(msgdata.get('moderation_reasons', ['n/a']))
# Hold the message by adding it to the list's request database.
request_id = hold_message(mlist, msg, msgdata, SEMISPACE.join(reasons))
# Calculate a confirmation token to send to the author of the
@@ -251,7 +251,8 @@ also appear in the first line of the body of the reply.""")),
# Log the held message. Log messages are not translated, so recast
# the reasons in the English.
with _.using('en'):
- reasons = msgdata.get('moderation_reasons', ['N/A'])
+ reasons = format_reasons(
+ msgdata.get('moderation_reasons', ['N/A']))
log.info('HOLD: %s post from %s held, message-id=%s: %s',
mlist.fqdn_listname, msg.sender,
msg.get('message-id', 'n/a'), SEMISPACE.join(reasons))
diff --git a/src/mailman/chains/reject.py b/src/mailman/chains/reject.py
index 3284bba9f..31f66c8fa 100644
--- a/src/mailman/chains/reject.py
+++ b/src/mailman/chains/reject.py
@@ -20,7 +20,7 @@
import logging
from mailman.app.bounces import bounce_message
-from mailman.chains.base import TerminalChainBase
+from mailman.chains.base import TerminalChainBase, format_reasons
from mailman.core.i18n import _
from mailman.interfaces.chain import RejectEvent
from mailman.interfaces.pipeline import RejectMessage
@@ -65,7 +65,7 @@ reasons:
The original message as received by Mailman is attached.
""").format(
list_name=mlist.display_name, # noqa: E122
- reasons=NEWLINE.join(reasons)
+ reasons=NEWLINE.join(format_reasons(reasons))
))
bounce_message(mlist, msg, error)
log.info('REJECT: %s', msg.get('message-id', 'n/a'))
diff --git a/src/mailman/chains/tests/test_headers.py b/src/mailman/chains/tests/test_headers.py
index cd4c932cc..2aae503b2 100644
--- a/src/mailman/chains/tests/test_headers.py
+++ b/src/mailman/chains/tests/test_headers.py
@@ -25,7 +25,8 @@ from mailman.chains.headers import HeaderMatchRule, make_link
from mailman.config import config
from mailman.core.chains import process
from mailman.email.message import Message
-from mailman.interfaces.chain import DiscardEvent, HoldEvent, LinkAction
+from mailman.interfaces.chain import (
+ DiscardEvent, HoldEvent, LinkAction, RejectEvent)
from mailman.interfaces.mailinglist import IHeaderMatchList
from mailman.testing.helpers import (
LogFileMark, configuration, event_subscribers,
@@ -343,3 +344,55 @@ A message body.
# ...and are actually the identical objects.
for link1, link2 in zip(links_1, links_2):
self.assertIs(link1.rule, link2.rule)
+
+ def test_hold_returns_reason(self):
+ # Test that a match with hold action returns a reason
+ msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+Subject: Bad subject
+Message-ID: <ant>
+
+body
+
+""")
+ msgdata = {}
+ header_matches = IHeaderMatchList(self._mlist)
+ header_matches.append('Subject', 'Bad', 'hold')
+ # This event subscriber records the event that occurs when the message
+ # is processed by the owner chain.
+ events = []
+ with event_subscribers(events.append):
+ process(self._mlist, msg, msgdata, start_chain='header-match')
+ self.assertEqual(len(events), 1)
+ event = events[0]
+ self.assertIsInstance(event, HoldEvent)
+ self.assertEqual(msgdata['moderation_reasons'],
+ [('Header "{}" matched a header rule',
+ 'Bad subject')])
+
+ def test_reject_returns_reason(self):
+ # Test that a match with reject action returns a reason
+ msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+Subject: Bad subject
+Message-ID: <ant>
+
+body
+
+""")
+ msgdata = {}
+ header_matches = IHeaderMatchList(self._mlist)
+ header_matches.append('Subject', 'Bad', 'reject')
+ # This event subscriber records the event that occurs when the message
+ # is processed by the owner chain.
+ events = []
+ with event_subscribers(events.append):
+ process(self._mlist, msg, msgdata, start_chain='header-match')
+ self.assertEqual(len(events), 1)
+ event = events[0]
+ self.assertIsInstance(event, RejectEvent)
+ self.assertEqual(msgdata['moderation_reasons'],
+ [('Header "{}" matched a header rule',
+ 'Bad subject')])
diff --git a/src/mailman/chains/tests/test_hold.py b/src/mailman/chains/tests/test_hold.py
index b973b874c..560916e3b 100644
--- a/src/mailman/chains/tests/test_hold.py
+++ b/src/mailman/chains/tests/test_hold.py
@@ -111,6 +111,7 @@ A message body.
msgdata = dict(moderation_reasons=[
'TEST-REASON-1',
'TEST-REASON-2',
+ ('TEST-{}-REASON-{}', 'FORMAT', 3),
])
logfile = LogFileMark('mailman.vette')
process_chain(self._mlist, msg, msgdata, start_chain='hold')
@@ -126,18 +127,22 @@ A message body.
self.fail('Unexpected message: %s' % item.msg)
self.assertIn(' TEST-REASON-1', payloads['owner'])
self.assertIn(' TEST-REASON-2', payloads['owner'])
+ self.assertIn(' TEST-FORMAT-REASON-3', payloads['owner'])
self.assertIn(' TEST-REASON-1', payloads['sender'])
self.assertIn(' TEST-REASON-2', payloads['sender'])
+ self.assertIn(' TEST-FORMAT-REASON-3', payloads['sender'])
logged = logfile.read()
self.assertIn('TEST-REASON-1', logged)
self.assertIn('TEST-REASON-2', logged)
+ self.assertIn('TEST-FORMAT-REASON-3', logged)
# Check the reason passed to hold_message().
requests = IListRequests(self._mlist)
self.assertEqual(requests.count_of(RequestType.held_message), 1)
request = requests.of_type(RequestType.held_message)[0]
key, data = requests.get_request(request.id)
self.assertEqual(
- data.get('_mod_reason'), 'TEST-REASON-1; TEST-REASON-2')
+ data.get('_mod_reason'),
+ 'TEST-REASON-1; TEST-REASON-2; TEST-FORMAT-REASON-3')
def test_hold_chain_no_reasons_given(self):
msg = mfs("""\
diff --git a/src/mailman/chains/tests/test_reject.py b/src/mailman/chains/tests/test_reject.py
index f6fd6a8fe..8fce1de4a 100644
--- a/src/mailman/chains/tests/test_reject.py
+++ b/src/mailman/chains/tests/test_reject.py
@@ -45,12 +45,14 @@ Subject: Ignore
msgdata = dict(moderation_reasons=[
'TEST-REASON-1',
'TEST-REASON-2',
+ ('TEST-{}-REASON-{}', 'FORMAT', 3),
])
process_chain(self._mlist, self._msg, msgdata, start_chain='reject')
bounces = get_queue_messages('virgin', expected_count=1)
payload = bounces[0].msg.get_payload(0).as_string()
self.assertIn('TEST-REASON-1', payload)
self.assertIn('TEST-REASON-2', payload)
+ self.assertIn('TEST-FORMAT-REASON-3', payload)
def test_no_reason(self):
# There may be no moderation reasons.