aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Mailman/Gui/Privacy.py24
-rw-r--r--Mailman/Handlers/Moderate.py25
-rwxr-xr-xMailman/MailList.py2
-rw-r--r--Mailman/Utils.py39
-rwxr-xr-xMailman/versions.py2
5 files changed, 92 insertions, 0 deletions
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</a> 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."""),
+ _("""<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 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