Release 0.6.9
This commit is contained in:
@@ -10,6 +10,7 @@ include testbms.py
|
|||||||
include testdspam.py
|
include testdspam.py
|
||||||
include bms.py
|
include bms.py
|
||||||
include spf.py
|
include spf.py
|
||||||
|
include spfquery.py
|
||||||
include test.py
|
include test.py
|
||||||
include sample.py
|
include sample.py
|
||||||
include test/*
|
include test/*
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
Here is a history of user visible changes to Python milter.
|
Here is a history of user visible changes to Python milter.
|
||||||
|
|
||||||
|
0.6.9 Reject invalid SRS immediately for benefit of callback verifiers
|
||||||
|
Fix include bug in spf.py
|
||||||
|
Fix check_header bug
|
||||||
|
Fix setup.py to work with python < 2.2.3, thanks to Eric S. Johansson
|
||||||
|
Test driver for SPF test suite. Fix bugs and add features to
|
||||||
|
pass most of test suite.
|
||||||
|
Use best_guess() and get_header() in bms.py for SPF support
|
||||||
0.6.8 Defang message/rfc822 content_type with boundary
|
0.6.8 Defang message/rfc822 content_type with boundary
|
||||||
Support SPF delegation
|
Support SPF delegation
|
||||||
Reject neutral SPF result for selected domains
|
Reject neutral SPF result for selected domains
|
||||||
@@ -7,6 +14,7 @@ Here is a history of user visible changes to Python milter.
|
|||||||
Don't report "spoofed" unless rcpt looks like SRS
|
Don't report "spoofed" unless rcpt looks like SRS
|
||||||
Check for bounce with multiple rcpts
|
Check for bounce with multiple rcpts
|
||||||
Make dspam see Received-SPF headers
|
Make dspam see Received-SPF headers
|
||||||
|
Fix sysv init for Redhat 9 and other single ps line per process systems
|
||||||
0.6.7 Fix failure to remove explicit unix socket thanks to Alexander again.
|
0.6.7 Fix failure to remove explicit unix socket thanks to Alexander again.
|
||||||
Support SRS forgery detection.
|
Support SRS forgery detection.
|
||||||
Detect thread resource starvation in Milter.py.
|
Detect thread resource starvation in Milter.py.
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
|
Web admin interface
|
||||||
|
RHBL
|
||||||
|
Check valid domains allowed by internal senders to detect PCs infected
|
||||||
|
with spam trojans.
|
||||||
|
Do CBV (callback verification) for mail with no published SPF record.
|
||||||
|
message log for automated stats and blacklisting
|
||||||
|
adapt init script to work on RH9
|
||||||
|
Skip dspam when SPF pass?
|
||||||
|
Report 551 with rcpt on SPF fail?
|
||||||
|
check spam keywords with character classes, e.g.
|
||||||
|
{a}=[a@ãä], {i}=[i1í], {e}=[eë], {o}=[o0ö]
|
||||||
|
|
||||||
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
|
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
|
||||||
forwarder accounts, and a util provides a special local alias for the
|
forwarder accounts, and a util provides a special local alias for the
|
||||||
user to give to the forwarder. Alias only works for mail from that
|
user to give to the forwarder. Alias only works for mail from that
|
||||||
forwarder. Milter gets forwarder domain from alias and uses it to
|
forwarder. Milter gets forwarder domain from alias and uses it to
|
||||||
SPF check forwarder.
|
SPF check forwarder.
|
||||||
|
|
||||||
adapt init script to work on RH9
|
|
||||||
Skip dspam when SPF pass?
|
|
||||||
Report 551 with rcpt on SPF fail?
|
|
||||||
|
|
||||||
Another special dspam user, 'honeypot', can be listed in innoculations.
|
Another special dspam user, 'honeypot', can be listed in innoculations.
|
||||||
All email to those addresses is treated as known spam.
|
All email to those addresses is treated as known spam.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,33 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# A simple milter.
|
# A simple milter.
|
||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.105 2004/04/20 15:16:00 stuart
|
||||||
|
# Release 0.6.9
|
||||||
|
#
|
||||||
|
# Revision 1.104 2004/04/19 21:56:26 stuart
|
||||||
|
# Support SPF best_guess and get_header
|
||||||
|
#
|
||||||
|
# Revision 1.103 2004/04/10 02:31:01 stuart
|
||||||
|
# Fix timeout config
|
||||||
|
#
|
||||||
|
# Revision 1.102 2004/04/08 20:25:11 stuart
|
||||||
|
# Make libmilter timeout a config option
|
||||||
|
#
|
||||||
|
# Revision 1.101 2004/04/08 19:18:16 stuart
|
||||||
|
# Preserve case of local part in sender
|
||||||
|
#
|
||||||
|
# Revision 1.100 2004/04/08 18:41:15 stuart
|
||||||
|
# Reject numeric hello names
|
||||||
|
#
|
||||||
|
# Revision 1.99 2004/04/06 19:46:39 stuart
|
||||||
|
# Reject invalid SRS immediately for benefit of CallBack Verifiers.
|
||||||
|
#
|
||||||
|
# Revision 1.98 2004/04/06 15:28:20 stuart
|
||||||
|
# Release 0.6.8-2
|
||||||
|
#
|
||||||
|
# Revision 1.97 2004/04/06 13:07:43 stuart
|
||||||
|
# Pass original header name to check_header
|
||||||
|
#
|
||||||
# Revision 1.96 2004/04/06 03:27:03 stuart
|
# Revision 1.96 2004/04/06 03:27:03 stuart
|
||||||
# bugs from Redhat 9 testing
|
# bugs from Redhat 9 testing
|
||||||
#
|
#
|
||||||
@@ -154,90 +181,6 @@
|
|||||||
# Revision 1.47 2003/08/26 05:01:38 stuart
|
# Revision 1.47 2003/08/26 05:01:38 stuart
|
||||||
# Release 0.6.0
|
# Release 0.6.0
|
||||||
#
|
#
|
||||||
# Revision 1.46 2003/08/26 04:45:16 stuart
|
|
||||||
# Modest dspam control
|
|
||||||
#
|
|
||||||
# Revision 1.43 2003/06/25 17:00:02 stuart
|
|
||||||
# fix hostaddr test
|
|
||||||
#
|
|
||||||
# Revision 1.42 2003/06/25 16:45:59 stuart
|
|
||||||
# Not using checking hostaddr properly
|
|
||||||
#
|
|
||||||
# Revision 1.41 2003/06/25 15:57:54 stuart
|
|
||||||
# Ready for 5.5 release.
|
|
||||||
#
|
|
||||||
# Revision 1.40 2003/06/25 15:41:41 stuart
|
|
||||||
# recognize internal connections.
|
|
||||||
# Give legitimate users a clue about banned subject keywords.
|
|
||||||
#
|
|
||||||
# Revision 1.39 2002/12/14 00:36:59 stuart
|
|
||||||
# Smart alias feature
|
|
||||||
#
|
|
||||||
# Revision 1.38 2002/11/14 17:52:53 stuart
|
|
||||||
# Redirection feature for wiretap
|
|
||||||
#
|
|
||||||
# Revision 1.37 2002/11/07 23:52:09 stuart
|
|
||||||
# config fixes
|
|
||||||
#
|
|
||||||
# Revision 1.36 2002/10/04 05:27:38 stuart
|
|
||||||
# Add get_submsg to allow modifying rfc822 attachment.
|
|
||||||
#
|
|
||||||
# Revision 1.35 2002/10/03 01:31:18 stuart
|
|
||||||
# Test encoded rfc822 attachment
|
|
||||||
#
|
|
||||||
# Revision 1.34 2002/10/03 00:55:42 stuart
|
|
||||||
# Decode rfc822 attachments
|
|
||||||
#
|
|
||||||
# Revision 1.33 2002/10/02 18:49:02 stuart
|
|
||||||
# Save and log messages which cause an exception while parsing attachments.
|
|
||||||
#
|
|
||||||
# Revision 1.32 2002/09/24 01:38:05 stuart
|
|
||||||
# Doc updates.
|
|
||||||
#
|
|
||||||
# Revision 1.31 2002/09/13 22:14:06 stuart
|
|
||||||
# Release 0.5.0 wrapup
|
|
||||||
#
|
|
||||||
# Revision 1.30 2002/09/13 20:22:37 stuart
|
|
||||||
# Additional config items
|
|
||||||
#
|
|
||||||
# Revision 1.29 2002/08/20 04:40:46 stuart
|
|
||||||
# Use config file
|
|
||||||
#
|
|
||||||
# Revision 1.28 2002/07/12 19:40:38 stuart
|
|
||||||
# Update docs, minor bugs.
|
|
||||||
#
|
|
||||||
# Revision 1.27 2002/06/16 02:06:24 stuart
|
|
||||||
# SPAM tweaks
|
|
||||||
#
|
|
||||||
# Revision 1.26 2002/06/07 22:07:30 stuart
|
|
||||||
# Isolate local hacks to configuration data.
|
|
||||||
#
|
|
||||||
# Revision 1.25 2002/05/02 20:41:00 stuart
|
|
||||||
# Top level virus needs top level header change.
|
|
||||||
#
|
|
||||||
# Revision 1.24 2002/05/02 20:31:43 stuart
|
|
||||||
# Handle quoted-printable HTML attachments.
|
|
||||||
# Remove entire attachment when HTML can't be parsed by sgmllib.
|
|
||||||
#
|
|
||||||
# Revision 1.23 2002/05/02 03:42:31 stuart
|
|
||||||
# base64 no longer needed
|
|
||||||
#
|
|
||||||
# Revision 1.22 2002/05/02 03:12:39 stuart
|
|
||||||
# Move check_html to mime module.
|
|
||||||
#
|
|
||||||
# Revision 1.21 2002/05/02 02:48:22 stuart
|
|
||||||
# Remove scripts from HTML even with base64 encoding.
|
|
||||||
#
|
|
||||||
# Revision 1.20 2002/05/02 00:21:01 stuart
|
|
||||||
# Test filtering HTML attachments.
|
|
||||||
#
|
|
||||||
# Revision 1.19 2002/05/01 22:12:41 stuart
|
|
||||||
# Remove scripts from HTML attachments.
|
|
||||||
#
|
|
||||||
# Revision 1.18 2002/03/01 20:29:00 stuart
|
|
||||||
# Ready for release.
|
|
||||||
#
|
|
||||||
|
|
||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
# Copyright 2001 Business Management Systems, Inc.
|
# Copyright 2001 Business Management Systems, Inc.
|
||||||
# This code is under GPL. See COPYING for details.
|
# This code is under GPL. See COPYING for details.
|
||||||
@@ -252,17 +195,22 @@ import Milter
|
|||||||
import tempfile
|
import tempfile
|
||||||
import ConfigParser
|
import ConfigParser
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
from fnmatch import fnmatchcase
|
from fnmatch import fnmatchcase
|
||||||
from email.Header import decode_header
|
from email.Header import decode_header
|
||||||
|
|
||||||
# Import pysrs if available
|
# Import pysrs if available
|
||||||
try:
|
try:
|
||||||
import SRS
|
import SRS
|
||||||
import re
|
|
||||||
srsre = re.compile(r'^SRS[01][+-=]',re.IGNORECASE)
|
srsre = re.compile(r'^SRS[01][+-=]',re.IGNORECASE)
|
||||||
except: SRS = None
|
except: SRS = None
|
||||||
|
|
||||||
|
# Import spf if available
|
||||||
try: import spf
|
try: import spf
|
||||||
except: spf = None
|
except: spf = None
|
||||||
|
|
||||||
|
ip4re = re.compile(r'^[1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*$')
|
||||||
#import syslog
|
#import syslog
|
||||||
#syslog.openlog('milter')
|
#syslog.openlog('milter')
|
||||||
|
|
||||||
@@ -297,10 +245,12 @@ dspam_whitelist = {}
|
|||||||
dspam_screener = None
|
dspam_screener = None
|
||||||
dspam_internal = True # True if internal mail should be dspammed
|
dspam_internal = True # True if internal mail should be dspammed
|
||||||
dspam_reject = ()
|
dspam_reject = ()
|
||||||
dspam_sizelimit = 80000
|
dspam_sizelimit = 180000
|
||||||
srs = None
|
srs = None
|
||||||
srs_reject_spoofed = False
|
srs_reject_spoofed = False
|
||||||
spf_reject_neutral = ()
|
spf_reject_neutral = ()
|
||||||
|
spf_best_guess = False
|
||||||
|
timeout = 600
|
||||||
|
|
||||||
class MilterConfigParser(ConfigParser.ConfigParser):
|
class MilterConfigParser(ConfigParser.ConfigParser):
|
||||||
|
|
||||||
@@ -351,6 +301,7 @@ def read_config(list):
|
|||||||
cp = MilterConfigParser({
|
cp = MilterConfigParser({
|
||||||
'tempdir': "/var/log/milter/save",
|
'tempdir': "/var/log/milter/save",
|
||||||
'socket': "/var/log/milter/pythonsock",
|
'socket': "/var/log/milter/pythonsock",
|
||||||
|
'timeout': '600',
|
||||||
'scan_html': 'no',
|
'scan_html': 'no',
|
||||||
'scan_rfc822': 'yes',
|
'scan_rfc822': 'yes',
|
||||||
'block_chinese': 'no',
|
'block_chinese': 'no',
|
||||||
@@ -358,12 +309,14 @@ def read_config(list):
|
|||||||
'blind_wiretap': 'yes',
|
'blind_wiretap': 'yes',
|
||||||
'maxage': '8',
|
'maxage': '8',
|
||||||
'hashlength': '8',
|
'hashlength': '8',
|
||||||
'reject_spoofed': 'no'
|
'reject_spoofed': 'no',
|
||||||
|
'best_guess': 'no'
|
||||||
})
|
})
|
||||||
cp.read(list)
|
cp.read(list)
|
||||||
tempfile.tempdir = cp.get('milter','tempdir')
|
tempfile.tempdir = cp.get('milter','tempdir')
|
||||||
global socketname, scan_rfc822, scan_html, block_chinese
|
global socketname, scan_rfc822, scan_html, block_chinese, timeout
|
||||||
socketname = cp.get('milter','socket')
|
socketname = cp.get('milter','socket')
|
||||||
|
timeout = cp.getint('milter','timeout')
|
||||||
scan_rfc822 = cp.getboolean('milter','scan_rfc822')
|
scan_rfc822 = cp.getboolean('milter','scan_rfc822')
|
||||||
scan_html = cp.getboolean('milter','scan_html')
|
scan_html = cp.getboolean('milter','scan_html')
|
||||||
block_chinese = cp.getboolean('milter','block_chinese')
|
block_chinese = cp.getboolean('milter','block_chinese')
|
||||||
@@ -402,7 +355,7 @@ def read_config(list):
|
|||||||
|
|
||||||
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt
|
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt
|
||||||
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
|
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
|
||||||
global spf_reject_neutral,SRS
|
global spf_reject_neutral,spf_best_guess,SRS
|
||||||
dspam_dict = cp.getdefault('dspam','dspam_dict')
|
dspam_dict = cp.getdefault('dspam','dspam_dict')
|
||||||
dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
|
dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
|
||||||
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
|
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
|
||||||
@@ -416,6 +369,7 @@ def read_config(list):
|
|||||||
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')
|
||||||
|
spf_best_guess = cp.getboolean('spf','best_guess')
|
||||||
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')
|
||||||
@@ -526,6 +480,10 @@ class bmsMilter(Milter.Milter):
|
|||||||
def hello(self,hostname):
|
def hello(self,hostname):
|
||||||
self.hello_name = hostname
|
self.hello_name = hostname
|
||||||
self.log("hello from %s" % hostname)
|
self.log("hello from %s" % hostname)
|
||||||
|
if ip4re.match(hostname):
|
||||||
|
self.log("REJECT: numeric hello name:",hostname)
|
||||||
|
self.setreply('550','5.7.1','hello name cannot be numeric ip')
|
||||||
|
return Milter.REJECT
|
||||||
if not self.internal_connection and hostname in hello_blacklist:
|
if not self.internal_connection and hostname in hello_blacklist:
|
||||||
self.log("REJECT: spam from self:",hostname)
|
self.log("REJECT: spam from self:",hostname)
|
||||||
self.setreply('550','5.7.1','I hate talking to myself.')
|
self.setreply('550','5.7.1','I hate talking to myself.')
|
||||||
@@ -579,73 +537,32 @@ class bmsMilter(Milter.Milter):
|
|||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def check_spf(self):
|
def check_spf(self):
|
||||||
user,host = spf.split_email(self.canon_from,self.hello_name)
|
t = parse_addr(self.mailfrom)
|
||||||
self.sender = '@'.join((user,host))
|
if len(t) == 2: t[1] = t[1].lower()
|
||||||
res,code,txt = spf.check(self.connectip,self.canon_from,self.hello_name)
|
q = spf.query(self.connectip,'@'.join(t),self.hello_name)
|
||||||
|
q.set_default_explanation('SPF fail: see http://spf.pobox.com/why.html')
|
||||||
|
res,code,txt = q.check()
|
||||||
|
receiver = self.receiver
|
||||||
|
if res == 'none' and spf_best_guess:
|
||||||
|
#self.log('SPF: no record published, guessing')
|
||||||
|
q.set_default_explanation('SPF guess: see http://spf.pobox.com/why.html')
|
||||||
|
# best_guess should not result in fail
|
||||||
|
res,code,txt = q.best_guess()
|
||||||
|
receiver += ': guessing'
|
||||||
if res in ('deny', 'fail'):
|
if res in ('deny', 'fail'):
|
||||||
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
||||||
# improve default explanation, but don't wipe out text from SPF record
|
|
||||||
if txt == 'access denied':
|
|
||||||
txt = 'SPF fail: see http://spf.pobox.com/why.html'
|
|
||||||
self.setreply(str(code),'5.7.1',txt)
|
self.setreply(str(code),'5.7.1',txt)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if res == 'pass':
|
if res == 'neutral' and q.o in spf_reject_neutral:
|
||||||
# Received-SPF: pass (mybox.example.org: domain of
|
self.log('REJECT: SPF neutral for',q.s)
|
||||||
# myname@example.com designates 192.0.2.1 as
|
|
||||||
# permitted sender);
|
|
||||||
# receiver=mybox.example.org;
|
|
||||||
# client_ip=192.0.2.1;
|
|
||||||
# envelope-from=myname@example.com;
|
|
||||||
self.add_header('Received-SPF',"""pass (%(receiver)s: domain of
|
|
||||||
%(sender)s designates %(connectip)s as permitted sender);
|
|
||||||
receiver=%(receiver)s; client_ip=%(connectip)s;
|
|
||||||
envelope-from=%(canon_from)s;""" % self.__dict__)
|
|
||||||
elif res == 'none' or res == 'unknown' and txt == 'no SPF record':
|
|
||||||
# Received-SPF: none (mybox.example.org: myname@example.com does
|
|
||||||
# not designated permitted sender hosts)
|
|
||||||
self.add_header('Received-SPF',"""none (%(receiver)s: %(sender)s does
|
|
||||||
not designate permitted sender hosts)""" % self.__dict__)
|
|
||||||
elif res == 'softfail':
|
|
||||||
# Received-SPF: softfail (mybox.example.org: domain of transitioning
|
|
||||||
# myname@example.com does not designate
|
|
||||||
# 192.0.2.1 as permitted sender)
|
|
||||||
self.add_header('Received-SPF',
|
|
||||||
"""softfail (%(receiver)s: domain of transitioning
|
|
||||||
%(sender)s does not designate
|
|
||||||
%(connectip)s as permitted sender)""" % self.__dict__)
|
|
||||||
elif res == 'neutral':
|
|
||||||
if host in spf_reject_neutral:
|
|
||||||
self.log('REJECT: SPF neutral for',self.sender)
|
|
||||||
self.setreply('550','5.7.1',
|
self.setreply('550','5.7.1',
|
||||||
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % host
|
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o
|
||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
# Received-SPF: neutral (mybox.example.org: 192.0.2.1 is neither
|
if res == 'error':
|
||||||
# permitted nor denied by domain of
|
|
||||||
# myname@example.com)
|
|
||||||
self.add_header('Received-SPF',
|
|
||||||
"""neutral (%(receiver)s: %(connectip)s is neither
|
|
||||||
permitted nor denied by domain of %(sender)s)""" % self.__dict__)
|
|
||||||
elif res == 'unknown':
|
|
||||||
# Received-SPF: unknown -extension:foo (mybox.example.org: domain
|
|
||||||
# of myname@example.com uses mechanism
|
|
||||||
# not recognized by this client)
|
|
||||||
self.spf_mech = txt
|
|
||||||
self.add_header('Received-SPF',
|
|
||||||
"""unknown %(spf_mech)s (%(receiver)s: domain
|
|
||||||
of %(sender)s uses mechanism not recognized by this client)"""
|
|
||||||
% self.__dict__)
|
|
||||||
elif res == 'error':
|
|
||||||
# Received-SPF: error (mybox.example.org: error in processing
|
|
||||||
# during lookup of myname@example.com: DNS
|
|
||||||
# timeout)
|
|
||||||
self.add_header('Received-SPF',
|
|
||||||
"""error (%s: error in processing
|
|
||||||
during lookup of %s: %s)""" % (self.receiver,self.sender,txt))
|
|
||||||
self.setreply(str(code),'4.3.0',txt)
|
self.setreply(str(code),'4.3.0',txt)
|
||||||
return Milter.TEMPFAIL
|
return Milter.TEMPFAIL
|
||||||
else:
|
self.add_header('Received-SPF',q.get_header(res,receiver))
|
||||||
self.log('SPF: %s %i %s' % (res,code,txt))
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
# hide_path causes a copy of the message to be saved - until we
|
# hide_path causes a copy of the message to be saved - until we
|
||||||
@@ -671,7 +588,9 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log("srs rcpt:",newaddr)
|
self.log("srs rcpt:",newaddr)
|
||||||
except:
|
except:
|
||||||
if srsre.match(oldaddr):
|
if srsre.match(oldaddr):
|
||||||
self.log("srs spoofed:",oldaddr)
|
self.log("REJECT: srs spoofed:",oldaddr)
|
||||||
|
self.setreply('550','5.7.1','Invalid SRS signature')
|
||||||
|
return Milter.REJECT
|
||||||
self.data_allowed = not srs_reject_spoofed
|
self.data_allowed = not srs_reject_spoofed
|
||||||
self.recipients.append('@'.join(t))
|
self.recipients.append('@'.join(t))
|
||||||
user,domain = t
|
user,domain = t
|
||||||
@@ -708,7 +627,8 @@ class bmsMilter(Milter.Milter):
|
|||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
# Heuristic checks for spam headers
|
# Heuristic checks for spam headers
|
||||||
def check_header(self,lname,val):
|
def check_header(self,name,val):
|
||||||
|
lname = name.lower()
|
||||||
# val is decoded header value
|
# val is decoded header value
|
||||||
if lname == 'subject':
|
if lname == 'subject':
|
||||||
|
|
||||||
@@ -743,6 +663,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
if not self.forward:
|
if not self.forward:
|
||||||
if lval.startswith("fwd:") or lval.startswith("[fw"):
|
if lval.startswith("fwd:") or lval.startswith("[fw"):
|
||||||
self.log('REJECT: %s: %s' % (name,val))
|
self.log('REJECT: %s: %s' % (name,val))
|
||||||
|
self.setreply('550','5.7.1','I find unedited forwards annoying')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
|
|
||||||
# check for invalid message id
|
# check for invalid message id
|
||||||
@@ -777,7 +698,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log('REJECT: %s: %s' % (name,hval))
|
self.log('REJECT: %s: %s' % (name,hval))
|
||||||
self.setreply('550','5.7.1',"We don't understand chinese")
|
self.setreply('550','5.7.1',"We don't understand chinese")
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
rc = self.check_header(lname,val)
|
rc = self.check_header(name,val)
|
||||||
if rc != Milter.CONTINUE: return rc
|
if rc != Milter.CONTINUE: return rc
|
||||||
# log selected headers
|
# log selected headers
|
||||||
if log_headers or lname in ('subject','x-mailer'):
|
if log_headers or lname in ('subject','x-mailer'):
|
||||||
@@ -1031,6 +952,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
os.remove(self.tempname) # remove in case session aborted
|
os.remove(self.tempname) # remove in case session aborted
|
||||||
if self.fp:
|
if self.fp:
|
||||||
self.fp.close()
|
self.fp.close()
|
||||||
|
sys.stdout.flush()
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
@@ -1047,7 +969,7 @@ def main():
|
|||||||
Milter.set_flags(flags)
|
Milter.set_flags(flags)
|
||||||
print "bms milter startup"
|
print "bms milter startup"
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
Milter.runmilter("pythonfilter",socketname,600)
|
Milter.runmilter("pythonfilter",socketname,timeout)
|
||||||
print "bms milter shutdown"
|
print "bms milter shutdown"
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+12
-1
@@ -1,10 +1,15 @@
|
|||||||
# features intended to filter or block incoming mail
|
# features intended to filter or block incoming mail
|
||||||
[milter]
|
[milter]
|
||||||
|
;socket=/var/log/milter/pythonsock
|
||||||
tempdir = /var/log/milter/save
|
tempdir = /var/log/milter/save
|
||||||
|
;timeout=600
|
||||||
|
|
||||||
scan_rfc822 = 1
|
scan_rfc822 = 1
|
||||||
# can be CPU intensive
|
# can be CPU intensive
|
||||||
scan_html = 0
|
scan_html = 0
|
||||||
|
# reject asian fonts because we can't read them
|
||||||
block_chinese = 1
|
block_chinese = 1
|
||||||
|
# users who hate forwarded mail
|
||||||
;block_forward = egghead@mycorp.com, busybee@mycorp.com
|
;block_forward = egghead@mycorp.com, busybee@mycorp.com
|
||||||
log_headers = 0
|
log_headers = 0
|
||||||
# Reject mail for domains mentioned unless user is mentioned here also
|
# Reject mail for domains mentioned unless user is mentioned here also
|
||||||
@@ -12,7 +17,9 @@ log_headers = 0
|
|||||||
# porn words are case insensitive
|
# porn words are case insensitive
|
||||||
porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
|
porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
|
||||||
vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck,
|
vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck,
|
||||||
p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax
|
p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam,
|
||||||
|
v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra,
|
||||||
|
x@n3x, vicod3n, penís, v|c0d1n, phentermine, en1arge, dip1oma, v1codin
|
||||||
# spam words are case sensitive
|
# spam words are case sensitive
|
||||||
spam_words = $$$, !!!, XXX, FREE, HGH
|
spam_words = $$$, !!!, XXX, FREE, HGH
|
||||||
|
|
||||||
@@ -43,6 +50,8 @@ reject_spoofed = 0
|
|||||||
;delegate = domain.com
|
;delegate = domain.com
|
||||||
# domains where a neutral SPF result should cause mail to be rejected
|
# domains where a neutral SPF result should cause mail to be rejected
|
||||||
;reject_neutral = aol.com
|
;reject_neutral = aol.com
|
||||||
|
# use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published
|
||||||
|
;best_guess = 0
|
||||||
|
|
||||||
# features intended to clean up outgoing mail
|
# features intended to clean up outgoing mail
|
||||||
[scrub]
|
[scrub]
|
||||||
@@ -93,6 +102,8 @@ blind = 1
|
|||||||
# defining this activates the dspam application
|
# defining this activates the dspam application
|
||||||
# dspam and dspam-python must be installed
|
# dspam and dspam-python must be installed
|
||||||
;dspam_userdir=/var/lib/dspam
|
;dspam_userdir=/var/lib/dspam
|
||||||
|
# do not dspam messages larger than this
|
||||||
|
;dspam_sizelimit=180000
|
||||||
|
|
||||||
# Map email addresses and aliases to dspam users
|
# Map email addresses and aliases to dspam users
|
||||||
;dspam_users=david,goliath,spam,falsepositive
|
;dspam_users=david,goliath,spam,falsepositive
|
||||||
|
|||||||
+40
-5
@@ -24,7 +24,7 @@ ALT="Viewable With Any Browser" BORDER="0"></A>
|
|||||||
Stuart D. Gathman</a><br>
|
Stuart D. Gathman</a><br>
|
||||||
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
|
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
|
||||||
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
|
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
|
||||||
Last updated Apr 05, 2004</h4>
|
Last updated Apr 21, 2004</h4>
|
||||||
|
|
||||||
See the <a href="faq.html">FAQ</a> | <a href="#download">Download now</a> |
|
See the <a href="faq.html">FAQ</a> | <a href="#download">Download now</a> |
|
||||||
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a>
|
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a>
|
||||||
@@ -45,6 +45,13 @@ I recommend upgrading.
|
|||||||
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
|
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
|
||||||
dspam bayes filter project</a> and <a href="dspam.html">
|
dspam bayes filter project</a> and <a href="dspam.html">
|
||||||
packaged it for python</a>.
|
packaged it for python</a>.
|
||||||
|
Release 0.6.6 adds support for <a href="http://spf.pobox.com/">SPF</a>,
|
||||||
|
a protocol to prevent forging of the envelope from address.
|
||||||
|
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
|
||||||
|
The included spf.py module is an updated version of the original 1.6
|
||||||
|
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
|
||||||
|
The updated version tracks the draft RFC and test suite.
|
||||||
|
<p>
|
||||||
Release 0.6.0 offers a simple application of dspam I call "header triage",
|
Release 0.6.0 offers a simple application of dspam I call "header triage",
|
||||||
which rejects messages with spammy headers. Since sendmail has to
|
which rejects messages with spammy headers. Since sendmail has to
|
||||||
read the entire message anyway once we start reading headers, it
|
read the entire message anyway once we start reading headers, it
|
||||||
@@ -140,14 +147,43 @@ wiretapping, and Win32 virus protection milter.
|
|||||||
|
|
||||||
<h3><a name=download>Downloading</a></h3>
|
<h3><a name=download>Downloading</a></h3>
|
||||||
|
|
||||||
The latest stable release is <a href="#stable">0.6.6</a>. A stable
|
The latest stable release is <a href="#stable">0.6.9</a>. A stable
|
||||||
release is one which has been installed (and working correctly) on
|
release is one which has been installed (and working correctly) on
|
||||||
production systems long enough to convince me that it is stable. As
|
production systems long enough to convince me that it is stable. As
|
||||||
the package gains more features and complexity, stable will mean no
|
the package gains more features and complexity, stable will mean no
|
||||||
bug reports from outside users either.
|
bug reports from outside users either.
|
||||||
<p>
|
<p>
|
||||||
The latest version is 0.6.7. See the <a href=NEWS>Change Log</a>.
|
The latest version is 0.6.9-1. See the <a href=NEWS>Change Log</a>.
|
||||||
|
<p>
|
||||||
|
<a name="stable"><b>Stable</b></a>
|
||||||
|
<a href="http://bmsi.com/python/milter-0.6.9.tar.gz">
|
||||||
|
milter-0.6.9.tar.gz</a> Add SPF test suite driver, and validate
|
||||||
|
spf.py against test suite. Add best_guess and get_header to spf.py.
|
||||||
|
Libmilter timeout option in config.
|
||||||
|
<br>
|
||||||
|
<a href="http://bmsi.com/linux/rh72/milter-0.6.9-1.i386.rpm">
|
||||||
|
milter-0.6.9-1.i386.rpm</a> Binary RPM for Redhat 7.x, now requires
|
||||||
|
sendmail-8.12 and <a href="http://www.python.org/2.3.3/rpms.html">
|
||||||
|
python2.3</a>.
|
||||||
|
<br>
|
||||||
|
<a href="http://bmsi.com/linux/rh9/milter-0.6.9-1.src.rpm">
|
||||||
|
milter-0.6.9-1.src.rpm</a> Source RPM for Redhat 9,7.x.
|
||||||
|
<p>
|
||||||
|
<a href="http://bmsi.com/python/milter-0.6.8.tar.gz">
|
||||||
|
milter-0.6.8.tar.gz</a> Include Received-SPF headers in Dspam analysis.
|
||||||
|
Fix sysv init for Redhat 9 and later. Reject bounces with multiple
|
||||||
|
recipients.
|
||||||
|
<br>
|
||||||
|
<a href="http://bmsi.com/python/milter-0.6.8.patch">milter-0.6.8.patch</a>
|
||||||
|
Last minutes fixes from production testing.
|
||||||
|
<p>
|
||||||
|
<a href="http://bmsi.com/linux/rh72/milter-0.6.8-3.i386.rpm">
|
||||||
|
milter-0.6.8-3.i386.rpm</a> Binary RPM for Redhat 7.x, now requires
|
||||||
|
sendmail-8.12 and <a href="http://www.python.org/2.3.3/rpms.html">
|
||||||
|
python2.3</a>.
|
||||||
|
<br>
|
||||||
|
<a href="http://bmsi.com/linux/rh9/milter-0.6.8-3.src.rpm">
|
||||||
|
milter-0.6.8-3.src.rpm</a> Source RPM for Redhat 9,7.x.
|
||||||
<p>
|
<p>
|
||||||
<a href="http://bmsi.com/python/milter-0.6.7.tar.gz">
|
<a href="http://bmsi.com/python/milter-0.6.7.tar.gz">
|
||||||
milter-0.6.7.tar.gz</a> Explicit local socket bug,
|
milter-0.6.7.tar.gz</a> Explicit local socket bug,
|
||||||
@@ -169,7 +205,6 @@ Release 0.6.7-3 patches:
|
|||||||
<li> Reject neutral SPF result for selected domains
|
<li> Reject neutral SPF result for selected domains
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
<a name="stable"><b>Stable</b></a>
|
|
||||||
<a href="http://bmsi.com/python/milter-0.6.6.tar.gz">
|
<a href="http://bmsi.com/python/milter-0.6.6.tar.gz">
|
||||||
milter-0.6.6.tar.gz</a> Plug another memory leak,
|
milter-0.6.6.tar.gz</a> Plug another memory leak,
|
||||||
<a href="http://spf.pobox.com/">SPF</a> support, hello blacklist.
|
<a href="http://spf.pobox.com/">SPF</a> support, hello blacklist.
|
||||||
|
|||||||
+17
-5
@@ -1,10 +1,10 @@
|
|||||||
%define name milter
|
%define name milter
|
||||||
%define version 0.6.8
|
%define version 0.6.9
|
||||||
%define release 1
|
%define release 1
|
||||||
# Redhat 7.x and earlier (multiple ps lines per thread)
|
# Redhat 7.x and earlier (multiple ps lines per thread)
|
||||||
#%define sysvinit rc7
|
%define sysvinit milter.rc7
|
||||||
# RH9, other systems (single ps line per process)
|
# RH9, other systems (single ps line per process)
|
||||||
%define sysvinit rc
|
#define sysvinit milter.rc
|
||||||
%ifos Linux
|
%ifos Linux
|
||||||
%define python python2.3
|
%define python python2.3
|
||||||
%else
|
%else
|
||||||
@@ -16,7 +16,7 @@ Name: %{name}
|
|||||||
Version: %{version}
|
Version: %{version}
|
||||||
Release: %{release}
|
Release: %{release}
|
||||||
Source: %{name}-%{version}.tar.gz
|
Source: %{name}-%{version}.tar.gz
|
||||||
#Patch: %{name}.patch
|
#Patch: %{name}-%{version}.patch
|
||||||
Copyright: GPL
|
Copyright: GPL
|
||||||
Group: Development/Libraries
|
Group: Development/Libraries
|
||||||
BuildRoot: %{_tmppath}/%{name}-buildroot
|
BuildRoot: %{_tmppath}/%{name}-buildroot
|
||||||
@@ -81,7 +81,7 @@ exec >>milter.log 2>&1
|
|||||||
echo $! >/var/run/milter/milter.pid
|
echo $! >/var/run/milter/milter.pid
|
||||||
EOF
|
EOF
|
||||||
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
|
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
|
||||||
cp milter.%{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter
|
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter
|
||||||
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF'
|
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF'
|
||||||
/^python=/
|
/^python=/
|
||||||
c
|
c
|
||||||
@@ -127,6 +127,18 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
%config /var/log/milter/milter.cfg
|
%config /var/log/milter/milter.cfg
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Fri Apr 09 2004 Stuart Gathman <stuart@bmsi.com> 0.6.9-1
|
||||||
|
- Validate spf.py against test suite, and add Received-SPF support to spf.py
|
||||||
|
- Support best_guess for SPF
|
||||||
|
- Reject numeric hello names
|
||||||
|
- Preserve case of local part in sender
|
||||||
|
- Make libmilter timeout a config option
|
||||||
|
- Fix setup.py to work with python < 2.2.3
|
||||||
|
* Tue Apr 06 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-3
|
||||||
|
- Reject invalid SRS immediately for benefit of callback verifiers
|
||||||
|
- Fix include bug in spf.py
|
||||||
|
* Tue Apr 06 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-2
|
||||||
|
- Bug in check_header
|
||||||
* Mon Apr 05 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-1
|
* Mon Apr 05 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-1
|
||||||
- Don't report spoofed unless rcpt looks like SRS
|
- Don't report spoofed unless rcpt looks like SRS
|
||||||
- Check for bounce with multiple rcpts
|
- Check for bounce with multiple rcpts
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from distutils.core import setup, Extension
|
from distutils.core import setup, Extension
|
||||||
|
|
||||||
# FIXME: on some versions of sendmail, smutil is renamed to sm
|
# FIXME: on some versions of sendmail, smutil is renamed to sm
|
||||||
libs = ["milter", "smutil"]
|
libs = ["milter", "smutil"]
|
||||||
|
|
||||||
setup(name = "milter", version = "0.6.8",
|
# patch distutils if it can't cope with the "classifiers" or
|
||||||
|
# "download_url" keywords
|
||||||
|
if sys.version < '2.2.3':
|
||||||
|
from distutils.dist import DistributionMetadata
|
||||||
|
DistributionMetadata.classifiers = None
|
||||||
|
DistributionMetadata.download_url = None
|
||||||
|
|
||||||
|
setup(name = "milter", version = "0.6.9",
|
||||||
description="Python interface to sendmail milter API",
|
description="Python interface to sendmail milter API",
|
||||||
long_description="""\
|
long_description="""\
|
||||||
This is a python extension module to enable python scripts to
|
This is a python extension module to enable python scripts to
|
||||||
|
|||||||
@@ -40,7 +40,26 @@ For news, bugfixes, etc. visit the home page for this implementation at
|
|||||||
# ditch the annoying Python 2.4 FutureWarning
|
# ditch the annoying Python 2.4 FutureWarning
|
||||||
# 18-dec-2003, v1.6, Failures on Intel hardware: endianness. Use ! on
|
# 18-dec-2003, v1.6, Failures on Intel hardware: endianness. Use ! on
|
||||||
# struct.pack(), struct.unpack().
|
# struct.pack(), struct.unpack().
|
||||||
|
#
|
||||||
|
# Development taken over by Stuart Gathman <stuart@bmsi.com> since
|
||||||
|
# Terrence is not responding to email.
|
||||||
|
#
|
||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.10 2004/04/19 22:12:11 stuart
|
||||||
|
# Release 0.6.9
|
||||||
|
#
|
||||||
|
# Revision 1.9 2004/04/18 03:29:35 stuart
|
||||||
|
# Pass most tests except -local and -rcpt-to
|
||||||
|
#
|
||||||
|
# Revision 1.8 2004/04/17 22:17:55 stuart
|
||||||
|
# Header comment method.
|
||||||
|
#
|
||||||
|
# Revision 1.7 2004/04/17 18:22:48 stuart
|
||||||
|
# Support default explanation.
|
||||||
|
#
|
||||||
|
# Revision 1.6 2004/04/06 20:18:02 stuart
|
||||||
|
# Fix bug in include
|
||||||
|
#
|
||||||
# Revision 1.5 2004/04/05 22:29:46 stuart
|
# Revision 1.5 2004/04/05 22:29:46 stuart
|
||||||
# SPF best_guess,
|
# SPF best_guess,
|
||||||
#
|
#
|
||||||
@@ -99,12 +118,13 @@ JOINERS = {'l': '.', 's': '.'}
|
|||||||
RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail',
|
RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail',
|
||||||
'pass': 'pass', 'fail': 'fail', 'unknown': 'unknown',
|
'pass': 'pass', 'fail': 'fail', 'unknown': 'unknown',
|
||||||
'neutral': 'neutral', 'softfail': 'softfail',
|
'neutral': 'neutral', 'softfail': 'softfail',
|
||||||
'none': 'none' }
|
'none': 'none', 'deny': 'fail' }
|
||||||
|
|
||||||
EXPLANATIONS = {'pass': 'sender SPF verified', 'fail': 'access denied',
|
EXPLANATIONS = {'pass': 'sender SPF verified', 'fail': 'access denied',
|
||||||
'unknown': 'SPF unknown', 'softfail': 'domain in transition',
|
'unknown': 'SPF unknown',
|
||||||
|
'softfail': 'domain in transition',
|
||||||
'neutral': 'access neither permitted nor denied',
|
'neutral': 'access neither permitted nor denied',
|
||||||
'none': 'no SPF records'
|
'none': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
# if set to a domain name, search _spf.domain namespace if no SPF record
|
# if set to a domain name, search _spf.domain namespace if no SPF record
|
||||||
@@ -123,7 +143,7 @@ except NameError:
|
|||||||
# standard default SPF record
|
# standard default SPF record
|
||||||
DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
|
DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
|
||||||
|
|
||||||
def check(i, s, h,default=None):
|
def check(i, s, h,local=None):
|
||||||
"""Test an incoming MAIL FROM:<s>, from a client with ip address i.
|
"""Test an incoming MAIL FROM:<s>, from a client with ip address i.
|
||||||
h is the HELO/EHLO domain name.
|
h is the HELO/EHLO domain name.
|
||||||
|
|
||||||
@@ -137,21 +157,7 @@ def check(i, s, h,default=None):
|
|||||||
#>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')
|
#>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if i.startswith('127.'):
|
return query(i=i, s=s, h=h,local=local).check()
|
||||||
return ('pass', 250, 'local connections always pass')
|
|
||||||
|
|
||||||
try:
|
|
||||||
q = query(i=i, s=s, h=h)
|
|
||||||
spf = q.dns_spf(q.d)
|
|
||||||
if not spf and default:
|
|
||||||
spf = default
|
|
||||||
return q.check(spf)
|
|
||||||
except DNS.DNSError:
|
|
||||||
return ('error', 450, 'SPF DNS Error')
|
|
||||||
|
|
||||||
def best_guess(i, s, h,spf=DEFAULT_SPF):
|
|
||||||
q = query(i=i, s=s, h=h)
|
|
||||||
return q.check(spf)
|
|
||||||
|
|
||||||
class query(object):
|
class query(object):
|
||||||
"""A query object keeps the relevant information about a single SPF
|
"""A query object keeps the relevant information about a single SPF
|
||||||
@@ -172,7 +178,7 @@ class query(object):
|
|||||||
|
|
||||||
Also keeps cache: DNS cache.
|
Also keeps cache: DNS cache.
|
||||||
"""
|
"""
|
||||||
def __init__(self, i, s, h):
|
def __init__(self, i, s, h,local=None):
|
||||||
self.i, self.s, self.h = i, s, h
|
self.i, self.s, self.h = i, s, h
|
||||||
self.l, self.o = split_email(s, h)
|
self.l, self.o = split_email(s, h)
|
||||||
self.t = str(int(time.time()))
|
self.t = str(int(time.time()))
|
||||||
@@ -180,6 +186,13 @@ class query(object):
|
|||||||
self.d = self.o
|
self.d = self.o
|
||||||
self.p = None
|
self.p = None
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
|
self.exps = dict(EXPLANATIONS)
|
||||||
|
self.local = local # local policy
|
||||||
|
|
||||||
|
def set_default_explanation(self,exp):
|
||||||
|
exps = self.exps
|
||||||
|
for i in 'softfail','fail','unknown':
|
||||||
|
exps[i] = exp
|
||||||
|
|
||||||
def getp(self):
|
def getp(self):
|
||||||
if not self.p:
|
if not self.p:
|
||||||
@@ -190,17 +203,32 @@ class query(object):
|
|||||||
self.p = self.i
|
self.p = self.i
|
||||||
return self.p
|
return self.p
|
||||||
|
|
||||||
def check(self, spf):
|
def best_guess(self,spf=DEFAULT_SPF):
|
||||||
|
"""Return a best guess based on a default SPF record"""
|
||||||
|
return self.check(spf)
|
||||||
|
|
||||||
|
def check(self, spf=None):
|
||||||
"""
|
"""
|
||||||
Returns (result, mta-status-code, explanation) where
|
Returns (result, mta-status-code, explanation) where
|
||||||
result in ['fail', 'unknown', 'pass']
|
result in ['fail', 'softfail', 'neutral' 'unknown', 'pass', 'error']
|
||||||
"""
|
"""
|
||||||
|
if self.i.startswith('127.'):
|
||||||
|
return ('pass', 250, 'local connections always pass')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not spf:
|
||||||
|
spf = self.dns_spf(self.d)
|
||||||
|
if self.local and spf:
|
||||||
|
spf += ' ' + self.local
|
||||||
return self.check1(spf, self.d, 0)
|
return self.check1(spf, self.d, 0)
|
||||||
|
except DNS.DNSError:
|
||||||
|
return ('error', 450, 'SPF DNS Error')
|
||||||
|
|
||||||
def check1(self, spf, domain, recursion):
|
def check1(self, spf, domain, recursion):
|
||||||
# spf rfc: 3.7 Processing Limits
|
# spf rfc: 3.7 Processing Limits
|
||||||
#
|
#
|
||||||
if recursion > 10:
|
if recursion > 20:
|
||||||
|
self.prob = 'Mechanisms used too many DNS lookups'
|
||||||
return ('unknown', 250, 'SPF recursion limit exceeded')
|
return ('unknown', 250, 'SPF recursion limit exceeded')
|
||||||
try:
|
try:
|
||||||
tmp, self.d = self.d, domain
|
tmp, self.d = self.d, domain
|
||||||
@@ -216,20 +244,21 @@ class query(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not spf:
|
if not spf:
|
||||||
return ('none', 250, 'no SPF records')
|
return ('none', 250, EXPLANATIONS['none'])
|
||||||
|
|
||||||
# split string by whitespace, drop the 'v=spf1'
|
# split string by whitespace, drop the 'v=spf1'
|
||||||
#
|
#
|
||||||
spf = spf.split()[1:]
|
spf = spf.split()[1:]
|
||||||
|
|
||||||
# copy of explanations to be modified by exp=
|
# copy of explanations to be modified by exp=
|
||||||
exps = dict(EXPLANATIONS)
|
exps = self.exps
|
||||||
redirect = None
|
redirect = None
|
||||||
|
|
||||||
# no mechanisms at all cause unknown result, unless
|
# no mechanisms at all cause unknown result, unless
|
||||||
# overridden with 'default=' modifier
|
# overridden with 'default=' modifier
|
||||||
#
|
#
|
||||||
default = 'neutral'
|
default = 'neutral'
|
||||||
|
self.mech = [] # unknown mechanisms
|
||||||
|
|
||||||
# Look for modifiers
|
# Look for modifiers
|
||||||
#
|
#
|
||||||
@@ -268,12 +297,21 @@ class query(object):
|
|||||||
|
|
||||||
if m == 'include':
|
if m == 'include':
|
||||||
if arg != self.d:
|
if arg != self.d:
|
||||||
tmp = self.check1(self.dns_spf(arg),
|
res,code,txt = self.check1(self.dns_spf(arg),
|
||||||
arg, recursion + 1)
|
arg, recursion + 1)
|
||||||
if tmp[0] == 'pass':
|
if res == 'pass':
|
||||||
break
|
break
|
||||||
if tmp[0] != 'fail':
|
if res in ('fail','neutral','softfail'):
|
||||||
return tmp
|
continue
|
||||||
|
if res == 'none':
|
||||||
|
self.prob = \
|
||||||
|
'Could not find a valid SPF record'
|
||||||
|
res = 'unknown'
|
||||||
|
return res,code,txt
|
||||||
|
else:
|
||||||
|
self.prob = 'Required option is missing'
|
||||||
|
self.mech.append(mech)
|
||||||
|
return ('unknown', 250, 'missing SPF option')
|
||||||
|
|
||||||
elif m == 'all':
|
elif m == 'all':
|
||||||
break
|
break
|
||||||
@@ -304,7 +342,9 @@ class query(object):
|
|||||||
else:
|
else:
|
||||||
# unknown mechanisms cause immediate unknown
|
# unknown mechanisms cause immediate unknown
|
||||||
# abort results
|
# abort results
|
||||||
return ('unknown', 250, mech)
|
self.mech.append(mech)
|
||||||
|
self.prob = 'Unknown mechanism found'
|
||||||
|
return ('unknown',250,'unknown SPF mechanism')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# no matches
|
# no matches
|
||||||
@@ -321,7 +361,10 @@ class query(object):
|
|||||||
|
|
||||||
def get_explanation(self, spec):
|
def get_explanation(self, spec):
|
||||||
"""Expand an explanation."""
|
"""Expand an explanation."""
|
||||||
|
if spec:
|
||||||
return self.expand(''.join(self.dns_txt(self.expand(spec))))
|
return self.expand(''.join(self.dns_txt(self.expand(spec))))
|
||||||
|
else:
|
||||||
|
return 'explanation : Required option is missing'
|
||||||
|
|
||||||
def expand(self, str):
|
def expand(self, str):
|
||||||
"""Do SPF RFC macro expansion.
|
"""Do SPF RFC macro expansion.
|
||||||
@@ -433,7 +476,9 @@ class query(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def dns_txt(self, domainname):
|
def dns_txt(self, domainname):
|
||||||
|
if domainname:
|
||||||
return [t for a in self.dns(domainname, 'TXT') for t in a]
|
return [t for a in self.dns(domainname, 'TXT') for t in a]
|
||||||
|
return []
|
||||||
|
|
||||||
def dns_mx(self, domainname):
|
def dns_mx(self, domainname):
|
||||||
"""Get a list of IP addresses for all MX exchanges for a
|
"""Get a list of IP addresses for all MX exchanges for a
|
||||||
@@ -490,6 +535,46 @@ class query(object):
|
|||||||
result = self.dns(cname, qtype)
|
result = self.dns(cname, qtype)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_header(self,res,receiver):
|
||||||
|
if res in ('pass','fail'):
|
||||||
|
return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
|
||||||
|
res,receiver,self.get_header_comment(res),self.i,
|
||||||
|
self.l + '@' + self.o, self.h)
|
||||||
|
if res == 'unknown':
|
||||||
|
return '%s (%s: %s)' % (' '.join([res] + self.mech),
|
||||||
|
receiver,self.get_header_comment(res))
|
||||||
|
return '%s (%s: %s)' % (res,receiver,self.get_header_comment(res))
|
||||||
|
|
||||||
|
def get_header_comment(self,res):
|
||||||
|
"""Return comment for Received-SPF header.
|
||||||
|
"""
|
||||||
|
sender = self.o
|
||||||
|
if res == 'pass':
|
||||||
|
if self.i.startswith('127.'):
|
||||||
|
return "localhost is always allowed."
|
||||||
|
else: return \
|
||||||
|
"domain of %s designates %s as permitted sender" \
|
||||||
|
% (sender,self.i)
|
||||||
|
elif res == 'softfail': return \
|
||||||
|
"transitioning domain of %s does not designate %s as permitted sender" \
|
||||||
|
% (sender,self.i)
|
||||||
|
elif res == 'neutral': return \
|
||||||
|
"%s is neither permitted nor denied by domain of %s" \
|
||||||
|
% (self.i,sender)
|
||||||
|
elif res == 'none': return \
|
||||||
|
"%s is neither permitted nor denied by domain of %s" \
|
||||||
|
% (self.i,sender)
|
||||||
|
#"%s does not designate permitted sender hosts" % sender
|
||||||
|
elif res == 'unknown': return \
|
||||||
|
"error in processing during lookup of domain of %s: %s" \
|
||||||
|
% (sender, self.prob)
|
||||||
|
elif res == 'error': return \
|
||||||
|
"error in processing during lookup of %s" % sender
|
||||||
|
elif res == 'fail': return \
|
||||||
|
"domain of %s does not designate %s as permitted sender" \
|
||||||
|
% (sender,self.i)
|
||||||
|
raise ValueError("invalid SPF result for header comment: "+res)
|
||||||
|
|
||||||
def split_email(s, h):
|
def split_email(s, h):
|
||||||
"""Given a sender email s and a HELO domain h, create a valid tuple
|
"""Given a sender email s and a HELO domain h, create a valid tuple
|
||||||
(l, d) local-part and domain-part.
|
(l, d) local-part and domain-part.
|
||||||
|
|||||||
Executable
+91
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/python2.3
|
||||||
|
# $Log$
|
||||||
|
# Revision 2.3 2004/04/19 22:12:11 stuart
|
||||||
|
# Release 0.6.9
|
||||||
|
#
|
||||||
|
# Revision 2.2 2004/04/18 03:29:35 stuart
|
||||||
|
# Pass most tests except -local and -rcpt-to
|
||||||
|
#
|
||||||
|
# Revision 2.1 2004/04/08 18:41:15 stuart
|
||||||
|
# Reject numeric hello names
|
||||||
|
#
|
||||||
|
# Driver for SPF test system
|
||||||
|
|
||||||
|
import spf
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
class PerlOptionParser(OptionParser):
|
||||||
|
def _process_args (self, largs, rargs, values):
|
||||||
|
"""_process_args(largs : [string],
|
||||||
|
rargs : [string],
|
||||||
|
values : Values)
|
||||||
|
|
||||||
|
Process command-line arguments and populate 'values', consuming
|
||||||
|
options and arguments from 'rargs'. If 'allow_interspersed_args' is
|
||||||
|
false, stop at the first non-option argument. If true, accumulate any
|
||||||
|
interspersed non-option arguments in 'largs'.
|
||||||
|
"""
|
||||||
|
while rargs:
|
||||||
|
arg = rargs[0]
|
||||||
|
# We handle bare "--" explicitly, and bare "-" is handled by the
|
||||||
|
# standard arg handler since the short arg case ensures that the
|
||||||
|
# len of the opt string is greater than 1.
|
||||||
|
if arg == "--":
|
||||||
|
del rargs[0]
|
||||||
|
return
|
||||||
|
elif arg[0:2] == "--":
|
||||||
|
# process a single long option (possibly with value(s))
|
||||||
|
self._process_long_opt(rargs, values)
|
||||||
|
elif arg[:1] == "-" and len(arg) > 1:
|
||||||
|
# process a single perl style long option
|
||||||
|
rargs[0] = '-' + arg
|
||||||
|
self._process_long_opt(rargs, values)
|
||||||
|
elif self.allow_interspersed_args:
|
||||||
|
largs.append(arg)
|
||||||
|
del rargs[0]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
def format(q):
|
||||||
|
res,code,txt = q.check()
|
||||||
|
print res
|
||||||
|
if res in ('pass','neutral','unknown'): print
|
||||||
|
else: print txt
|
||||||
|
print 'spfquery:',q.get_header_comment(res)
|
||||||
|
print 'Received-SPF:',q.get_header(res,'spfquery')
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
parser = PerlOptionParser()
|
||||||
|
parser.add_option("--file",dest="file")
|
||||||
|
parser.add_option("--ip",dest="ip")
|
||||||
|
parser.add_option("--sender",dest="sender")
|
||||||
|
parser.add_option("--helo",dest="hello_name")
|
||||||
|
parser.add_option("--local",dest="local_policy")
|
||||||
|
parser.add_option("--rcpt-to",dest="rcpt")
|
||||||
|
parser.add_option("--default-explanation",dest="explanation")
|
||||||
|
parser.add_option("--sanitize",type="int",dest="sanitize")
|
||||||
|
parser.add_option("--debug",type="int",dest="debug")
|
||||||
|
opts,args = parser.parse_args(argv)
|
||||||
|
if opts.ip:
|
||||||
|
q = spf.query(opts.ip,opts.sender,opts.hello_name,local=opts.local_policy)
|
||||||
|
if opts.explanation:
|
||||||
|
q.set_default_explanation(opts.explanation)
|
||||||
|
format(q)
|
||||||
|
if opts.file:
|
||||||
|
if opts.file == '0':
|
||||||
|
fp = sys.stdin
|
||||||
|
else:
|
||||||
|
fp = open(opts.file,'r')
|
||||||
|
for ln in fp:
|
||||||
|
ip,sender,helo,rcpt = ln.split(None,3)
|
||||||
|
q = spf.query(ip,sender,helo,local=opts.local_policy)
|
||||||
|
if opts.explanation:
|
||||||
|
q.set_default_explanation(opts.explanation)
|
||||||
|
format(q)
|
||||||
|
fp.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
main(sys.argv[1:])
|
||||||
Reference in New Issue
Block a user