Skip to content
Snippets Groups Projects
Commit d34efa39 authored by Stuart Gathman's avatar Stuart Gathman
Browse files

Configure SPF policy via sendmail access file.

parent 36b5b4e6
No related branches found
No related tags found
No related merge requests found
#!/usr/bin/env python
# A simple milter that has grown quite a bit.
# $Log$
# Revision 1.26 2005/10/07 03:23:40 customdesigned
# Banned users option. Experimental feature to supply Sender when
# missing and MFROM domain doesn't match From. Log cipher bits for
# SMTP AUTH. Sketch access file feature.
#
# Revision 1.25 2005/09/08 03:55:08 customdesigned
# Handle perverse MFROM quoting.
#
......@@ -269,6 +274,7 @@ import traceback
import ConfigParser
import time
import re
import anydbm
import Milter.dsn as dsn
from Milter.dynip import is_dynip as dynip
......@@ -336,6 +342,8 @@ spf_accept_softfail = ()
spf_accept_fail = ()
spf_best_guess = False
spf_reject_noptr = False
supply_sender = False
access_file = None
time_format = '%Y%b%d %H:%M:%S %Z'
timeout = 600
cbv_cache = {}
......@@ -412,6 +420,7 @@ def read_config(list):
'hashlength': '8',
'reject_spoofed': 'no',
'reject_noptr': 'no',
'supply_sender': 'no',
'best_guess': 'no',
'dspam_internal': 'yes'
})
......@@ -487,7 +496,7 @@ def read_config(list):
# spf section
global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr
global spf_accept_softfail,spf_accept_fail
global spf_accept_softfail,spf_accept_fail,supply_sender,access_file
if spf:
spf.DELEGATE = cp.getdefault('spf','delegate')
spf_reject_neutral = cp.getlist('spf','reject_neutral')
......@@ -495,6 +504,8 @@ def read_config(list):
spf_accept_fail = cp.getlist('spf','accept_fail')
spf_best_guess = cp.getboolean('spf','best_guess')
spf_reject_noptr = cp.getboolean('spf','reject_noptr')
supply_sender = cp.getboolean('spf','supply_sender')
access_file = cp.getdefault('spf','access_file')
srs_config = cp.getdefault('srs','config')
if srs_config: cp.read([srs_config])
srs_secret = cp.getdefault('srs','secret')
......@@ -565,6 +576,76 @@ def parse_header(val):
except email.Errors.HeaderParseError: pass
return val
class SPFPolicy(object):
"Get SPF policy by result, defaulting to classic policy from pymilter.cfg"
def __init__(self,domain):
self.domain = domain.lower()
if access_file:
try: acf = anydbm.open(access_file,'r')
except: acf = None
else: acf = None
self.acf = acf
def getPolicy(self,pfx):
acf = self.acf
if not acf: return None
try:
return acf[pfx + self.domain]
except KeyError:
try:
return acf[pfx]
except KeyError:
return None
def getFailPolicy(self):
policy = self.getPolicy('SPF-Fail:')
if not policy:
if self.domain in spf_accept_fail:
policy = 'CBV'
else:
policy = 'REJECT'
return policy
def getNonePolicy(self):
policy = self.getPolicy('SPF-None:')
if not policy:
if spf_reject_noptr:
policy = 'REJECT'
else:
policy = 'CBV'
return policy
def getSoftfailPolicy(self):
policy = self.getPolicy('SPF-Softfail:')
if not policy:
if self.domain in spf_accept_softfail:
policy = 'OK'
elif self.domain in spf_reject_neutral:
policy = 'REJECT'
else:
policy = 'CBV'
return policy
def getNeutralPolicy(self):
policy = self.getPolicy('SPF-Neutral:')
if not policy:
if self.domain in spf_reject_neutral:
policy = 'REJECT'
policy = 'OK'
return policy
def getPermErrorPolicy(self):
policy = self.getPolicy('SPF-PermError:')
if not policy:
policy = 'REJECT'
return policy
def getPassPolicy(self):
policy = self.getPolicy('SPF-Pass:')
if not policy:
policy = 'OK'
return policy
class bmsMilter(Milter.Milter):
"""Milter to replace attachments poisonous to Windows with a WARNING message,
check SPF, and other anti-forgery features, and implement wiretapping
......@@ -776,13 +857,14 @@ class bmsMilter(Milter.Milter):
'SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s' % (q.s,q.i))
res,code,txt = q.check()
q.result = res
if res == 'unknown' and q.perm_error and q.perm_error.ext:
if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext:
self.cbv_needed = q # report SPF syntax error to sender
res,code,txt = q.perm_error.ext # extended (lax processing) result
txt = 'EXT: ' + txt
if res in ('none','softfail','deny','fail'):
p = SPFPolicy(q.o)
if res in ('none','softfail','deny','fail','neutral'):
if self.mailfrom != '<>':
# check hello name via spf
# check hello name via spf unless spf pass
h = spf.query(self.connectip,'',self.hello_name,receiver=receiver)
hres,hcode,htxt = h.check()
if hres in ('deny','fail','neutral','softfail'):
......@@ -798,6 +880,7 @@ class bmsMilter(Milter.Milter):
and not dynip(self.hello_name,self.connectip):
hres,hcode,htxt = h.best_guess()
else: hres = res
ores = res
if spf_best_guess and res == 'none':
#self.log('SPF: no record published, guessing')
q.set_default_explanation(
......@@ -809,34 +892,43 @@ class bmsMilter(Milter.Milter):
else:
res,code,txt = q.best_guess()
receiver += ': guessing'
if q.perm_error:
if q.perm_error: # FIXME: should never happen?
res,code,txt = q.perm_error.ext # extended result
txt = 'EXT: ' + txt
if self.missing_ptr and res in ('neutral', 'none') and hres != 'pass':
if spf_reject_noptr:
if self.missing_ptr and ores == 'none' and res != 'pass' \
and hres != 'pass':
policy = p.getNonePolicy()
if policy == 'CBV':
if self.mailfrom != '<>':
q.result = ores
self.cbv_needed = q # accept, but inform sender via DSN
elif policy != 'OK':
self.log('REJECT: no PTR, HELO or SPF')
self.setreply('550','5.7.1',
'You must have a reverse lookup or publish SPF: http://spf.pobox.com',
'Contact your mail administrator IMMEDIATELY! Your mail server is',
'severely misconfigured. It has no PTR record (dynamic PTR records',
"You must have a reverse lookup or publish SPF: http://spf.pobox.com",
"Contact your mail administrator IMMEDIATELY! Your mail server is",
"severely misconfigured. It has no PTR record (dynamic PTR records",
"that contain your IP don't count), an invalid HELO, and no SPF record."
)
return Milter.REJECT
if self.mailfrom != '<>':
self.cbv_needed = q
if res in ('deny', 'fail'):
if hres == 'pass' and q.o in spf_accept_fail:
policy = p.getFailPolicy()
if hres == 'pass' and policy == 'CBV':
if self.mailfrom != '<>':
self.cbv_needed = q
else:
elif policy != 'OK':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt)
# A proper SPF fail error message would read:
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
return Milter.REJECT
if res == 'softfail' and not q.o in spf_accept_softfail:
if self.missing_ptr and hres != 'pass':
if spf_reject_noptr or q.o in spf_reject_neutral:
if res == 'softfail':
policy = p.getSoftfailPolicy()
if policy == 'CBV' and hres == 'pass':
if self.mailfrom != '<>':
self.cbv_needed = q
elif policy != 'OK':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply('550','5.7.1',
'SPF softfail: If you get this Delivery Status Notice, your email',
......@@ -846,9 +938,12 @@ class bmsMilter(Milter.Milter):
'notify your administrator of the problem immediately.'
)
return Milter.REJECT
if res == 'neutral' and q.o in spf_reject_neutral:
policy = p.getNeutralPolicy()
if policy == 'CBV' and hres == 'pass':
if self.mailfrom != '<>':
self.cbv_needed = q
if res == 'neutral' and q.o in spf_reject_neutral:
elif policy != 'OK':
self.log('REJECT: SPF neutral for',q.s)
self.setreply('550','5.7.1',
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o,
......@@ -859,12 +954,20 @@ class bmsMilter(Milter.Milter):
'servers for %s should accomplish this.' % q.o
)
return Milter.REJECT
if res == 'unknown':
if res in ('unknown','permerror'):
policy = p.getPermErrorPolicy()
if policy == 'CBV' and hres == 'pass':
if self.mailfrom != '<>':
self.cbv_needed = q
elif policy != 'OK':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
# latest SPF draft recommends 5.5.2 instead of 5.7.1
self.setreply(str(code),'5.5.2',txt)
self.setreply(str(code),'5.5.2',txt,
'There is a fatal syntax error in the SPF record for %s' % q.o,
'We cannot accept mail from %s until this is corrected.' % q.o
)
return Milter.REJECT
if res == 'error':
if res in ('error','temperror'):
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'4.3.0',txt)
return Milter.TEMPFAIL
......@@ -1043,10 +1146,10 @@ class bmsMilter(Milter.Milter):
for name,val in self.new_headers:
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
self.fp.write("\n") # terminate headers
self.fp.seek(0)
# log when neither sender nor from domains matches mail from domain
if self.mailfrom != '<>':
if supply_sender and self.mailfrom != '<>':
mf_domain = self.canon_from.split('@')[-1]
self.fp.seek(0)
msg = rfc822.Message(self.fp)
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
t = parse_addr(hf)
......@@ -1321,8 +1424,10 @@ class bmsMilter(Milter.Milter):
q = self.cbv_needed
if q.result in ('softfail','fail','deny'):
template_name = 'softfail.txt'
elif q.result == 'unknown':
elif q.result in ('unknown','permerror'):
template_name = 'permerror.txt'
elif q.result == 'neutral':
template_name = 'neutral.txt'
else:
template_name = 'strike3.txt'
rc = self.send_dsn(q,msg,template_name)
......
......@@ -93,7 +93,12 @@ reject_spoofed = 0
# or an important sender is screwed up. Must have valid HELO, however.
;accept_fail = custhelp.com
# use sendmail access file or similar format for detailed spf policy
# This will override any defaults set above
;access_file = /etc/mail/access.db
# Add MAIL FROM as Sender when Sender is missing and From domain
# doesn't match MAIL FROM. Outlook and other email clients will then display
# something like: "Sent by sender@domain.com on behalf of from@example.com"
;supply_sender = 0
# features intended to clean up outgoing mail
[scrub]
......
Subject: SPF %(result)s (POSSIBLE FORGERY)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your sender policy (or lack thereof) indicated that the above email was not
sent via an authorized SMTP server, but may still be legitimate. Since there
is no positive confirmation that the message is really from you, we have
to give it extra scrutiny - including verifying that the sender really
exists by sending you this DSN. We will remember this sender and not
bother you again for while. You can avoid this message entirely for
legitimate mail by using an authorized SMTP server. Contact your mail
administrator and ask how to configure your email client to use an
authorized server.
If you never sent the above message, then your domain has been forged.
Your mail admin needs to publish a strict SPF record so that I can reject
those forgeries instead of bugging you about them.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
postmaster@%(receiver)s
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment