Add config file to spfmilter
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
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
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# A simple milter that has grown quite a bit.
|
||||
# $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
|
||||
# Move parse_addr, iniplist, ip4re to Milter.utils
|
||||
#
|
||||
@@ -49,7 +52,6 @@ import mime
|
||||
import email.Errors
|
||||
import Milter
|
||||
import tempfile
|
||||
from ConfigParser import ConfigParser
|
||||
import time
|
||||
import socket
|
||||
import struct
|
||||
@@ -60,6 +62,7 @@ import anydbm
|
||||
import Milter.dsn as dsn
|
||||
from Milter.dynip import is_dynip as dynip
|
||||
from Milter.utils import iniplist,parse_addr,ip4re
|
||||
from Milter.config import MilterConfigParser
|
||||
|
||||
from fnmatch import fnmatchcase
|
||||
from email.Header import decode_header
|
||||
@@ -167,64 +170,6 @@ milter_log = logging.getLogger('milter')
|
||||
if gossip:
|
||||
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):
|
||||
cp = MilterConfigParser({
|
||||
'tempdir': "/var/log/milter/save",
|
||||
@@ -393,7 +338,7 @@ def parse_header(val):
|
||||
return val
|
||||
|
||||
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):
|
||||
self.sender = sender
|
||||
self.domain = sender.split('@')[-1].lower()
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ reject_spoofed = 0
|
||||
# refuses mail from user names commonly abused in that way.
|
||||
;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]
|
||||
# namespace where SPF records can be supplied for domains without one
|
||||
# records are searched for under _spf.domain.com
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[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
|
||||
+86
-18
@@ -13,26 +13,55 @@ import spf
|
||||
import struct
|
||||
import socket
|
||||
import syslog
|
||||
|
||||
from Milter.config import MilterConfigParser
|
||||
from Milter.utils import iniplist,parse_addr
|
||||
|
||||
syslog.openlog('spfmilter',0,syslog.LOG_MAIL)
|
||||
|
||||
# list of trusted forwarder domains. An SPF record for a forwarder
|
||||
# domain lists IP addresses from which forwarded mail is accepted.
|
||||
trusted_forwarder = []
|
||||
# list of internal LAN ips. No SPF check is done for these.
|
||||
internal_connect = ['127.0.0.1','192.168.0.0/16']
|
||||
# list of trusted relays. These are typically secondary MXes, and
|
||||
# no SPF check is done for these.
|
||||
trusted_relay = []
|
||||
class Config(object):
|
||||
"Hold configuration options."
|
||||
pass
|
||||
|
||||
socketname = "/var/run/milter/spfmiltersock"
|
||||
#socketname = os.getenv("HOME") + "/pythonsock"
|
||||
miltername = "pyspffilter"
|
||||
def read_config(list):
|
||||
"Return new config object."
|
||||
cp = MilterConfigParser()
|
||||
cp.read(list)
|
||||
conf = Config()
|
||||
conf.socketname = cp.getdefault('milter','socketname', '/tmp/spfmiltersock')
|
||||
conf.miltername = cp.getdefault('milter','name','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):
|
||||
"Milter to check SPF."
|
||||
"Milter to check SPF. Each connection gets its own instance."
|
||||
|
||||
def log(self,*msg):
|
||||
syslog.syslog('[%d] %s' % (self.id,' '.join([str(m) for m in msg])))
|
||||
@@ -40,6 +69,8 @@ class spfMilter(Milter.Milter):
|
||||
def __init__(self):
|
||||
self.mailfrom = None
|
||||
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
|
||||
# which can then be applied by alter_headers()
|
||||
@@ -55,9 +86,9 @@ class spfMilter(Milter.Milter):
|
||||
self.receiver = self.getsymval('j').strip()
|
||||
if hostaddr and len(hostaddr) > 0:
|
||||
ipaddr = hostaddr[0]
|
||||
if iniplist(ipaddr,internal_connect):
|
||||
if iniplist(ipaddr,self.conf.internal_connect):
|
||||
self.internal_connection = True
|
||||
if iniplist(ipaddr,trusted_relay):
|
||||
if iniplist(ipaddr,self.conf.trusted_relay):
|
||||
self.trusted_relay = True
|
||||
else: ipaddr = ''
|
||||
self.connectip = ipaddr
|
||||
@@ -112,7 +143,7 @@ class spfMilter(Milter.Milter):
|
||||
|
||||
def check_spf(self):
|
||||
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)
|
||||
res,code,txt = q.check()
|
||||
if res == 'pass':
|
||||
@@ -141,14 +172,30 @@ class spfMilter(Milter.Milter):
|
||||
else:
|
||||
hres,hcode,htxt = res,code,txt
|
||||
else: hres = None
|
||||
|
||||
p = SPFPolicy(q.s)
|
||||
|
||||
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.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 == '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))
|
||||
# latest SPF draft recommends 5.5.2 instead of 5.7.1
|
||||
self.setreply(str(code),'5.5.2',txt,
|
||||
@@ -156,10 +203,27 @@ class spfMilter(Milter.Milter):
|
||||
'We cannot accept mail from %s until this is corrected.' % q.o
|
||||
)
|
||||
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.setreply(str(code),'4.3.0',txt)
|
||||
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)
|
||||
if hres and q.h != q.o:
|
||||
self.add_header('X-Hello-SPF',hres,0)
|
||||
@@ -168,6 +232,10 @@ class spfMilter(Milter.Milter):
|
||||
if __name__ == "__main__":
|
||||
Milter.factory = spfMilter
|
||||
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:
|
||||
|
||||
O InputMailFilters=%s
|
||||
|
||||
Reference in New Issue
Block a user