diff options
| author | Barry Warsaw | 2017-01-04 15:10:55 +0000 |
|---|---|---|
| committer | Barry Warsaw | 2017-01-04 15:10:55 +0000 |
| commit | 9ecb0d462b79bec42e82b6aecea8853262e15753 (patch) | |
| tree | dc422ff2eec850387fcdb7a3c326f8d3a267a16a /src/mailman/handlers | |
| parent | 8cb4546b9a2bf87a3e2d465651686ca26a5404db (diff) | |
| parent | ae3eeb642b452ee844ba7a2999361f31c3c327c7 (diff) | |
| download | mailman-9ecb0d462b79bec42e82b6aecea8853262e15753.tar.gz mailman-9ecb0d462b79bec42e82b6aecea8853262e15753.tar.zst mailman-9ecb0d462b79bec42e82b6aecea8853262e15753.zip | |
Merge branch 'dmarc' into 'master'
Implement DMARC mitigations in mailman
Closes #247
See merge request !215
Diffstat (limited to 'src/mailman/handlers')
| -rw-r--r-- | src/mailman/handlers/cook_headers.py | 2 | ||||
| -rw-r--r-- | src/mailman/handlers/dmarc.py | 226 | ||||
| -rw-r--r-- | src/mailman/handlers/docs/dmarc-mitigations.rst | 162 | ||||
| -rw-r--r-- | src/mailman/handlers/tests/test_dmarc.py | 441 |
4 files changed, 830 insertions, 1 deletions
diff --git a/src/mailman/handlers/cook_headers.py b/src/mailman/handlers/cook_headers.py index 672738bf9..413304ea2 100644 --- a/src/mailman/handlers/cook_headers.py +++ b/src/mailman/handlers/cook_headers.py @@ -114,7 +114,7 @@ def process(mlist, msg, msgdata): d[lcaddr] = pair new.append(pair) # List admin wants an explicit Reply-To: added - if mlist.reply_goes_to_list == ReplyToMunging.explicit_header: + if mlist.reply_goes_to_list is ReplyToMunging.explicit_header: add(parseaddr(mlist.reply_to_address)) # If we're not first stripping existing Reply-To: then we need to add # the original Reply-To:'s to the list we're building up. In both diff --git a/src/mailman/handlers/dmarc.py b/src/mailman/handlers/dmarc.py new file mode 100644 index 000000000..13707d397 --- /dev/null +++ b/src/mailman/handlers/dmarc.py @@ -0,0 +1,226 @@ +# Copyright (C) 2016 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/>. + +"""Do DMARC Munge From and Wrap Message actions. + +This does the work of modifying the messages From: and Cc: or Reply-To: or +wrapping the message in an outer message with From: and Cc: or Reply-To: +as appropriate to avoid issues because of the original From: domain's DMARC +policy. It does this either to selected messages flagged by the DMARC +moderation rule based on list settings and the original From: domain's DMARC +policy or to all messages based on list settings.""" + +import re +import copy +import logging + +from email.header import Header, decode_header +from email.mime.message import MIMEMessage +from email.mime.text import MIMEText +from email.utils import formataddr, getaddresses, make_msgid +from mailman.core.i18n import _ +from mailman.interfaces.handler import IHandler +from mailman.interfaces.mailinglist import DMARCMitigateAction, ReplyToMunging +from mailman.utilities.string import wrap +from public import public +from zope.interface import implementer + + +log = logging.getLogger('mailman.error') + +COMMASPACE = ', ' +EMPTYSTRING = '' +MAXLINELEN = 78 +NONASCII = re.compile('[^\s!-~]') +# Headers from the original that we want to keep in the wrapper. These are +# actually regexps matched with re.match so they match anything that starts +# with the given string unless they end with '$'. +KEEPERS = ( + 'archived-at', + 'date', + 'in-reply-to', + 'list-', + 'precedence', + 'references', + 'subject', + 'to', + 'x-mailman-', + ) + + +def munged_headers(mlist, msg, msgdata): + # This returns a list of tuples (header, content) where header is the + # name of a header to be added to or replaced in the wrapper or message + # for DMARC mitigation. It sets From: to the string + # 'original From: display name' via 'list name' <list posting address> + # and adds the original From: to Reply-To: or Cc: per the following. + # Our goals for this process are not completely compatible, so we do + # the best we can. Our goals are: + # 1) as long as the list is not anonymous, the original From: address + # should be obviously exposed, i.e. not just in a header that MUAs + # don't display. + # 2) the original From: address should not be in a comment or display + # name in the new From: because it is claimed that multiple domains + # in any fields in From: are indicative of spamminess. This means + # it should be in Reply-To: or Cc:. + # 3) the behavior of an MUA doing a 'reply' or 'reply all' should be + # consistent regardless of whether or not the From: is munged. + # Goal 3) implies sometimes the original From: should be in Reply-To: + # and sometimes in Cc:, and even so, this goal won't be achieved in + # all cases with all MUAs. In cases of conflict, the above ordering of + # goals is priority order. + # + # Be as robust as possible here. + all_froms = getaddresses(msg.get_all('from', [])) + # Strip the nulls and bad emails. + froms = [email for email in all_froms if '@' in email[1]] + if len(froms) == 1: + realname, email = original_from = froms[0] + else: + # No From: or multiple addresses. Just punt and take + # the get_sender result. + realname = '' + email = msgdata['original_sender'] + original_from = (realname, email) + # If there was no display name in the email header, see if we have a + # matching member with a display name. + if len(realname) == 0: + member = mlist.members.get_member(email) + if member: + realname = member.display_name or email + else: + realname = email + # Remove the domain from realname if it looks like an email address. + realname = re.sub(r'@([^ .]+\.)+[^ .]+$', '---', realname) + # Make a display name and RFC 2047 encode it if necessary. This is + # difficult and kludgy. If the realname came from From: it should be + # ASCII or RFC 2047 encoded. If it came from the member record, it should + # be a string. If it's from the email address, it should be an ASCII + # string. In any case, ensure it's an unencoded string. + realname_bits = [] + for fragment, charset in decode_header(realname): + if not charset: + # Character set should be ASCII, but use iso-8859-1 anyway. + charset = 'iso-8859-1' + if not isinstance(fragment, str): + realname_bits.append(str(fragment, charset, errors='replace')) + else: + realname_bits.append(fragment) + # The member's display name is a string. + realname = EMPTYSTRING.join(realname_bits) + # Ensure the i18n context is the list's preferred_language. + with _.using(mlist.preferred_language.code): + via = _('$realname via $mlist.display_name') + # Get an RFC 2047 encoded header string. + display_name = str(Header(via, mlist.preferred_language.charset)) + value = [('From', formataddr((display_name, mlist.posting_address)))] + # We've made the munged From:. Now put the original in Reply-To: or Cc: + if mlist.reply_goes_to_list is ReplyToMunging.no_munging: + # Add original from to Reply-To: + add_to = 'Reply-To' + else: + # Add original from to Cc: + add_to = 'Cc' + original = getaddresses(msg.get_all(add_to, [])) + if original_from[1] not in [x[1] for x in original]: + original.append(original_from) + value.append((add_to, COMMASPACE.join(formataddr(x) for x in original))) + return value + + +def munge_from(mlist, msg, msgdata): + for key, value in munged_headers(mlist, msg, msgdata): + del msg[key] + msg[key] = value + return + + +def wrap_message(mlist, msg, msgdata): + # Create a wrapper message around the original. + # + # There are various headers in msg that we don't want, so we basically + # make a copy of the message, then delete almost everything and set/copy + # what we want. + original_msg = copy.deepcopy(msg) + for key in msg: + keep = False + for keeper in KEEPERS: + if re.match(keeper, key, re.IGNORECASE): + keep = True + break + if not keep: + del msg[key] + msg['MIME-Version'] = '1.0' + msg['Message-ID'] = make_msgid() + for key, value in munged_headers(mlist, original_msg, msgdata): + msg[key] = value + # Are we including dmarc_wrapped_message_text? + if len(mlist.dmarc_wrapped_message_text) > 0: + part1 = MIMEText( + wrap(mlist.dmarc_wrapped_message_text), + 'plain', + mlist.preferred_language.charset) + part1['Content-Disposition'] = 'inline' + part2 = MIMEMessage(original_msg) + part2['Content-Disposition'] = 'inline' + msg['Content-Type'] = 'multipart/mixed' + msg.set_payload([part1, part2]) + else: + msg['Content-Type'] = 'message/rfc822' + msg['Content-Disposition'] = 'inline' + msg.set_payload([original_msg]) + return + + +def process(mlist, msg, msgdata): + # If we're mitigating on policy and we have no hit, return. + if not msgdata.get('dmarc') and not mlist.dmarc_mitigate_unconditionally: + return + # If we're not mitigating, return. + if mlist.dmarc_mitigate_action is DMARCMitigateAction.no_mitigation: + return + if mlist.anonymous_list: + # DMARC mitigation is not required for anonymous lists. + return + if msgdata.get('dmarc') or mlist.dmarc_mitigate_unconditionally: + if mlist.dmarc_mitigate_action is DMARCMitigateAction.munge_from: + munge_from(mlist, msg, msgdata) + elif mlist.dmarc_mitigate_action is DMARCMitigateAction.wrap_message: + wrap_message(mlist, msg, msgdata) + else: + # We can get here if DMARCMitigateAction is reject or discard but + # the From: domain has no reject or quarantine policy and + # mlist.dmarc_mitigate_unconditionally is True. Log and ignore + # this. + log.error('Invalid DMARC combination for list: %s', mlist) + return + else: + raise AssertionError( + 'handlers/dmarc.py: no hit and unconditional is False') + + +@public +@implementer(IHandler) +class DMARC: + """Apply DMARC mitigations.""" + + name = 'dmarc' + description = _('Apply DMARC mitigations.') + + def process(self, mlist, msg, msgdata): + """See `IHandler`.""" + process(mlist, msg, msgdata) diff --git a/src/mailman/handlers/docs/dmarc-mitigations.rst b/src/mailman/handlers/docs/dmarc-mitigations.rst new file mode 100644 index 000000000..12a0de0ff --- /dev/null +++ b/src/mailman/handlers/docs/dmarc-mitigations.rst @@ -0,0 +1,162 @@ +================= +DMARC Mitigations +================= + +In order to mitigate the effects of DMARC_ on mailing list traffic, list +administrators have the ability to apply transformations to messages delivered +to list members. These transformations are applied only to individual +messages sent to list members and not to messages in digests, archives, or +gated via NNTP. + +The messages can be transformed by either munging the ``From:`` header and +putting original ``From:`` in ``Cc:`` or ``Reply-To:``, or by wrapping the +original message in an outer message ``From:`` the list. + +Exactly which transformations are applied depends on a number of list settings. + +The settings and their effects are: + +``anonymous_list`` + If True, no mitigations are ever applied because the message is already + ``From:`` the list. +``dmarc_mitigate_action`` + The action to apply to messages ``From:`` a domain publishing a DMARC + policy of **reject** or **quarantine**, or to all messages depending on the + setting of ``dmarc_mitigate_unconditionally``. +``dmarc_mitigate_unconditionally`` + If True, apply ``dmarc_mitigate_action`` to all messages, but only if + ``dmarc_mitigate_action`` is neither ``reject`` or ``discard``. +``dmarc_moderation_notice`` + Text to include in any rejection notice to be sent when + ``dmarc_policy_mitigation`` of ``reject`` applies. This overrides the + built-in default text. +``dmarc_wrapped_message_text`` + Text to be added as a separate ``text/plain`` MIME part preceding the + original message part in the wrapped message when a ``wrap_message`` + mitigation applies. If this is not provided the separate ``text/plain`` + MIME part is not added. +``reply_goes_to_list`` + If this is set to other than no-munging of ``Reply-To:``, the original + ``From:`` goes in ``Cc:`` rather than ``Reply-To:``. This is intended to + make MUA functions of *reply* and *reply-all* have the same effect with + messages to which mitigations have been applied as they do with other + messages. + +The possible actions for ``dmarc_mitigate_action`` are: + +``no_mitigation`` + Make no transformation to the message. +``munge_from`` + Change the ``From:`` header and put the original ``From:`` in ``Reply-To:`` + or in some cases ``Cc:``. +``wrap_message`` + Wrap the message in an outer message with headers from the original message + as in ``munge_from``. +``reject`` + Bounce the message back to the sender with a default reason or one supplied + in ``dmarc_moderation_notice``. +``discard`` + Silently discard the message. + +Here's what happens when we munge the ``From:``. +:: + + >>> from mailman.interfaces.mailinglist import ( + ... DMARCMitigateAction, ReplyToMunging) + + >>> mlist = create_list('ant@example.com') + >>> mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + >>> mlist.reply_goes_to_list = ReplyToMunging.no_munging + + >>> msg = message_from_string("""\ + ... From: Anne Person <aperson@example.com> + ... To: ant@example.com + ... + ... A message of great import. + ... """) + >>> msgdata = dict(dmarc=True, original_sender='aperson@example.com') + + >>> from mailman.handlers.dmarc import process + >>> process(mlist, msg, msgdata) + >>> print(msg.as_string()) + To: ant@example.com + From: Anne Person via Ant <ant@example.com> + Reply-To: Anne Person <aperson@example.com> + <BLANKLINE> + A message of great import. + <BLANKLINE> + +Here we wrap the message without adding a text part. +:: + + >>> mlist.dmarc_mitigate_action = DMARCMitigateAction.wrap_message + >>> mlist.dmarc_wrapped_message_text = '' + + >>> msg = message_from_string("""\ + ... From: Anne Person <aperson@example.com> + ... To: ant@example.com + ... + ... A message of great import. + ... """) + >>> msgdata = dict(dmarc=True, original_sender='aperson@example.com') + + >>> process(mlist, msg, msgdata) + >>> print(msg.as_string()) + To: ant@example.com + MIME-Version: 1.0 + Message-ID: <...> + From: Anne Person via Ant <ant@example.com> + Reply-To: Anne Person <aperson@example.com> + Content-Type: message/rfc822 + Content-Disposition: inline + <BLANKLINE> + From: Anne Person <aperson@example.com> + To: ant@example.com + <BLANKLINE> + A message of great import. + <BLANKLINE> + +And here's a wrapped message with an added text part. +:: + + >>> mlist.dmarc_wrapped_message_text = 'The original message is attached.' + + >>> msg = message_from_string("""\ + ... From: Anne Person <aperson@example.com> + ... To: ant@example.com + ... + ... A message of great import. + ... """) + >>> msgdata = dict(dmarc=True, original_sender='aperson@example.com') + + >>> process(mlist, msg, msgdata) + >>> print(msg.as_string()) + To: ant@example.com + MIME-Version: 1.0 + Message-ID: <...> + From: Anne Person via Ant <ant@example.com> + Reply-To: Anne Person <aperson@example.com> + Content-Type: multipart/mixed; boundary="..." + <BLANKLINE> + --... + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Disposition: inline + <BLANKLINE> + The original message is attached. + --... + Content-Type: message/rfc822 + MIME-Version: 1.0 + Content-Disposition: inline + <BLANKLINE> + From: Anne Person <aperson@example.com> + To: ant@example.com + <BLANKLINE> + A message of great import. + <BLANKLINE> + --...-- + <BLANKLINE> + + +.. _DMARC: https://wikipedia.org/wiki/DMARC diff --git a/src/mailman/handlers/tests/test_dmarc.py b/src/mailman/handlers/tests/test_dmarc.py new file mode 100644 index 000000000..bd1e2a1d6 --- /dev/null +++ b/src/mailman/handlers/tests/test_dmarc.py @@ -0,0 +1,441 @@ +# Copyright (C) 2016 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 DMARC handler.""" + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.app.membership import add_member +from mailman.handlers import dmarc +from mailman.interfaces.mailinglist import DMARCMitigateAction, ReplyToMunging +from mailman.interfaces.subscriptions import RequestRecord +from mailman.testing.helpers import specialized_message_from_string as mfs +from mailman.testing.layers import ConfigLayer + + +class TestDMARCMitigations(unittest.TestCase): + """Test the dmarc handler.""" + + layer = ConfigLayer + maxDiff = None + + def setUp(self): + self._mlist = create_list('ant@example.com') + self._mlist.anonymous_list = False + self._mlist.dmarc_policy_mitigate_action = ( + DMARCMitigateAction.no_mitigation) + self._mlist.dmarc_wrapped_message_text = '' + self._mlist.dmarc_mitigate_unconditionally = False + self._mlist.reply_goes_to_list = ReplyToMunging.no_munging + # We can use the same message text for most tests. + self._text = """\ +From: anne@example.com +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""" + + def test_default_no_change(self): + msg = mfs(self._text) + dmarc.process(self._mlist, msg, {}) + self.assertMultiLineEqual(msg.as_string(), self._text) + + def test_anonymous_no_change(self): + self._mlist.anonymous_list = True + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + msgdata = {'dmarc': True} + msg = mfs(self._text) + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), self._text) + + def test_no_mitigation_no_change_1(self): + msg = mfs(self._text) + self._mlist.dmarc_mitigate_unconditionally = True + dmarc.process(self._mlist, msg, {}) + self.assertMultiLineEqual(msg.as_string(), self._text) + + def test_no_mitigation_no_change_2(self): + msg = mfs(self._text) + msgdata = {'dmarc': True} + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), self._text) + + def test_action_reject_mitigate_unconditionally(self): + msg = mfs(self._text) + self._mlist.dmarc_mitigate_unconditionally = True + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.reject + dmarc.process(self._mlist, msg, {}) + self.assertMultiLineEqual(msg.as_string(), self._text) + + def test_action_discard_mitigate_unconditionally(self): + msg = mfs(self._text) + self._mlist.dmarc_mitigate_unconditionally = True + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.discard + dmarc.process(self._mlist, msg, {}) + self.assertMultiLineEqual(msg.as_string(), self._text) + + def test_action_munge_from(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + msgdata = {'dmarc': True} + msg = mfs(self._text) + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" +From: anne--- via Ant <ant@example.com> +Reply-To: anne@example.com + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""") + + def test_action_munge_from_no_from(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + msgdata = dict( + dmarc=True, + original_sender='anne@example.com', + ) + msg = mfs(self._text) + del msg['from'] + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" +From: anne--- via Ant <ant@example.com> +Reply-To: anne@example.com + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""") + + def test_action_munge_multiple_froms(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + msgdata = dict( + dmarc=True, + original_sender='cate@example.com', + ) + msg = mfs(self._text) + # Put multiple addresses in the From: header. The msgdata must + # contain a key naming the "original sender" as determined by the + # Message.sender attribute. + del msg['from'] + msg['From'] = 'anne@example.com, bart@example.com' + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" +From: cate--- via Ant <ant@example.com> +Reply-To: cate@example.com + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""") + + def test_action_munge_from_display_name_in_from(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + msgdata = {'dmarc': True} + msg = mfs(self._text) + del msg['from'] + msg['From'] = 'Anne Person <anne@example.com>' + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" +From: Anne Person via Ant <ant@example.com> +Reply-To: Anne Person <anne@example.com> + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""") + + def test_action_munge_from_display_name_in_list(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + add_member( + self._mlist, + RequestRecord('anne@example.com', 'Anna Banana') + ) + msgdata = {'dmarc': True} + msg = mfs(self._text) + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" +From: Anna Banana via Ant <ant@example.com> +Reply-To: anne@example.com + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""") + + def test_no_action_without_msgdata(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + msg = mfs(self._text) + dmarc.process(self._mlist, msg, {}) + self.assertMultiLineEqual(msg.as_string(), self._text) + + def test_unconditional_no_msgdata(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + self._mlist.dmarc_mitigate_unconditionally = True + msg = mfs(self._text) + dmarc.process(self._mlist, msg, {}) + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" +From: anne--- via Ant <ant@example.com> +Reply-To: anne@example.com + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""") + + def test_from_in_cc(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + self._mlist.reply_goes_to_list = ReplyToMunging.point_to_list + msgdata = {'dmarc': True} + msg = mfs(self._text) + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" +From: anne--- via Ant <ant@example.com> +Cc: anne@example.com + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""") + + def test_wrap_message_unconditionally(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.wrap_message + self._mlist.dmarc_mitigate_unconditionally = True + msg = mfs(self._text) + dmarc.process(self._mlist, msg, {}) + # We can't predict the Message-ID in the wrapper so delete it, but + # ensure we have one. + self.assertIsNotNone(msg.get('message-id')) + del msg['message-id'] + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Date: Fri, 1 Jan 2016 00:00:01 +0000 +MIME-Version: 1.0 +From: anne--- via Ant <ant@example.com> +Reply-To: anne@example.com +Content-Type: message/rfc822 +Content-Disposition: inline + +""" + self._text) + + def test_wrap_message(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.wrap_message + msgdata = {'dmarc': True} + msg = mfs(self._text) + dmarc.process(self._mlist, msg, msgdata) + # We can't predict the Message-ID in the wrapper so delete it, but + # ensure we have one. + self.assertIsNotNone(msg.get('message-id')) + del msg['message-id'] + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Date: Fri, 1 Jan 2016 00:00:01 +0000 +MIME-Version: 1.0 +From: anne--- via Ant <ant@example.com> +Reply-To: anne@example.com +Content-Type: message/rfc822 +Content-Disposition: inline + +""" + self._text) + + def test_wrap_message_cc(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.wrap_message + self._mlist.reply_goes_to_list = ReplyToMunging.point_to_list + msgdata = {'dmarc': True} + msg = mfs(self._text) + dmarc.process(self._mlist, msg, msgdata) + # We can't predict the Message-ID in the wrapper so delete it, but + # ensure we have one. + self.assertIsNotNone(msg.get('message-id')) + del msg['message-id'] + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Date: Fri, 1 Jan 2016 00:00:01 +0000 +MIME-Version: 1.0 +From: anne--- via Ant <ant@example.com> +Cc: anne@example.com +Content-Type: message/rfc822 +Content-Disposition: inline + +""" + self._text) + + def test_rfc2047_encoded_from(self): + self._mlist.dmarc_mitigate_action = DMARCMitigateAction.munge_from + msgdata = {'dmarc': True} + msg = mfs(self._text) + del msg['from'] + msg['From'] = '=?iso-8859-1?Q?A_Pers=F3n?= <anne@example.com>' + dmarc.process(self._mlist, msg, msgdata) + self.assertMultiLineEqual(msg.as_string(), """\ +To: ant@example.com +Subject: A subject +X-Mailman-Version: X.Y +Message-ID: <alpha@example.com> +Date: Fri, 1 Jan 2016 00:00:01 +0000 +Another-Header: To test removal in wrapper +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="=====abc==" +From: =?utf-8?q?A_Pers=C3=B3n_via_Ant?= <ant@example.com> +Reply-To: =?iso-8859-1?Q?A_Pers=F3n?= <anne@example.com> + +--=====abc== +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Some things to say. +--=====abc== +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +<html><head></head><body>Some things to say.</body></html> +--=====abc==-- +""") |
