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

Add config file to spfmilter

parent 139e141e
Branches
Tags
No related merge requests found
from ConfigParser import ConfigParser
class MilterConfigParser(ConfigParser):
def __init__(self,defaults={}):
ConfigParser.__init__(self)
self.defaults = defaults
# The defaults provided by ConfigParser show up in all sections,
# which screws up iterating over all options in a section.
# Worse, passing "defaults" with vars= overrides the config file!
# So we roll our own defaults.
def get(self,sect,opt):
if not self.has_option(sect,opt) and opt in self.defaults:
return self.defaults[opt]
return ConfigParser.get(self,sect,opt)
def getlist(self,sect,opt):
if self.has_option(sect,opt):
return [q.strip() for q in self.get(sect,opt).split(',')]
return []
def getaddrset(self,sect,opt):
if not self.has_option(sect,opt):
return {}
s = self.get(sect,opt)
d = {}
for q in s.split(','):
q = q.strip()
if q.startswith('file:'):
domain = q[5:].lower()
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
else:
user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user)
return d
def getaddrdict(self,sect,opt):
if not self.has_option(sect,opt):
return {}
d = {}
for q in self.get(sect,opt).split(','):
q = q.strip()
if self.has_option(sect,q):
l = self.get(sect,q)
for addr in l.split(','):
addr = addr.strip()
if addr.startswith('file:'):
fname = addr[5:]
for a in open(fname,'r').read().split():
d[a] = q
else:
d[addr] = q
return d
def getdefault(self,sect,opt,default=None):
if self.has_option(sect,opt):
return self.get(sect,opt)
return default
#!/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.81 2007/01/05 23:33:55 customdesigned
# Make blacklist an AddrCache
#
# Revision 1.80 2007/01/05 23:12:12 customdesigned # Revision 1.80 2007/01/05 23:12:12 customdesigned
# Move parse_addr, iniplist, ip4re to Milter.utils # Move parse_addr, iniplist, ip4re to Milter.utils
# #
...@@ -49,7 +52,6 @@ import mime ...@@ -49,7 +52,6 @@ import mime
import email.Errors import email.Errors
import Milter import Milter
import tempfile import tempfile
from ConfigParser import ConfigParser
import time import time
import socket import socket
import struct import struct
...@@ -60,6 +62,7 @@ import anydbm ...@@ -60,6 +62,7 @@ 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
from Milter.utils import iniplist,parse_addr,ip4re from Milter.utils import iniplist,parse_addr,ip4re
from Milter.config import MilterConfigParser
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from email.Header import decode_header from email.Header import decode_header
...@@ -167,64 +170,6 @@ milter_log = logging.getLogger('milter') ...@@ -167,64 +170,6 @@ milter_log = logging.getLogger('milter')
if gossip: if gossip:
gossip_node = Gossip('gossip4.db',120) gossip_node = Gossip('gossip4.db',120)
class MilterConfigParser(ConfigParser):
def __init__(self,defaults):
ConfigParser.__init__(self)
self.defaults = defaults
# The defaults provided by ConfigParser show up in all sections,
# which screws up iterating over all options in a section.
# Worse, passing "defaults" with vars= overrides the config file!
# So we roll our own defaults.
def get(self,sect,opt):
if not self.has_option(sect,opt) and opt in self.defaults:
return self.defaults[opt]
return ConfigParser.get(self,sect,opt)
def getlist(self,sect,opt):
if self.has_option(sect,opt):
return [q.strip() for q in self.get(sect,opt).split(',')]
return []
def getaddrset(self,sect,opt):
if not self.has_option(sect,opt):
return {}
s = self.get(sect,opt)
d = {}
for q in s.split(','):
q = q.strip()
if q.startswith('file:'):
domain = q[5:].lower()
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
else:
user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user)
return d
def getaddrdict(self,sect,opt):
if not self.has_option(sect,opt):
return {}
d = {}
for q in self.get(sect,opt).split(','):
q = q.strip()
if self.has_option(sect,q):
l = self.get(sect,q)
for addr in l.split(','):
addr = addr.strip()
if addr.startswith('file:'):
fname = addr[5:]
for a in open(fname,'r').read().split():
d[a] = q
else:
d[addr] = q
return d
def getdefault(self,sect,opt,default=None):
if self.has_option(sect,opt):
return self.get(sect,opt)
return default
def read_config(list): def read_config(list):
cp = MilterConfigParser({ cp = MilterConfigParser({
'tempdir': "/var/log/milter/save", 'tempdir': "/var/log/milter/save",
...@@ -393,7 +338,7 @@ def parse_header(val): ...@@ -393,7 +338,7 @@ def parse_header(val):
return val return val
class SPFPolicy(object): class SPFPolicy(object):
"Get SPF policy by result, defaulting to classic policy from pymilter.cfg" "Get SPF policy by result from sendmail style access file."
def __init__(self,sender): def __init__(self,sender):
self.sender = sender self.sender = sender
self.domain = sender.split('@')[-1].lower() self.domain = sender.split('@')[-1].lower()
......
...@@ -78,7 +78,7 @@ reject_spoofed = 0 ...@@ -78,7 +78,7 @@ reject_spoofed = 0
# refuses mail from user names commonly abused in that way. # refuses mail from user names commonly abused in that way.
;banned_users = postmaster, mailer-daemon, clamav ;banned_users = postmaster, mailer-daemon, clamav
# See http://spf.pobox.com for more info on SPF. # See http://www.openspf.com for more info on SPF.
[spf] [spf]
# namespace where SPF records can be supplied for domains without one # namespace where SPF records can be supplied for domains without one
# records are searched for under _spf.domain.com # records are searched for under _spf.domain.com
......
[milter]
# The socket used to communicate with sendmail
socketname = /tmp/spfmiltersock
# Name of the milter given to sendmail
name = pyspffilter
# Trusted relays such as secondary MXes that should not have SPF checked.
;trusted_relay =
# Internal networks that should not have SPF checked.
internal_connect = 127.0.0.1,192.168.0.0/16
# See http://www.openspf.com for more info on SPF.
[spf]
# Use sendmail access map or similar format for detailed spf policy.
# SPF entries in the access map will override defaults.
;access_file = /etc/mail/access.db
# Connections that get an SPF pass for a pretend MAIL FROM of
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
# This is for non-SRS forwarders. It is a simple implementation that
# is inefficient for more than a few entries.
;trusted_forwarder = careerbuilder.com
...@@ -13,26 +13,55 @@ import spf ...@@ -13,26 +13,55 @@ import spf
import struct import struct
import socket import socket
import syslog import syslog
from Milter.config import MilterConfigParser
from Milter.utils import iniplist,parse_addr from Milter.utils import iniplist,parse_addr
syslog.openlog('spfmilter',0,syslog.LOG_MAIL) syslog.openlog('spfmilter',0,syslog.LOG_MAIL)
# list of trusted forwarder domains. An SPF record for a forwarder class Config(object):
# domain lists IP addresses from which forwarded mail is accepted. "Hold configuration options."
trusted_forwarder = [] pass
# list of internal LAN ips. No SPF check is done for these.
internal_connect = ['127.0.0.1','192.168.0.0/16'] def read_config(list):
# list of trusted relays. These are typically secondary MXes, and "Return new config object."
# no SPF check is done for these. cp = MilterConfigParser()
trusted_relay = [] cp.read(list)
conf = Config()
socketname = "/var/run/milter/spfmiltersock" conf.socketname = cp.getdefault('milter','socketname', '/tmp/spfmiltersock')
#socketname = os.getenv("HOME") + "/pythonsock" conf.miltername = cp.getdefault('milter','name','pyspffilter')
miltername = "pyspffilter" conf.trusted_relay = cp.getlist('milter','trusted_relay')
conf.internal_connect = cp.getlist('milter','internal_connect')
conf.trusted_forwarder = cp.getlist('spf','trusted_relay')
conf.access_file = cp.getdefault('spf','access_file',None)
return conf
class SPFPolicy(object):
"Get SPF policy by result from sendmail style access file."
def __init__(self,sender):
self.sender = sender
self.domain = sender.split('@')[-1].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.sender]
except KeyError:
try:
return acf[pfx + self.domain]
except KeyError:
try:
return acf[pfx]
except KeyError:
return None
class spfMilter(Milter.Milter): class spfMilter(Milter.Milter):
"Milter to check SPF." "Milter to check SPF. Each connection gets its own instance."
def log(self,*msg): def log(self,*msg):
syslog.syslog('[%d] %s' % (self.id,' '.join([str(m) for m in msg]))) syslog.syslog('[%d] %s' % (self.id,' '.join([str(m) for m in msg])))
...@@ -40,6 +69,8 @@ class spfMilter(Milter.Milter): ...@@ -40,6 +69,8 @@ class spfMilter(Milter.Milter):
def __init__(self): def __init__(self):
self.mailfrom = None self.mailfrom = None
self.id = Milter.uniqueID() self.id = Milter.uniqueID()
# we don't want config used to change during a connection
self.conf = config
# addheader can only be called from eom(). This accumulates added headers # addheader can only be called from eom(). This accumulates added headers
# which can then be applied by alter_headers() # which can then be applied by alter_headers()
...@@ -55,9 +86,9 @@ class spfMilter(Milter.Milter): ...@@ -55,9 +86,9 @@ class spfMilter(Milter.Milter):
self.receiver = self.getsymval('j').strip() self.receiver = self.getsymval('j').strip()
if hostaddr and len(hostaddr) > 0: if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0] ipaddr = hostaddr[0]
if iniplist(ipaddr,internal_connect): if iniplist(ipaddr,self.conf.internal_connect):
self.internal_connection = True self.internal_connection = True
if iniplist(ipaddr,trusted_relay): if iniplist(ipaddr,self.conf.trusted_relay):
self.trusted_relay = True self.trusted_relay = True
else: ipaddr = '' else: ipaddr = ''
self.connectip = ipaddr self.connectip = ipaddr
...@@ -112,7 +143,7 @@ class spfMilter(Milter.Milter): ...@@ -112,7 +143,7 @@ class spfMilter(Milter.Milter):
def check_spf(self): def check_spf(self):
receiver = self.receiver receiver = self.receiver
for tf in trusted_forwarder: for tf in self.conf.trusted_forwarder:
q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False) q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False)
res,code,txt = q.check() res,code,txt = q.check()
if res == 'pass': if res == 'pass':
...@@ -141,14 +172,30 @@ class spfMilter(Milter.Milter): ...@@ -141,14 +172,30 @@ class spfMilter(Milter.Milter):
else: else:
hres,hcode,htxt = res,code,txt hres,hcode,htxt = res,code,txt
else: hres = None else: hres = None
p = SPFPolicy(q.s)
if res == 'fail': if res == 'fail':
policy = p.getPolicy('spf-fail:')
if not policy or policy == 'REJECT':
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 == 'permerror': if res == 'softfail':
policy = p.getPolicy('spf-softfail:')
if policy and policy == 'REJECT':
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
elif res == 'permerror':
policy = p.getPolicy('spf-permerror:')
if not policy or policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt)) self.log('REJECT: SPF %s %i %s' % (res,code,txt))
# latest SPF draft recommends 5.5.2 instead of 5.7.1 # 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,
...@@ -156,10 +203,27 @@ class spfMilter(Milter.Milter): ...@@ -156,10 +203,27 @@ class spfMilter(Milter.Milter):
'We cannot accept mail from %s until this is corrected.' % q.o 'We cannot accept mail from %s until this is corrected.' % q.o
) )
return Milter.REJECT return Milter.REJECT
if res == 'temperror': elif res == 'temperror':
policy = p.getPolicy('spf-temperror:')
if not policy or policy == 'REJECT':
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
elif res == 'neutral' or res == 'none':
policy = p.getPolicy('spf-neutral:')
if policy and policy == 'REJECT':
self.log('REJECT NEUTRAL:',q.s)
self.setreply('550','5.7.1',
"%s requires and SPF PASS to accept mail from %s. [http://openspf.org]"
% (receiver,q.s))
return Milter.REJECT
elif res == 'pass':
policy = p.getPolicy('spf-pass:')
if policy and policy == 'REJECT':
self.log('REJECT PASS:',q.s)
self.setreply('550','5.7.1',
"%s has been blacklisted by %s." % (q.s,receiver))
return Milter.REJECT
self.add_header('Received-SPF',q.get_header(res,receiver),0) self.add_header('Received-SPF',q.get_header(res,receiver),0)
if hres and q.h != q.o: if hres and q.h != q.o:
self.add_header('X-Hello-SPF',hres,0) self.add_header('X-Hello-SPF',hres,0)
...@@ -168,6 +232,10 @@ class spfMilter(Milter.Milter): ...@@ -168,6 +232,10 @@ class spfMilter(Milter.Milter):
if __name__ == "__main__": if __name__ == "__main__":
Milter.factory = spfMilter Milter.factory = spfMilter
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS) Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
global config
config = read_config(['spfmilter.cfg','/etc/mail/spfmilter.cfg'])
miltername = config.miltername
socketname = config.socketname
print """To use this with sendmail, add the following to sendmail.cf: print """To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=%s O InputMailFilters=%s
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment