This commit was generated by cvs2svn to track changes on a CVS vendor
branch.
This commit is contained in:
@@ -10,6 +10,7 @@ include testbms.py
|
||||
include testdspam.py
|
||||
include bms.py
|
||||
include spf.py
|
||||
include spfquery.py
|
||||
include test.py
|
||||
include sample.py
|
||||
include test/*
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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
|
||||
Support SPF delegation
|
||||
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
|
||||
Check for bounce with multiple rcpts
|
||||
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.
|
||||
Support SRS forgery detection.
|
||||
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
|
||||
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
|
||||
forwarder. Milter gets forwarder domain from alias and uses it to
|
||||
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.
|
||||
All email to those addresses is treated as known spam.
|
||||
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
# A simple milter.
|
||||
# $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
|
||||
# bugs from Redhat 9 testing
|
||||
#
|
||||
@@ -154,90 +181,6 @@
|
||||
# Revision 1.47 2003/08/26 05:01:38 stuart
|
||||
# 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>
|
||||
# Copyright 2001 Business Management Systems, Inc.
|
||||
# This code is under GPL. See COPYING for details.
|
||||
@@ -252,17 +195,22 @@ import Milter
|
||||
import tempfile
|
||||
import ConfigParser
|
||||
import time
|
||||
import re
|
||||
|
||||
from fnmatch import fnmatchcase
|
||||
from email.Header import decode_header
|
||||
|
||||
# Import pysrs if available
|
||||
try:
|
||||
import SRS
|
||||
import re
|
||||
srsre = re.compile(r'^SRS[01][+-=]',re.IGNORECASE)
|
||||
except: SRS = None
|
||||
|
||||
# Import spf if available
|
||||
try: import spf
|
||||
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
|
||||
#syslog.openlog('milter')
|
||||
|
||||
@@ -297,10 +245,12 @@ dspam_whitelist = {}
|
||||
dspam_screener = None
|
||||
dspam_internal = True # True if internal mail should be dspammed
|
||||
dspam_reject = ()
|
||||
dspam_sizelimit = 80000
|
||||
dspam_sizelimit = 180000
|
||||
srs = None
|
||||
srs_reject_spoofed = False
|
||||
spf_reject_neutral = ()
|
||||
spf_best_guess = False
|
||||
timeout = 600
|
||||
|
||||
class MilterConfigParser(ConfigParser.ConfigParser):
|
||||
|
||||
@@ -351,6 +301,7 @@ def read_config(list):
|
||||
cp = MilterConfigParser({
|
||||
'tempdir': "/var/log/milter/save",
|
||||
'socket': "/var/log/milter/pythonsock",
|
||||
'timeout': '600',
|
||||
'scan_html': 'no',
|
||||
'scan_rfc822': 'yes',
|
||||
'block_chinese': 'no',
|
||||
@@ -358,12 +309,14 @@ def read_config(list):
|
||||
'blind_wiretap': 'yes',
|
||||
'maxage': '8',
|
||||
'hashlength': '8',
|
||||
'reject_spoofed': 'no'
|
||||
'reject_spoofed': 'no',
|
||||
'best_guess': 'no'
|
||||
})
|
||||
cp.read(list)
|
||||
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')
|
||||
timeout = cp.getint('milter','timeout')
|
||||
scan_rfc822 = cp.getboolean('milter','scan_rfc822')
|
||||
scan_html = cp.getboolean('milter','scan_html')
|
||||
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_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_exempt = cp.getaddrset('dspam','dspam_exempt')
|
||||
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
|
||||
@@ -416,6 +369,7 @@ def read_config(list):
|
||||
if spf:
|
||||
spf.DELEGATE = cp.getdefault('spf','delegate')
|
||||
spf_reject_neutral = cp.getlist('spf','reject_neutral')
|
||||
spf_best_guess = cp.getboolean('spf','best_guess')
|
||||
srs_config = cp.getdefault('srs','config')
|
||||
if srs_config: cp.read([srs_config])
|
||||
srs_secret = cp.getdefault('srs','secret')
|
||||
@@ -526,6 +480,10 @@ class bmsMilter(Milter.Milter):
|
||||
def hello(self,hostname):
|
||||
self.hello_name = 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:
|
||||
self.log("REJECT: spam from self:",hostname)
|
||||
self.setreply('550','5.7.1','I hate talking to myself.')
|
||||
@@ -579,73 +537,32 @@ class bmsMilter(Milter.Milter):
|
||||
return Milter.CONTINUE
|
||||
|
||||
def check_spf(self):
|
||||
user,host = spf.split_email(self.canon_from,self.hello_name)
|
||||
self.sender = '@'.join((user,host))
|
||||
res,code,txt = spf.check(self.connectip,self.canon_from,self.hello_name)
|
||||
t = parse_addr(self.mailfrom)
|
||||
if len(t) == 2: t[1] = t[1].lower()
|
||||
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'):
|
||||
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)
|
||||
return Milter.REJECT
|
||||
if res == 'pass':
|
||||
# Received-SPF: pass (mybox.example.org: domain of
|
||||
# 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)
|
||||
if res == 'neutral' and q.o in spf_reject_neutral:
|
||||
self.log('REJECT: SPF neutral for',q.s)
|
||||
self.setreply('550','5.7.1',
|
||||
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % host
|
||||
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o
|
||||
)
|
||||
return Milter.REJECT
|
||||
# Received-SPF: neutral (mybox.example.org: 192.0.2.1 is neither
|
||||
# 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))
|
||||
if res == 'error':
|
||||
self.setreply(str(code),'4.3.0',txt)
|
||||
return Milter.TEMPFAIL
|
||||
else:
|
||||
self.log('SPF: %s %i %s' % (res,code,txt))
|
||||
self.add_header('Received-SPF',q.get_header(res,receiver))
|
||||
return Milter.CONTINUE
|
||||
|
||||
# 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)
|
||||
except:
|
||||
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.recipients.append('@'.join(t))
|
||||
user,domain = t
|
||||
@@ -708,7 +627,8 @@ class bmsMilter(Milter.Milter):
|
||||
return Milter.CONTINUE
|
||||
|
||||
# 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
|
||||
if lname == 'subject':
|
||||
|
||||
@@ -743,6 +663,7 @@ class bmsMilter(Milter.Milter):
|
||||
if not self.forward:
|
||||
if lval.startswith("fwd:") or lval.startswith("[fw"):
|
||||
self.log('REJECT: %s: %s' % (name,val))
|
||||
self.setreply('550','5.7.1','I find unedited forwards annoying')
|
||||
return Milter.REJECT
|
||||
|
||||
# check for invalid message id
|
||||
@@ -777,7 +698,7 @@ class bmsMilter(Milter.Milter):
|
||||
self.log('REJECT: %s: %s' % (name,hval))
|
||||
self.setreply('550','5.7.1',"We don't understand chinese")
|
||||
return Milter.REJECT
|
||||
rc = self.check_header(lname,val)
|
||||
rc = self.check_header(name,val)
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# log selected headers
|
||||
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
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
sys.stdout.flush()
|
||||
return Milter.CONTINUE
|
||||
|
||||
def abort(self):
|
||||
@@ -1047,7 +969,7 @@ def main():
|
||||
Milter.set_flags(flags)
|
||||
print "bms milter startup"
|
||||
sys.stdout.flush()
|
||||
Milter.runmilter("pythonfilter",socketname,600)
|
||||
Milter.runmilter("pythonfilter",socketname,timeout)
|
||||
print "bms milter shutdown"
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+12
-1
@@ -1,10 +1,15 @@
|
||||
# features intended to filter or block incoming mail
|
||||
[milter]
|
||||
;socket=/var/log/milter/pythonsock
|
||||
tempdir = /var/log/milter/save
|
||||
;timeout=600
|
||||
|
||||
scan_rfc822 = 1
|
||||
# can be CPU intensive
|
||||
scan_html = 0
|
||||
# reject asian fonts because we can't read them
|
||||
block_chinese = 1
|
||||
# users who hate forwarded mail
|
||||
;block_forward = egghead@mycorp.com, busybee@mycorp.com
|
||||
log_headers = 0
|
||||
# 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 = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
|
||||
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 = $$$, !!!, XXX, FREE, HGH
|
||||
|
||||
@@ -43,6 +50,8 @@ reject_spoofed = 0
|
||||
;delegate = domain.com
|
||||
# domains where a neutral SPF result should cause mail to be rejected
|
||||
;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
|
||||
[scrub]
|
||||
@@ -93,6 +102,8 @@ blind = 1
|
||||
# defining this activates the dspam application
|
||||
# dspam and dspam-python must be installed
|
||||
;dspam_userdir=/var/lib/dspam
|
||||
# do not dspam messages larger than this
|
||||
;dspam_sizelimit=180000
|
||||
|
||||
# Map email addresses and aliases to dspam users
|
||||
;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>
|
||||
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>
|
||||
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> |
|
||||
<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/">
|
||||
dspam bayes filter project</a> and <a href="dspam.html">
|
||||
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",
|
||||
which rejects messages with spammy headers. Since sendmail has to
|
||||
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>
|
||||
|
||||
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
|
||||
production systems long enough to convince me that it is stable. As
|
||||
the package gains more features and complexity, stable will mean no
|
||||
bug reports from outside users either.
|
||||
<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>
|
||||
<a href="http://bmsi.com/python/milter-0.6.7.tar.gz">
|
||||
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
|
||||
</ul>
|
||||
<p>
|
||||
<a name="stable"><b>Stable</b></a>
|
||||
<a href="http://bmsi.com/python/milter-0.6.6.tar.gz">
|
||||
milter-0.6.6.tar.gz</a> Plug another memory leak,
|
||||
<a href="http://spf.pobox.com/">SPF</a> support, hello blacklist.
|
||||
|
||||
+17
-5
@@ -1,10 +1,10 @@
|
||||
%define name milter
|
||||
%define version 0.6.8
|
||||
%define version 0.6.9
|
||||
%define release 1
|
||||
# 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)
|
||||
%define sysvinit rc
|
||||
#define sysvinit milter.rc
|
||||
%ifos Linux
|
||||
%define python python2.3
|
||||
%else
|
||||
@@ -16,7 +16,7 @@ Name: %{name}
|
||||
Version: %{version}
|
||||
Release: %{release}
|
||||
Source: %{name}-%{version}.tar.gz
|
||||
#Patch: %{name}.patch
|
||||
#Patch: %{name}-%{version}.patch
|
||||
Copyright: GPL
|
||||
Group: Development/Libraries
|
||||
BuildRoot: %{_tmppath}/%{name}-buildroot
|
||||
@@ -81,7 +81,7 @@ exec >>milter.log 2>&1
|
||||
echo $! >/var/run/milter/milter.pid
|
||||
EOF
|
||||
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'
|
||||
/^python=/
|
||||
c
|
||||
@@ -127,6 +127,18 @@ rm -rf $RPM_BUILD_ROOT
|
||||
%config /var/log/milter/milter.cfg
|
||||
|
||||
%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
|
||||
- Don't report spoofed unless rcpt looks like SRS
|
||||
- Check for bounce with multiple rcpts
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import os
|
||||
import sys
|
||||
from distutils.core import setup, Extension
|
||||
|
||||
# FIXME: on some versions of sendmail, smutil is renamed to sm
|
||||
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",
|
||||
long_description="""\
|
||||
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
|
||||
# 18-dec-2003, v1.6, Failures on Intel hardware: endianness. Use ! on
|
||||
# struct.pack(), struct.unpack().
|
||||
#
|
||||
# Development taken over by Stuart Gathman <stuart@bmsi.com> since
|
||||
# Terrence is not responding to email.
|
||||
#
|
||||
# $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
|
||||
# SPF best_guess,
|
||||
#
|
||||
@@ -99,12 +118,13 @@ JOINERS = {'l': '.', 's': '.'}
|
||||
RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail',
|
||||
'pass': 'pass', 'fail': 'fail', 'unknown': 'unknown',
|
||||
'neutral': 'neutral', 'softfail': 'softfail',
|
||||
'none': 'none' }
|
||||
'none': 'none', 'deny': 'fail' }
|
||||
|
||||
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',
|
||||
'none': 'no SPF records'
|
||||
'none': ''
|
||||
}
|
||||
|
||||
# if set to a domain name, search _spf.domain namespace if no SPF record
|
||||
@@ -123,7 +143,7 @@ except NameError:
|
||||
# standard default SPF record
|
||||
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.
|
||||
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')
|
||||
|
||||
"""
|
||||
if i.startswith('127.'):
|
||||
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)
|
||||
return query(i=i, s=s, h=h,local=local).check()
|
||||
|
||||
class query(object):
|
||||
"""A query object keeps the relevant information about a single SPF
|
||||
@@ -172,7 +178,7 @@ class query(object):
|
||||
|
||||
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.l, self.o = split_email(s, h)
|
||||
self.t = str(int(time.time()))
|
||||
@@ -180,6 +186,13 @@ class query(object):
|
||||
self.d = self.o
|
||||
self.p = None
|
||||
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):
|
||||
if not self.p:
|
||||
@@ -190,17 +203,32 @@ class query(object):
|
||||
self.p = self.i
|
||||
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
|
||||
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)
|
||||
except DNS.DNSError:
|
||||
return ('error', 450, 'SPF DNS Error')
|
||||
|
||||
def check1(self, spf, domain, recursion):
|
||||
# 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')
|
||||
try:
|
||||
tmp, self.d = self.d, domain
|
||||
@@ -216,20 +244,21 @@ class query(object):
|
||||
"""
|
||||
|
||||
if not spf:
|
||||
return ('none', 250, 'no SPF records')
|
||||
return ('none', 250, EXPLANATIONS['none'])
|
||||
|
||||
# split string by whitespace, drop the 'v=spf1'
|
||||
#
|
||||
spf = spf.split()[1:]
|
||||
|
||||
# copy of explanations to be modified by exp=
|
||||
exps = dict(EXPLANATIONS)
|
||||
exps = self.exps
|
||||
redirect = None
|
||||
|
||||
# no mechanisms at all cause unknown result, unless
|
||||
# overridden with 'default=' modifier
|
||||
#
|
||||
default = 'neutral'
|
||||
self.mech = [] # unknown mechanisms
|
||||
|
||||
# Look for modifiers
|
||||
#
|
||||
@@ -268,12 +297,21 @@ class query(object):
|
||||
|
||||
if m == 'include':
|
||||
if arg != self.d:
|
||||
tmp = self.check1(self.dns_spf(arg),
|
||||
res,code,txt = self.check1(self.dns_spf(arg),
|
||||
arg, recursion + 1)
|
||||
if tmp[0] == 'pass':
|
||||
if res == 'pass':
|
||||
break
|
||||
if tmp[0] != 'fail':
|
||||
return tmp
|
||||
if res in ('fail','neutral','softfail'):
|
||||
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':
|
||||
break
|
||||
@@ -304,7 +342,9 @@ class query(object):
|
||||
else:
|
||||
# unknown mechanisms cause immediate unknown
|
||||
# abort results
|
||||
return ('unknown', 250, mech)
|
||||
self.mech.append(mech)
|
||||
self.prob = 'Unknown mechanism found'
|
||||
return ('unknown',250,'unknown SPF mechanism')
|
||||
|
||||
else:
|
||||
# no matches
|
||||
@@ -321,7 +361,10 @@ class query(object):
|
||||
|
||||
def get_explanation(self, spec):
|
||||
"""Expand an explanation."""
|
||||
if spec:
|
||||
return self.expand(''.join(self.dns_txt(self.expand(spec))))
|
||||
else:
|
||||
return 'explanation : Required option is missing'
|
||||
|
||||
def expand(self, str):
|
||||
"""Do SPF RFC macro expansion.
|
||||
@@ -433,7 +476,9 @@ class query(object):
|
||||
return None
|
||||
|
||||
def dns_txt(self, domainname):
|
||||
if domainname:
|
||||
return [t for a in self.dns(domainname, 'TXT') for t in a]
|
||||
return []
|
||||
|
||||
def dns_mx(self, domainname):
|
||||
"""Get a list of IP addresses for all MX exchanges for a
|
||||
@@ -490,6 +535,46 @@ class query(object):
|
||||
result = self.dns(cname, qtype)
|
||||
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):
|
||||
"""Given a sender email s and a HELO domain h, create a valid tuple
|
||||
(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