diff options
| -rw-r--r-- | src/mailman/rules/docs/dmarc-moderation.rst | 56 | ||||
| -rw-r--r-- | src/mailman/rules/tests/test_dmarc.py | 81 |
2 files changed, 115 insertions, 22 deletions
diff --git a/src/mailman/rules/docs/dmarc-moderation.rst b/src/mailman/rules/docs/dmarc-moderation.rst index c2210011a..8aab08161 100644 --- a/src/mailman/rules/docs/dmarc-moderation.rst +++ b/src/mailman/rules/docs/dmarc-moderation.rst @@ -2,18 +2,23 @@ DMARC moderation ================ -This rule is different from others in that it never matches bucause a match -would cause the message to be held. The rule looks at the list's -dmarc_moderation_policy and if it is other than 'none', it checks the domain -of the From: address for a DMARC policy and depending on settings may reject -or discard the message or just flag in for the dmarc handler to apply DMARC -mitigations to the message. +This rule only matches in order to jump to the moderation chain to reject +or discard the message. The rule looks at the list's dmarc_moderation_policy +and if it is other than 'none', it checks the domain of the From: address for +a DMARC policy and depending on settings may reject or discard the message or +just flag it for the dmarc handler to apply DMARC mitigations to the message. >>> mlist = create_list('_xtest@example.com') >>> rule = config.rules['dmarc-moderation'] >>> print(rule.name) dmarc-moderation +First we set up a mock patcher to return predictable responses to DNS lookups. +This returns p=reject for the example.biz domain and not for any others. + + >>> from mailman.rules.tests.test_dmarc import get_dns_resolver + >>> patcher = get_dns_resolver() + A message From: a domain without a DMARC policy does not set any flags. >>> from mailman.interfaces.mailinglist import DMARCModerationAction @@ -25,7 +30,8 @@ A message From: a domain without a DMARC policy does not set any flags. ... ... """) >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> with patcher as Resolver: + ... rule.check(mlist, msg, msgdata) False >>> msgdata == {} True @@ -35,13 +41,14 @@ action is none. >>> mlist.dmarc_moderation_action = DMARCModerationAction.none >>> msg = message_from_string("""\ - ... From: aperson@yahoo.com + ... From: aperson@example.biz ... To: _xtest@example.com ... Subject: A posted message ... ... """) >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> with patcher as Resolver: + ... rule.check(mlist, msg, msgdata) False >>> msgdata == {} True @@ -50,13 +57,14 @@ But with a different list setting, the message is flagged. >>> mlist.dmarc_moderation_action = DMARCModerationAction.munge_from >>> msg = message_from_string("""\ - ... From: aperson@yahoo.com + ... From: aperson@example.biz ... To: _xtest@example.com ... Subject: A posted message ... ... """) >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> with patcher as Resolver: + ... rule.check(mlist, msg, msgdata) False >>> msgdata['dmarc'] True @@ -64,13 +72,14 @@ But with a different list setting, the message is flagged. Subdomains which don't have a policy will check the organizational domain. >>> msg = message_from_string("""\ - ... From: aperson@sub.domain.yahoo.com + ... From: aperson@sub.domain.example.biz ... To: _xtest@example.com ... Subject: A posted message ... ... """) >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> with patcher as Resolver: + ... rule.check(mlist, msg, msgdata) False >>> msgdata['dmarc'] True @@ -80,14 +89,15 @@ message. >>> mlist.dmarc_moderation_action = DMARCModerationAction.discard >>> msg = message_from_string("""\ - ... From: aperson@yahoo.com + ... From: aperson@example.biz ... To: _xtest@example.com ... Subject: A posted message - ... Message-ID: <xxx_message_id@yahoo.com> + ... Message-ID: <xxx_message_id@example.biz> ... ... """) >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> with patcher as Resolver: + ... rule.check(mlist, msg, msgdata) True >>> msgdata['dmarc'] True @@ -98,14 +108,15 @@ We can reject the message with a default reason. >>> mlist.dmarc_moderation_action = DMARCModerationAction.reject >>> msg = message_from_string("""\ - ... From: aperson@yahoo.com + ... From: aperson@example.biz ... To: _xtest@example.com ... Subject: A posted message - ... Message-ID: <xxx_message_id@yahoo.com> + ... Message-ID: <xxx_message_id@example.biz> ... ... """) >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> with patcher as Resolver: + ... rule.check(mlist, msg, msgdata) True >>> msgdata['dmarc'] True @@ -118,14 +129,15 @@ And, we can reject with a custom message. >>> mlist.dmarc_moderation_notice = 'A silly reason' >>> msg = message_from_string("""\ - ... From: aperson@yahoo.com + ... From: aperson@example.biz ... To: _xtest@example.com ... Subject: A posted message - ... Message-ID: <xxx_message_id@yahoo.com> + ... Message-ID: <xxx_message_id@example.biz> ... ... """) >>> msgdata = {} - >>> rule.check(mlist, msg, msgdata) + >>> with patcher as Resolver: + ... rule.check(mlist, msg, msgdata) True >>> msgdata['dmarc'] True diff --git a/src/mailman/rules/tests/test_dmarc.py b/src/mailman/rules/tests/test_dmarc.py new file mode 100644 index 000000000..49aeea8c6 --- /dev/null +++ b/src/mailman/rules/tests/test_dmarc.py @@ -0,0 +1,81 @@ +# 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/>. + +"""Support for mocking dnspython calls from dmarc rules.""" + +from dns.rdatatype import TXT +from dns.resolver import NXDOMAIN, NoAnswer +from mailman import public +from unittest import mock + + +@public +def get_dns_resolver(): + """Create a dns.resolver.Resolver mock. + + This is used to return a predictable response to a _dmarc query. It + returns p=reject for the example.biz domain and raises either NXDOMAIN + or NoAnswer for any other. + + It only implements those classes and attributes used by the dmarc rule. + """ + class Name: + # mock answer.name + def __init__(self): + pass + + def to_text(self): + return '_dmarc.example.biz.' + + class Item: + # mock answer.items + def __init__(self): + self.strings = [b'v=DMARC1; p=reject;'] + + class Ans_e: + # mock answer element + def __init__(self): + self.rdtype = TXT + self.items = [Item()] + self.name = Name() + + class Answer: + # mock answer + def __init__(self): + self.answer = [Ans_e()] + + class Resolver: + # mock dns.resolver.Resolver class. + def __init__(self): + pass + + def query(self, domain, data_type): + if data_type != TXT: + raise NoAnswer + dparts = domain.split('.') + if len(dparts) < 3: + raise NXDOMAIN + if len(dparts) > 3: + raise NoAnswer + if dparts[0] != '_dmarc': + raise NoAnswer + if dparts[1] != 'example' or dparts[2] != 'biz': + raise NXDOMAIN + self.response = Answer() + return self + patcher = mock.patch('dns.resolver.Resolver', Resolver) + return patcher |
