From 990f7226da9deb667e6b026ba7ae24ef728900c4 Mon Sep 17 00:00:00 2001 From: Jim Popovitch Date: Sun, 3 Mar 2013 03:04:37 -0500 Subject: Hold/Reject/Discard moderation support for Senders with a DMARC p=reject policy --- Mailman/Gui/Privacy.py | 24 ++++++++++++++++++++++++ Mailman/Handlers/Moderate.py | 25 +++++++++++++++++++++++++ Mailman/MailList.py | 2 ++ Mailman/Utils.py | 39 +++++++++++++++++++++++++++++++++++++++ Mailman/versions.py | 2 ++ 5 files changed, 92 insertions(+) diff --git a/Mailman/Gui/Privacy.py b/Mailman/Gui/Privacy.py index 75eff2b5..ae78b209 100644 --- a/Mailman/Gui/Privacy.py +++ b/Mailman/Gui/Privacy.py @@ -235,6 +235,30 @@ class Privacy(GUIBase): >rejection notice to be sent to moderated members who post to this list.""")), + ('dmarc_moderation_action', mm_cfg.Radio, + (_('Hold'), _('Reject'), _('Discard')), 0, + _("""Action to take when anyone posts to the + list from a domain with a DMARC Reject Policy."""), + _("""""")), + + ('dmarc_moderation_notice', mm_cfg.Text, (10, WIDTH), 1, + _("""Text to include in any + rejection notice to + be sent to anyone who posts to this list from a domain + with DMARC Reject Policy.""")), + _('Non-member filters'), ('accept_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1, diff --git a/Mailman/Handlers/Moderate.py b/Mailman/Handlers/Moderate.py index 199c97ac..81b554e4 100644 --- a/Mailman/Handlers/Moderate.py +++ b/Mailman/Handlers/Moderate.py @@ -56,6 +56,31 @@ def process(mlist, msg, msgdata): else: sender = None if sender: + if Utils.IsDmarcProhibited(sender): + # Note that for dmarc_moderation_action, 0==Hold, 1=Reject, + # 2==Discard + if mlist.dmarc_moderation_action == 0: + msgdata['sender'] = sender + Hold.hold_for_approval(mlist, msg, msgdata, + ModeratedMemberPost) + elif mlist.dmarc_moderation_action == 1: + # Reject + text = mlist.dmarc_moderation_notice + if text: + text = Utils.wrap(text) + else: + # Use the default RejectMessage notice string + text = None + raise Errors.RejectMessage, text + elif mlist.dmarc_moderation_action == 2: + raise Errors.DiscardMessage + else: + assert 0, 'bad dmarc_moderation_action' + + # sender's domain has a 'p=reject' _dmarc TXT record, + # we should NOT automatically reflect this email + return + # If the member's moderation flag is on, then perform the moderation # action. if mlist.getMemberOption(sender, mm_cfg.Moderate): diff --git a/Mailman/MailList.py b/Mailman/MailList.py index 2d653acb..a51f4ea6 100755 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -388,6 +388,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # 2==Discard self.member_moderation_action = 0 self.member_moderation_notice = '' + self.dmarc_moderation_action = 0 + self.dmarc_moderation_notice = '' self.accept_these_nonmembers = [] self.hold_these_nonmembers = [] self.reject_these_nonmembers = [] diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 93e1fba1..a2cc0caa 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -71,6 +71,13 @@ except NameError: True = 1 False = 0 +try: + import dns.resolver + from dns.exception import DNSException + dns_resolver = True +except ImportError: + dns_resolver = False + EMPTYSTRING = '' UEMPTYSTRING = u'' NL = '\n' @@ -1057,3 +1064,35 @@ def suspiciousHTML(html): else: return False + +# This takes an email address, and returns True if DMARC policy is p=reject +def IsDmarcProhibited(email): + if not dns_resolver: + return False + + email = email.lower() + at_sign = email.find('@') + if at_sign < 1: + return False + dmarc_domain = '_dmarc.' + email[at_sign+1:] + + try: + resolver = dns.resolver.Resolver() + resolver.timeout = 1 + resolver.lifetime = 5 + txt_recs = resolver.query(dmarc_domain, dns.rdatatype.TXT) + except dns.resolver.NXDOMAIN: + return False + except DNSException, e: + syslog('error', 'DNSException: Unable to query DMARC policy for %s (%s). %s', + email, dmarc_domain, e.__class__) + return False + else: + for txt_rec in txt_recs.response.answer: + assert( txt_rec.rdtype == dns.rdatatype.TXT) + if re.search(r"[^s]p=reject", "".join(txt_rec.items[0].strings), re.IGNORECASE): + return True + + return False + + diff --git a/Mailman/versions.py b/Mailman/versions.py index 84943efc..02d266da 100755 --- a/Mailman/versions.py +++ b/Mailman/versions.py @@ -385,6 +385,8 @@ def NewVars(l): # the current GUI description model. So, 0==Hold, 1==Reject, 2==Discard add_only_if_missing('member_moderation_action', 0) add_only_if_missing('member_moderation_notice', '') + add_only_if_missing('dmarc_moderation_action', 0) + add_only_if_missing('dmarc_moderation_notice', '') add_only_if_missing('new_member_options', mm_cfg.DEFAULT_NEW_MEMBER_OPTIONS) # Emergency moderation flag -- cgit v1.2.3