Configure SPF policy via sendmail access file.
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# A simple milter that has grown quite a bit.
|
# A simple milter that has grown quite a bit.
|
||||||
# $Log$
|
# $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
|
# Revision 1.25 2005/09/08 03:55:08 customdesigned
|
||||||
# Handle perverse MFROM quoting.
|
# Handle perverse MFROM quoting.
|
||||||
#
|
#
|
||||||
@@ -269,6 +274,7 @@ import traceback
|
|||||||
import ConfigParser
|
import ConfigParser
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
import anydbm
|
||||||
import Milter.dsn as dsn
|
import Milter.dsn as dsn
|
||||||
from Milter.dynip import is_dynip as dynip
|
from Milter.dynip import is_dynip as dynip
|
||||||
|
|
||||||
@@ -336,6 +342,8 @@ spf_accept_softfail = ()
|
|||||||
spf_accept_fail = ()
|
spf_accept_fail = ()
|
||||||
spf_best_guess = False
|
spf_best_guess = False
|
||||||
spf_reject_noptr = False
|
spf_reject_noptr = False
|
||||||
|
supply_sender = False
|
||||||
|
access_file = None
|
||||||
time_format = '%Y%b%d %H:%M:%S %Z'
|
time_format = '%Y%b%d %H:%M:%S %Z'
|
||||||
timeout = 600
|
timeout = 600
|
||||||
cbv_cache = {}
|
cbv_cache = {}
|
||||||
@@ -412,6 +420,7 @@ def read_config(list):
|
|||||||
'hashlength': '8',
|
'hashlength': '8',
|
||||||
'reject_spoofed': 'no',
|
'reject_spoofed': 'no',
|
||||||
'reject_noptr': 'no',
|
'reject_noptr': 'no',
|
||||||
|
'supply_sender': 'no',
|
||||||
'best_guess': 'no',
|
'best_guess': 'no',
|
||||||
'dspam_internal': 'yes'
|
'dspam_internal': 'yes'
|
||||||
})
|
})
|
||||||
@@ -487,7 +496,7 @@ def read_config(list):
|
|||||||
|
|
||||||
# spf section
|
# spf section
|
||||||
global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr
|
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:
|
if spf:
|
||||||
spf.DELEGATE = cp.getdefault('spf','delegate')
|
spf.DELEGATE = cp.getdefault('spf','delegate')
|
||||||
spf_reject_neutral = cp.getlist('spf','reject_neutral')
|
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_accept_fail = cp.getlist('spf','accept_fail')
|
||||||
spf_best_guess = cp.getboolean('spf','best_guess')
|
spf_best_guess = cp.getboolean('spf','best_guess')
|
||||||
spf_reject_noptr = cp.getboolean('spf','reject_noptr')
|
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')
|
srs_config = cp.getdefault('srs','config')
|
||||||
if srs_config: cp.read([srs_config])
|
if srs_config: cp.read([srs_config])
|
||||||
srs_secret = cp.getdefault('srs','secret')
|
srs_secret = cp.getdefault('srs','secret')
|
||||||
@@ -565,6 +576,76 @@ def parse_header(val):
|
|||||||
except email.Errors.HeaderParseError: pass
|
except email.Errors.HeaderParseError: pass
|
||||||
return val
|
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):
|
class bmsMilter(Milter.Milter):
|
||||||
"""Milter to replace attachments poisonous to Windows with a WARNING message,
|
"""Milter to replace attachments poisonous to Windows with a WARNING message,
|
||||||
check SPF, and other anti-forgery features, and implement wiretapping
|
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))
|
'SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s' % (q.s,q.i))
|
||||||
res,code,txt = q.check()
|
res,code,txt = q.check()
|
||||||
q.result = res
|
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
|
self.cbv_needed = q # report SPF syntax error to sender
|
||||||
res,code,txt = q.perm_error.ext # extended (lax processing) result
|
res,code,txt = q.perm_error.ext # extended (lax processing) result
|
||||||
txt = 'EXT: ' + txt
|
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 != '<>':
|
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)
|
h = spf.query(self.connectip,'',self.hello_name,receiver=receiver)
|
||||||
hres,hcode,htxt = h.check()
|
hres,hcode,htxt = h.check()
|
||||||
if hres in ('deny','fail','neutral','softfail'):
|
if hres in ('deny','fail','neutral','softfail'):
|
||||||
@@ -798,6 +880,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
and not dynip(self.hello_name,self.connectip):
|
and not dynip(self.hello_name,self.connectip):
|
||||||
hres,hcode,htxt = h.best_guess()
|
hres,hcode,htxt = h.best_guess()
|
||||||
else: hres = res
|
else: hres = res
|
||||||
|
ores = res
|
||||||
if spf_best_guess and res == 'none':
|
if spf_best_guess and res == 'none':
|
||||||
#self.log('SPF: no record published, guessing')
|
#self.log('SPF: no record published, guessing')
|
||||||
q.set_default_explanation(
|
q.set_default_explanation(
|
||||||
@@ -809,62 +892,82 @@ class bmsMilter(Milter.Milter):
|
|||||||
else:
|
else:
|
||||||
res,code,txt = q.best_guess()
|
res,code,txt = q.best_guess()
|
||||||
receiver += ': guessing'
|
receiver += ': guessing'
|
||||||
if q.perm_error:
|
if q.perm_error: # FIXME: should never happen?
|
||||||
res,code,txt = q.perm_error.ext # extended result
|
res,code,txt = q.perm_error.ext # extended result
|
||||||
txt = 'EXT: ' + txt
|
txt = 'EXT: ' + txt
|
||||||
if self.missing_ptr and res in ('neutral', 'none') and hres != 'pass':
|
if self.missing_ptr and ores == 'none' and res != 'pass' \
|
||||||
if spf_reject_noptr:
|
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.log('REJECT: no PTR, HELO or SPF')
|
||||||
self.setreply('550','5.7.1',
|
self.setreply('550','5.7.1',
|
||||||
'You must have a reverse lookup or publish SPF: http://spf.pobox.com',
|
"You must have a reverse lookup or publish SPF: http://spf.pobox.com",
|
||||||
'Contact your mail administrator IMMEDIATELY! Your mail server is',
|
"Contact your mail administrator IMMEDIATELY! Your mail server is",
|
||||||
'severely misconfigured. It has no PTR record (dynamic PTR records',
|
"severely misconfigured. It has no PTR record (dynamic PTR records",
|
||||||
"that contain your IP don't count), an invalid HELO, and no SPF record."
|
"that contain your IP don't count), an invalid HELO, and no SPF record."
|
||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if self.mailfrom != '<>':
|
|
||||||
self.cbv_needed = q
|
|
||||||
if res in ('deny', 'fail'):
|
if res in ('deny', 'fail'):
|
||||||
if hres == 'pass' and q.o in spf_accept_fail:
|
policy = p.getFailPolicy()
|
||||||
self.cbv_needed = q
|
if hres == 'pass' and policy == 'CBV':
|
||||||
else:
|
if self.mailfrom != '<>':
|
||||||
|
self.cbv_needed = q
|
||||||
|
elif policy != 'OK':
|
||||||
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
||||||
self.setreply(str(code),'5.7.1',txt)
|
self.setreply(str(code),'5.7.1',txt)
|
||||||
# A proper SPF fail error message would read:
|
# A proper SPF fail error message would read:
|
||||||
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
|
# 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>.
|
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if res == 'softfail' and not q.o in spf_accept_softfail:
|
if res == 'softfail':
|
||||||
if self.missing_ptr and hres != 'pass':
|
policy = p.getSoftfailPolicy()
|
||||||
if spf_reject_noptr or q.o in spf_reject_neutral:
|
if policy == 'CBV' and hres == 'pass':
|
||||||
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
if self.mailfrom != '<>':
|
||||||
self.setreply('550','5.7.1',
|
self.cbv_needed = q
|
||||||
'SPF softfail: If you get this Delivery Status Notice, your email',
|
elif policy != 'OK':
|
||||||
'was probably legitimate. Your administrator has published SPF',
|
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
||||||
'records in a testing mode. The SPF record reported your email as',
|
self.setreply('550','5.7.1',
|
||||||
'a forgery, which is a mistake if you are reading this. Please',
|
'SPF softfail: If you get this Delivery Status Notice, your email',
|
||||||
'notify your administrator of the problem immediately.'
|
'was probably legitimate. Your administrator has published SPF',
|
||||||
)
|
'records in a testing mode. The SPF record reported your email as',
|
||||||
return Milter.REJECT
|
'a forgery, which is a mistake if you are reading this. Please',
|
||||||
if self.mailfrom != '<>':
|
'notify your administrator of the problem immediately.'
|
||||||
self.cbv_needed = q
|
)
|
||||||
|
return Milter.REJECT
|
||||||
if res == 'neutral' and q.o in spf_reject_neutral:
|
if res == 'neutral' and q.o in spf_reject_neutral:
|
||||||
self.log('REJECT: SPF neutral for',q.s)
|
policy = p.getNeutralPolicy()
|
||||||
self.setreply('550','5.7.1',
|
if policy == 'CBV' and hres == 'pass':
|
||||||
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o,
|
if self.mailfrom != '<>':
|
||||||
'The %s domain is one that spammers love to forge. Due to' % q.o,
|
self.cbv_needed = q
|
||||||
'the volume of forged mail, we can only accept mail that',
|
elif policy != 'OK':
|
||||||
'the SPF record for %s explicitly designates as legitimate.' % q.o,
|
self.log('REJECT: SPF neutral for',q.s)
|
||||||
'Sending your email through the recommended outgoing SMTP',
|
self.setreply('550','5.7.1',
|
||||||
'servers for %s should accomplish this.' % q.o
|
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o,
|
||||||
)
|
'The %s domain is one that spammers love to forge. Due to' % q.o,
|
||||||
return Milter.REJECT
|
'the volume of forged mail, we can only accept mail that',
|
||||||
if res == 'unknown':
|
'the SPF record for %s explicitly designates as legitimate.' % q.o,
|
||||||
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
'Sending your email through the recommended outgoing SMTP',
|
||||||
# latest SPF draft recommends 5.5.2 instead of 5.7.1
|
'servers for %s should accomplish this.' % q.o
|
||||||
self.setreply(str(code),'5.5.2',txt)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if res == 'error':
|
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,
|
||||||
|
'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 in ('error','temperror'):
|
||||||
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
|
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
|
||||||
self.setreply(str(code),'4.3.0',txt)
|
self.setreply(str(code),'4.3.0',txt)
|
||||||
return Milter.TEMPFAIL
|
return Milter.TEMPFAIL
|
||||||
@@ -1043,10 +1146,10 @@ class bmsMilter(Milter.Milter):
|
|||||||
for name,val in self.new_headers:
|
for name,val in self.new_headers:
|
||||||
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
|
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
|
||||||
self.fp.write("\n") # terminate headers
|
self.fp.write("\n") # terminate headers
|
||||||
self.fp.seek(0)
|
|
||||||
# log when neither sender nor from domains matches mail from domain
|
# 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]
|
mf_domain = self.canon_from.split('@')[-1]
|
||||||
|
self.fp.seek(0)
|
||||||
msg = rfc822.Message(self.fp)
|
msg = rfc822.Message(self.fp)
|
||||||
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
|
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
|
||||||
t = parse_addr(hf)
|
t = parse_addr(hf)
|
||||||
@@ -1321,8 +1424,10 @@ class bmsMilter(Milter.Milter):
|
|||||||
q = self.cbv_needed
|
q = self.cbv_needed
|
||||||
if q.result in ('softfail','fail','deny'):
|
if q.result in ('softfail','fail','deny'):
|
||||||
template_name = 'softfail.txt'
|
template_name = 'softfail.txt'
|
||||||
elif q.result == 'unknown':
|
elif q.result in ('unknown','permerror'):
|
||||||
template_name = 'permerror.txt'
|
template_name = 'permerror.txt'
|
||||||
|
elif q.result == 'neutral':
|
||||||
|
template_name = 'neutral.txt'
|
||||||
else:
|
else:
|
||||||
template_name = 'strike3.txt'
|
template_name = 'strike3.txt'
|
||||||
rc = self.send_dsn(q,msg,template_name)
|
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.
|
# or an important sender is screwed up. Must have valid HELO, however.
|
||||||
;accept_fail = custhelp.com
|
;accept_fail = custhelp.com
|
||||||
# use sendmail access file or similar format for detailed spf policy
|
# use sendmail access file or similar format for detailed spf policy
|
||||||
|
# This will override any defaults set above
|
||||||
;access_file = /etc/mail/access.db
|
;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
|
# features intended to clean up outgoing mail
|
||||||
[scrub]
|
[scrub]
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
Reference in New Issue
Block a user