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

This commit was generated by cvs2svn to track changes on a CVS vendor

branch.
parents c510c457 1205d50b
No related branches found
No related tags found
No related merge requests found
......@@ -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/*
......
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.
......
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.
......
#!/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__":
......
# 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, fre, xnax, valum, vlium, via-gra,
x@n3x, vicod3n, pens, 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
......
......@@ -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.
......
%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
......
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.
......
#!/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:])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment