diff options
Diffstat (limited to 'Mailman')
-rw-r--r-- | Mailman/Gui/Privacy.py | 24 | ||||
-rw-r--r-- | Mailman/Handlers/Moderate.py | 19 | ||||
-rwxr-xr-x | Mailman/MailList.py | 2 | ||||
-rw-r--r-- | Mailman/Utils.py | 83 | ||||
-rwxr-xr-x | Mailman/versions.py | 3 |
5 files changed, 131 insertions, 0 deletions
diff --git a/Mailman/Gui/Privacy.py b/Mailman/Gui/Privacy.py index 75eff2b5..90560bff 100644 --- a/Mailman/Gui/Privacy.py +++ b/Mailman/Gui/Privacy.py @@ -235,6 +235,30 @@ class Privacy(GUIBase): >rejection notice</a> to be sent to moderated members who post to this list.""")), + ('dmarc_moderation_action', mm_cfg.Radio, + (_('Accept'), _('Hold'), _('Reject'), _('Discard')), 0, + _("""Action to take when anyone posts to the + list from a domain with a DMARC Reject/Quarantine Policy."""), + _("""<ul><li><b>Hold</b> -- this holds the message for approval + by the list moderators. + + <p><li><b>Reject</b> -- this automatically rejects the message by + sending a bounce notice to the post's author. The text of the + bounce notice can be <a + href="?VARHELP=privacy/sender/dmarc_moderation_notice" + >configured by you</a>. + + <p><li><b>Discard</b> -- this simply discards the message, with + no notice sent to the post's author. + </ul>""")), + + ('dmarc_moderation_notice', mm_cfg.Text, (10, WIDTH), 1, + _("""Text to include in any + <a href="?VARHELP/privacy/sender/dmarc_moderation_action" + >rejection notice</a> to + be sent to anyone who posts to this list from a domain + with DMARC Reject/Quarantine 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 42dffb88..884030de 100644 --- a/Mailman/Handlers/Moderate.py +++ b/Mailman/Handlers/Moderate.py @@ -56,6 +56,25 @@ def process(mlist, msg, msgdata): else: sender = None if sender: + if Utils.IsDmarcProhibited(sender): + # Note that for dmarc_moderation_action, 0 = Accept, + # 1 = Hold, 2 = Reject, 3 = Discard + if mlist.dmarc_moderation_action == 1: + msgdata['sender'] = sender + Hold.hold_for_approval(mlist, msg, msgdata, + ModeratedMemberPost) + elif mlist.dmarc_moderation_action == 2: + # 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 == 3: + raise Errors.DiscardMessage + # 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 cdebb507..d13ca169 100755 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -389,6 +389,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # 2==Discard self.member_moderation_action = 0 self.member_moderation_notice = '' + self.dmarc_moderation_action = mm_cfg.DEFAULT_DMARC_MODERATION_ACTION + 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..37ae940b 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -35,6 +35,7 @@ import errno import base64 import random import urlparse +import collections import htmlentitydefs import email.Header import email.Iterators @@ -71,6 +72,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 +1065,78 @@ 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 = 3 + resolver.lifetime = 5 + txt_recs = resolver.query(dmarc_domain, dns.rdatatype.TXT) + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + 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: +# people are already being dumb, don't trust them to provide honest DNS +# where the answer section only contains what was asked for, nor to include +# CNAMEs before the values they point to. + full_record = "" + results_by_name = collections.defaultdict(list) + cnames = {} + want_names = set([dmarc_domain + '.']) + for txt_rec in txt_recs.response.answer: + if txt_rec.rdtype == dns.rdatatype.CNAME: + cnames[txt_rec.name.to_text()] = txt_rec.items[0].target.to_text() + if txt_rec.rdtype != dns.rdatatype.TXT: + continue + results_by_name[txt_rec.name.to_text()].append("".join(txt_rec.items[0].strings)) + expands = list(want_names) + seen = set(expands) + while expands: + item = expands.pop(0) + if item in cnames: + if cnames[item] in seen: + continue # cname loop + expands.append(cnames[item]) + seen.add(cnames[item]) + want_names.add(cnames[item]) + want_names.discard(item) + + if len(want_names) != 1: + syslog('error', 'multiple DMARC entries in results for %s, processing each to be strict', + dmarc_domain) + for name in want_names: + if name not in results_by_name: + continue + dmarcs = filter(lambda n: n.startswith('v=DMARC1;'), results_by_name[name]) + if len(dmarcs) == 0: + return False + if len(dmarcs) > 1: + syslog('error', 'RRset of TXT records for %s has %d v=DMARC1 entries; testing them all', + dmarc_domain, len(dmarc)) + for entry in dmarcs: + if re.search(r'\bp=reject\b', entry, re.IGNORECASE): + syslog('info', 'DMARC lookup for %s (%s) found p=reject in %s = %s', + email, dmarc_domain, name, entry) + return True + + if re.search(r'\bp=quarantine\b', entry, re.IGNORECASE): + syslog('info', 'DMARC lookup for %s (%s) found p=quarantine in %s = %s', + email, dmarc_domain, name, entry) + return True + + return False + + diff --git a/Mailman/versions.py b/Mailman/versions.py index 31c4f470..db5b2914 100755 --- a/Mailman/versions.py +++ b/Mailman/versions.py @@ -388,6 +388,9 @@ 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', + mm_cfg.DEFAULT_DMARC_MODERATION_ACTION) + add_only_if_missing('dmarc_moderation_notice', '') add_only_if_missing('new_member_options', mm_cfg.DEFAULT_NEW_MEMBER_OPTIONS) # Emergency moderation flag |