Compare commits

...

34 Commits

Author SHA1 Message Date
cvs2svn 9e6d1a8cb7 This commit was manufactured by cvs2svn to create tag 'milter-0_8_5'.
Sprout from master 2005-12-29 22:46:07 UTC Stuart Gathman <stuart@gathman.org> 'Documentation updates.'
Cherrypick from bmsi 2005-05-31 18:23:49 UTC Stuart Gathman <stuart@gathman.org> 'Development changes since 0.7.2':
    README
    cid2spf.py
    rejects.py
    rhsbl.m4
    sample.py
    test.py
    test/amazon
    test/big5
    test/bounce
    test/bounce1
    test/bound
    test/honey
    test/missingboundary
    test/samp1
    test/spam44
    test/spam7
    test/spam8
    test/test1
    test/test8
    test/virus1
    test/virus13
    test/virus2
    test/virus3
    test/virus4
    test/virus5
    test/virus6
    test/virus7
    testsample.py
2005-12-29 22:46:08 +00:00
Stuart Gathman 63e45eb884 Documentation updates. 2005-12-29 22:46:07 +00:00
Stuart Gathman 28bc84eda0 Release 0.8.5 2005-12-29 19:33:18 +00:00
Stuart Gathman 7f7f2500dc Include report. 2005-12-29 19:23:14 +00:00
Stuart Gathman 4f220b48cf Release 0.8.5 2005-12-29 19:21:37 +00:00
Stuart Gathman a9ca154a92 Handle NULL MX 2005-12-29 19:15:35 +00:00
Stuart Gathman 65672fb26f Update log parser for new ops, etc 2005-12-29 04:50:39 +00:00
Stuart Gathman 155eb4e675 Do not auto-whitelist autoreplys 2005-12-29 04:49:10 +00:00
Stuart Gathman 14d5869019 parse milter.log from bms.py into a sequence of connections 2005-12-28 22:24:34 +00:00
Stuart Gathman 28ca3b2837 Expire and renew AddrCache entries 2005-12-28 20:17:29 +00:00
Stuart Gathman 52b0ac9377 Put guessed result in separate header. 2005-12-23 22:34:46 +00:00
Stuart Gathman 8bc182cb37 Move Received-SPF header to top. 2005-12-23 21:47:07 +00:00
Stuart Gathman fb3c140d4c Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER) 2005-12-23 21:46:36 +00:00
Stuart Gathman 52d23604f7 Always include keyword data in Received-SPF header. 2005-12-23 21:44:15 +00:00
Stuart Gathman 15f8b797bf Select neutral DSN template for best_guess 2005-12-09 16:54:01 +00:00
Stuart Gathman 3b544a4076 improve gossip support.
Initialize srs_domain from srs.srs config property.  Should probably
always block unsigned DSN when signing all.
2005-12-01 22:42:32 +00:00
Stuart Gathman 36a7dce2e5 Fix neutral policy. pobox.com -> openspf.org 2005-12-01 18:59:25 +00:00
Stuart Gathman a418f34491 GOSSiP support, local database only. 2005-11-07 21:22:35 +00:00
Stuart Gathman ba5854fc91 Simple implementation of trusted_forwarder list. Inefficient for
more than 1 or 2 entries.
2005-10-31 00:09:41 +00:00
Stuart Gathman a0878320fa Doc updates 2005-10-31 00:09:12 +00:00
Stuart Gathman d1583d88c9 Add titles. 2005-10-30 01:08:52 +00:00
Stuart Gathman 3ad67bd33b Ignore records missing spaces. 2005-10-30 01:08:14 +00:00
Stuart Gathman eb2e730b5d Don't check internal_domains for trusted_relay. 2005-10-28 19:36:54 +00:00
Stuart Gathman daa1eacff3 Do not send quarantine DSN when sender is DSN. 2005-10-28 09:30:49 +00:00
Stuart Gathman aaf23f35f8 New webpage design based on ht2html. 2005-10-25 21:39:47 +00:00
Stuart Gathman 25b6378631 Consider MAIL FROM a match for supply_sender when a subdomain of From or Sender 2005-10-23 16:01:30 +00:00
Stuart Gathman c6ac3ddad8 Release 0.8.4 2005-10-20 23:36:11 +00:00
Stuart Gathman b3dce26928 Include smfi_progress is SMFIR_PROGRESS defined 2005-10-20 23:23:36 +00:00
Stuart Gathman fcd85dbfb5 Add optional idx for position of added header. 2005-10-20 23:04:49 +00:00
Stuart Gathman 3a1c964f0d Configure auto_whitelist senders. 2005-10-20 18:47:27 +00:00
Stuart Gathman 36ae390f01 access.db stores keys in lower case 2005-10-19 21:07:49 +00:00
Stuart Gathman 4c0cf4fb95 Train screener on whitelisted messages. 2005-10-19 19:37:50 +00:00
Stuart Gathman 8f8de8fa97 Auto whitelist refinements. 2005-10-14 16:17:31 +00:00
Stuart Gathman bc516456c1 Auto whitelist feature. 2005-10-14 01:14:08 +00:00
26 changed files with 1040 additions and 406 deletions
+4 -1
View File
@@ -5,8 +5,11 @@ wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a
real, usable Python extension.
Other contributors:
Other contributors (in random order):
Dave MacQuigg
for noticing that smfi_insheader wasn't supported, and creating
a template to help first time pymilter users create their own milter.
Terence Way
for providing a Python port of SPF
Scott Kitterman
+2
View File
@@ -10,6 +10,7 @@ include testmime.py
include testbms.py
include testdspam.py
include rejects.py
include report.py
include bms.py
include spf.py
include cid2spf.py
@@ -17,6 +18,7 @@ include spfquery.py
include test.py
include sample.py
include test/*
include doc/*
include Milter/*.py
include *.spec
include start.sh
+4 -4
View File
@@ -16,7 +16,7 @@ from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
try: from milter import QUARANTINE
except: pass
__version__ = '0.8.3'
__version__ = '0.8.5'
_seq_lock = thread.allocate_lock()
_seq = 0
@@ -44,7 +44,7 @@ class Milter:
for i in msg: print i,
print
def connect(self,hostname,unused,hostaddr):
def connect(self,hostname,family,hostaddr):
"Called for each connection to sendmail."
self.log("connect from %s at %s" % (hostname,hostaddr))
return CONTINUE
@@ -106,8 +106,8 @@ class Milter:
return self.__ctx.setreply(rcode,xcode,msg,*ml)
# Milter methods which can only be called from eom callback.
def addheader(self,field,value):
return self.__ctx.addheader(field,value)
def addheader(self,field,value,idx=-1):
return self.__ctx.addheader(field,value,idx)
def chgheader(self,field,idx,value):
return self.__ctx.chgheader(field,idx,value)
+14
View File
@@ -1,5 +1,19 @@
Here is a history of user visible changes to Python milter.
0.8.5 Simple trusted_forwarder implementation.
Fix access_file neutral policy
Move Received-SPF header to beginning of headers
Supply keyword info for all results in Received-SPF header.
Move guessed SPF result to separate header
Activate smfi_insheader only when SMFIR_INSHEADER defined
Handle NULL MX in spf.py
in-process GOSSiP server support (to be extended later)
Expire CBV cache and renew auto-whitelist entries
0.8.4 Auto-whitelist recipients of outgoing email.
Fix SPF policy via sendmail access map (case insensitive keys).
Train screener on whitelisted messages
Optional idx parameter to addheader to invoke smfi_insheader
Activate progress API when SMFIR_PROGRESS defined
0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
spf_accept_fail option for braindead SPF senders
(treats fail like softfail)
+13 -9
View File
@@ -1,10 +1,16 @@
Require signed MFROM for all incoming bounces when signing all outgoing mail -
except from trusted relays.
Send DSN for permerror before processing extended result. An additional
DSN may be sent based on extended result.
Rescind whitelist for banned extensions, in case sender is infected.
Train honeypot on error only.
Find rfc2822 policy for MFROM quoting.
Use /etc/mail/access for domain specific SPF policies.
SPF-Fail: REJECT
SPF-Softfail: OK
SPF-Neutral: OK
Support explicit errors for SPF policy in access file:
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
Defer TEMPERROR in SPF evaluation - give precedence to security
@@ -16,9 +22,6 @@ Option to add Received-SPF header, but never reject on SPF.
Create null config that does nothing - except maybe add Received-SPF
headers. Many admins would like to turn features on one at a time.
Auto whitelist based on outgoing email - perhaps with magic subject
or recipient prefix.
Can't output messages with malformed rfc822 attachments.
Move milter,Milter,mime,spf modules to pymilter
@@ -33,7 +36,8 @@ check spam keywords with character classes, e.g.
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
user to give to the forwarder. (Or user just adds arbitrary alias
unique to that forwarder to a database.) Alias only works for mail from that
forwarder. Milter gets forwarder domain from alias and uses it to
SPF check forwarder.
+251 -55
View File
@@ -1,6 +1,60 @@
#!/usr/bin/env python
# A simple milter that has grown quite a bit.
# $Log$
# Revision 1.46 2005/12/28 20:17:29 customdesigned
# Expire and renew AddrCache entries
#
# Revision 1.45 2005/12/23 22:34:46 customdesigned
# Put guessed result in separate header.
#
# Revision 1.44 2005/12/23 21:47:07 customdesigned
# Move Received-SPF header to top.
#
# Revision 1.43 2005/12/09 16:54:01 customdesigned
# Select neutral DSN template for best_guess
#
# Revision 1.42 2005/12/01 22:42:32 customdesigned
# improve gossip support.
# Initialize srs_domain from srs.srs config property. Should probably
# always block unsigned DSN when signing all.
#
# Revision 1.41 2005/12/01 18:59:25 customdesigned
# Fix neutral policy. pobox.com -> openspf.org
#
# Revision 1.40 2005/11/07 21:22:35 customdesigned
# GOSSiP support, local database only.
#
# Revision 1.39 2005/10/31 00:04:58 customdesigned
# Simple implementation of trusted_forwarder list. Inefficient for
# more than 1 or 2 entries.
#
# Revision 1.38 2005/10/28 19:36:54 customdesigned
# Don't check internal_domains for trusted_relay.
#
# Revision 1.37 2005/10/28 09:30:49 customdesigned
# Do not send quarantine DSN when sender is DSN.
#
# Revision 1.36 2005/10/23 16:01:29 customdesigned
# Consider MAIL FROM a match for supply_sender when a subdomain of From or Sender
#
# Revision 1.35 2005/10/20 18:47:27 customdesigned
# Configure auto_whitelist senders.
#
# Revision 1.34 2005/10/19 21:07:49 customdesigned
# access.db stores keys in lower case
#
# Revision 1.33 2005/10/19 19:37:50 customdesigned
# Train screener on whitelisted messages.
#
# Revision 1.32 2005/10/14 16:17:31 customdesigned
# Auto whitelist refinements.
#
# Revision 1.31 2005/10/14 01:14:08 customdesigned
# Auto whitelist feature.
#
# Revision 1.30 2005/10/12 16:36:30 customdesigned
# Release 0.8.3
#
# Revision 1.29 2005/10/11 22:50:07 customdesigned
# Always check HELO except for SPF pass, temperror.
#
@@ -290,6 +344,12 @@ from Milter.dynip import is_dynip as dynip
from fnmatch import fnmatchcase
from email.Header import decode_header
# Import gossip if available
try:
import gossip
from gossip.server import Gossip
except: gossip = None
# Import pysrs if available
try:
import SRS
@@ -328,6 +388,7 @@ scan_html = True
scan_rfc822 = True
internal_connect = ()
trusted_relay = ()
trusted_forwarder = ()
internal_domains = ()
banned_users = ()
hello_blacklist = ()
@@ -337,6 +398,7 @@ dspam_users = {}
dspam_userdir = None
dspam_exempt = {}
dspam_whitelist = {}
whitelist_senders = {}
dspam_screener = ()
dspam_internal = True # True if internal mail should be dspammed
dspam_reject = ()
@@ -352,9 +414,8 @@ spf_best_guess = False
spf_reject_noptr = False
supply_sender = False
access_file = None
time_format = '%Y%b%d %H:%M:%S %Z'
timeout = 600
cbv_cache = {}
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
@@ -363,18 +424,8 @@ logging.basicConfig(
)
milter_log = logging.getLogger('milter')
try:
too_old = time.time() - 7*24*60*60 # 7 days
for ln in open('send_dsn.log'):
try:
rcpt,ts = ln.strip().split(None,1)
l = time.strptime(ts,time_format)
t = time.mktime(l)
if t > too_old:
cbv_cache[rcpt] = None
except:
cbv_cache[ln.strip()] = None
except IOError: pass
if gossip:
gossip_node = Gossip('gossip4.db',120)
class MilterConfigParser(ConfigParser.ConfigParser):
@@ -499,6 +550,8 @@ def read_config(list):
# dspam section
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt, dspam_internal
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
global whitelist_senders
whitelist_senders = cp.getaddrset('dspam','whitelist_senders')
dspam_dict = cp.getdefault('dspam','dspam_dict')
dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
@@ -513,6 +566,7 @@ def read_config(list):
# spf section
global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr
global spf_accept_softfail,spf_accept_fail,supply_sender,access_file
global trusted_forwarder
if spf:
spf.DELEGATE = cp.getdefault('spf','delegate')
spf_reject_neutral = cp.getlist('spf','reject_neutral')
@@ -522,6 +576,7 @@ def read_config(list):
spf_reject_noptr = cp.getboolean('spf','reject_noptr')
supply_sender = cp.getboolean('spf','supply_sender')
access_file = cp.getdefault('spf','access_file')
trusted_forwarder = cp.getlist('spf','trusted_forwarder')
srs_config = cp.getdefault('srs','config')
if srs_config: cp.read([srs_config])
srs_secret = cp.getdefault('srs','secret')
@@ -543,7 +598,7 @@ def read_config(list):
ses = SES.new(secret=srs_secret,expiration=maxage)
srs_domain = cp.getlist('srs','ses')
else:
srs_domain = []
srs_domain = cp.getlist('srs','srs')
srs_domain.append(cp.getdefault('srs','fwdomain'))
banned_users = cp.getlist('srs','banned_users')
#print srs_domain
@@ -614,7 +669,7 @@ class SPFPolicy(object):
return None
def getFailPolicy(self):
policy = self.getPolicy('SPF-Fail:')
policy = self.getPolicy('spf-fail:')
if not policy:
if self.domain in spf_accept_fail:
policy = 'CBV'
@@ -623,7 +678,7 @@ class SPFPolicy(object):
return policy
def getNonePolicy(self):
policy = self.getPolicy('SPF-None:')
policy = self.getPolicy('spf-none:')
if not policy:
if spf_reject_noptr:
policy = 'REJECT'
@@ -632,7 +687,7 @@ class SPFPolicy(object):
return policy
def getSoftfailPolicy(self):
policy = self.getPolicy('SPF-Softfail:')
policy = self.getPolicy('spf-softfail:')
if not policy:
if self.domain in spf_accept_softfail:
policy = 'OK'
@@ -643,7 +698,7 @@ class SPFPolicy(object):
return policy
def getNeutralPolicy(self):
policy = self.getPolicy('SPF-Neutral:')
policy = self.getPolicy('spf-neutral:')
if not policy:
if self.domain in spf_reject_neutral:
policy = 'REJECT'
@@ -651,17 +706,80 @@ class SPFPolicy(object):
return policy
def getPermErrorPolicy(self):
policy = self.getPolicy('SPF-PermError:')
policy = self.getPolicy('spf-permerror:')
if not policy:
policy = 'REJECT'
return policy
def getPassPolicy(self):
policy = self.getPolicy('SPF-Pass:')
policy = self.getPolicy('spf-pass:')
if not policy:
policy = 'OK'
return policy
class AddrCache(object):
time_format = '%Y%b%d %H:%M:%S %Z'
def __init__(self,renew=7):
self.age = renew
def load(self,fname,age=0):
if not age:
age = self.age
self.fname = fname
cache = {}
self.cache = cache
now = time.time()
try:
too_old = now - age*24*60*60 # max age in days
for ln in open(self.fname):
try:
rcpt,ts = ln.strip().split(None,1)
l = time.strptime(ts,AddrCache.time_format)
t = time.mktime(l)
if t > too_old:
cache[rcpt.lower()] = (t,None)
except:
cache[ln.strip().lower()] = (now,None)
except IOError: pass
def has_key(self,sender):
try:
ts,res = self.cache[sender.lower()]
too_old = time.time() - self.age*24*60*60 # max age in days
if ts > too_old:
return True
del self.cache[sender.lower()]
except KeyError:
pass
return False
def __getitem__(self,sender):
ts,res = self.cache[sender.lower()]
too_old = time.time() - self.age*24*60*60 # max age in days
if ts > too_old:
return res
del self.cache[sender.lower()]
raise KeyError, sender
def __setitem__(self,sender,res):
lsender = sender.lower()
now = time.time()
cached = self.has_key(sender)
if not cached:
self.cache[lsender] = (now,res)
if not res:
s = time.strftime(AddrCache.time_format,time.localtime(now))
print >>open(self.fname,'a'),sender,s # log refreshed senders
def __len__(self):
return len(self.cache)
cbv_cache = AddrCache(renew=7)
cbv_cache.load('send_dsn.log',age=7)
auto_whitelist = AddrCache(renew=30)
auto_whitelist.load('auto_whitelist.log',age=120)
class bmsMilter(Milter.Milter):
"""Milter to replace attachments poisonous to Windows with a WARNING message,
check SPF, and other anti-forgery features, and implement wiretapping
@@ -694,14 +812,15 @@ class bmsMilter(Milter.Milter):
# addheader can only be called from eom(). This accumulates added headers
# which can then be applied by alter_headers()
def add_header(self,name,val):
self.new_headers.append((name,val))
def add_header(self,name,val,idx=-1):
self.new_headers.append((name,val,idx))
self.log('%s: %s' % (name,val))
def connect(self,hostname,unused,hostaddr):
self.internal_connection = False
self.trusted_relay = False
self.receiver = self.getsymval('j')
# sometimes people put extra space in sendmail config, so we strip
self.receiver = self.getsymval('j').strip()
if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0]
for pat in internal_connect:
@@ -778,6 +897,7 @@ class bmsMilter(Milter.Milter):
self.hidepath = False
self.discard = False
self.dspam = True
self.whitelist = False
self.reject_spam = True
self.data_allowed = True
self.trust_received = self.trusted_relay
@@ -787,6 +907,7 @@ class bmsMilter(Milter.Milter):
self.new_headers = []
self.recipients = []
self.cbv_needed = None
self.whitelist_sender = False
t = parse_addr(f)
if len(t) == 2: t[1] = t[1].lower()
self.canon_from = '@'.join(t)
@@ -826,22 +947,28 @@ class bmsMilter(Milter.Milter):
if len(t) == 2:
user,domain = t
if not self.internal_connection:
if not self.trusted_relay:
for pat in internal_domains:
if fnmatchcase(domain,pat):
self.log("REJECT: spam from self",pat)
self.setreply('550','5.7.1','I hate talking to myself.')
return Milter.REJECT
elif internal_domains:
else:
if internal_domains:
for pat in internal_domains:
if fnmatchcase(domain,pat): break
else:
self.log("REJECT: zombie PC at ",self.connectip," sending MAIL FROM ",
self.canon_from)
self.log("REJECT: zombie PC at ",self.connectip,
" sending MAIL FROM ",self.canon_from)
self.setreply('550','5.7.1',
'Your PC is using an unauthorized MAIL FROM.',
'It is either badly misconfigured or controlled by organized crime.'
)
return Milter.REJECT
wl_users = whitelist_senders.get(domain,())
if user in wl_users or '' in wl_users:
self.whitelist_sender = True
self.rejectvirus = domain in reject_virus_from
if user in wiretap_users.get(domain,()):
self.add_recipient(wiretap_dest)
@@ -853,22 +980,32 @@ class bmsMilter(Milter.Milter):
self.dspam = False
else:
self.rejectvirus = False
domain = None
if not self.hello_name:
self.log("REJECT: missing HELO")
self.setreply('550','5.7.1',"It's polite to say HELO first.")
return Milter.REJECT
self.umis = None
if not (self.internal_connection or self.trusted_relay) \
and self.connectip and spf:
return self.check_spf()
else:
self.spf = None
return Milter.CONTINUE
def check_spf(self):
receiver = self.receiver
for tf in trusted_forwarder:
q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False)
res,code,txt = q.check()
if res == 'pass':
self.log("TRUSTED_FORWARDER:",tf)
break
else:
q = spf.query(self.connectip,self.canon_from,self.hello_name,
receiver=receiver,strict=False)
q.set_default_explanation(
'SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s' % (q.s,q.i))
'SPF fail: see http://openspf.org/why.html?sender=%s&ip=%s' % (q.s,q.i))
res,code,txt = q.check()
q.result = res
if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext:
@@ -898,14 +1035,13 @@ class bmsMilter(Milter.Milter):
if spf_best_guess and res == 'none':
#self.log('SPF: no record published, guessing')
q.set_default_explanation(
'SPF guess: see http://spf.pobox.com/why.html')
'SPF guess: see http://openspf.org/why.html')
# best_guess should not result in fail
if self.missing_ptr:
# ignore dynamic PTR for best guess
res,code,txt = q.best_guess('v=spf1 a/24 mx/24')
else:
res,code,txt = q.best_guess()
receiver += ': guessing'
if q.perm_error: # FIXME: should never happen?
res,code,txt = q.perm_error.ext # extended result
txt = 'EXT: ' + txt
@@ -919,10 +1055,11 @@ class bmsMilter(Milter.Milter):
elif policy != 'OK':
self.log('REJECT: no PTR, HELO or SPF')
self.setreply('550','5.7.1',
"You must have a reverse lookup or publish SPF: http://spf.pobox.com",
"Contact your mail administrator IMMEDIATELY! Your mail server is",
"severely misconfigured. It has no PTR record (dynamic PTR records",
"that contain your IP don't count), an invalid HELO, and no SPF record."
"You must have a valid HELO or publish SPF: http://www.openspf.org ",
"Contact your mail administrator IMMEDIATELY! Your mail server is ",
"severely misconfigured. It has no PTR record (dynamic PTR records ",
"that contain your IP don't count), an invalid or dynamic HELO, ",
"and no SPF record."
)
return Milter.REJECT
if res in ('deny', 'fail'):
@@ -952,15 +1089,16 @@ class bmsMilter(Milter.Milter):
'notify your administrator of the problem immediately.'
)
return Milter.REJECT
if res == 'neutral' and q.o in spf_reject_neutral:
if res == 'neutral':
policy = p.getNeutralPolicy()
if policy == 'CBV' and hres == 'pass':
if self.mailfrom != '<>':
self.cbv_needed = q
q.result = res # select neutral DSN template
elif policy != 'OK':
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' % q.o,
'mail from %s must pass SPF: http://openspf.org/why.html' % q.o,
'The %s domain is one that spammers love to forge. Due to' % q.o,
'the volume of forged mail, we can only accept mail that',
'the SPF record for %s explicitly designates as legitimate.' % q.o,
@@ -985,8 +1123,21 @@ class bmsMilter(Milter.Milter):
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'4.3.0',txt)
return Milter.TEMPFAIL
self.add_header('Received-SPF',q.get_header(res,receiver))
self.add_header('Received-SPF',q.get_header(q.result,receiver),0)
if res != q.result:
self.add_header('X-Guessed-SPF',res,0)
self.spf = q
if res == 'pass' and auto_whitelist.has_key(self.canon_from):
self.whitelist = True
self.log("WHITELIST",self.canon_from)
if gossip:
if res == 'pass':
qual = 'SPF'
else:
qual = self.connectip
self.umis = gossip.umis(q.o+qual,self.id+time.time())
res,hdr,val = gossip_node.query(self.umis,q.o,qual,1)
self.add_header(hdr,val)
return Milter.CONTINUE
# hide_path causes a copy of the message to be saved - until we
@@ -998,8 +1149,9 @@ class bmsMilter(Milter.Milter):
self.log('DISCARD: RCPT TO:',to,str)
return Milter.DISCARD
self.log("rcpt to",to,str)
t = parse_addr(to.lower())
t = parse_addr(to)
if len(t) == 2:
t[1] = t[1].lower()
user,domain = t
if self.is_bounce and srs and domain in srs_domain:
oldaddr = '@'.join(parse_addr(to))
@@ -1014,6 +1166,7 @@ class bmsMilter(Milter.Milter):
newaddr = srs.reverse(oldaddr)
# Currently, a sendmail map reverses SRS. We just log it here.
self.log("srs rcpt:",newaddr)
self.dspam = False # verified as reply to mail we sent
except:
if not (self.internal_connection or self.trusted_relay):
if srsre.match(oldaddr):
@@ -1027,7 +1180,8 @@ class bmsMilter(Milter.Milter):
self.data_allowed = not srs_reject_spoofed
# non DSN mail to SRS address will bounce due to invalid local part
self.recipients.append('@'.join(t))
canon_to = '@'.join(t)
self.recipients.append(canon_to)
users = check_user.get(domain)
if self.discard:
self.del_recipient(to)
@@ -1044,6 +1198,7 @@ class bmsMilter(Milter.Milter):
if not domain in dspam_reject:
self.reject_spam = False
self.smart_alias(to)
# get recipient after virtusertable aliasing
#rcpt = self.getsymval("{rcpt_addr}")
#self.log("rcpt-addr",rcpt);
return Milter.CONTINUE
@@ -1115,7 +1270,7 @@ class bmsMilter(Milter.Milter):
self.log('REJECT: bounce with no SRS encoding')
self.setreply('550','5.7.1',
"I did not send you that message. Please consider implementing SPF",
"(http://openspf.com) to avoid bouncing mail to spoofed senders.",
"(http://openspf.org) to avoid bouncing mail to spoofed senders.",
"Thank you."
)
return Milter.REJECT
@@ -1136,6 +1291,11 @@ class bmsMilter(Milter.Milter):
return Milter.REJECT
rc = self.check_header(name,val)
if rc != Milter.CONTINUE: return rc
elif self.whitelist_sender and lname == 'subject':
# check for AutoReplys
if val.lower().find('autoreply:') >= 0:
self.whitelist_sender = False
# log selected headers
if log_headers or lname in ('subject','x-mailer'):
self.log('%s: %s' % (name,val))
@@ -1157,18 +1317,19 @@ class bmsMilter(Milter.Milter):
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
if not self.data_allowed:
return self.forged_bounce()
for name,val in self.new_headers:
for name,val,idx in self.new_headers:
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
self.fp.write("\n") # terminate headers
# log when neither sender nor from domains matches mail from domain
if supply_sender and self.mailfrom != '<>':
if supply_sender and self.mailfrom != '<>' and not self.internal_connection:
mf_domain = self.canon_from.split('@')[-1]
self.fp.seek(0)
msg = rfc822.Message(self.fp)
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
t = parse_addr(hf)
if len(t) == 2 and t[1].lower() == mf_domain:
break
if len(t) == 2:
hd = t[1].lower()
if hd == mf_domain or mf_domain.endswith('.'+hd): break
else:
for f in msg.getallmatchingheaders('from'):
self.log(f)
@@ -1194,7 +1355,7 @@ class bmsMilter(Milter.Milter):
dspam.DSF_CHAINED|dspam.DSF_CLASSIFY)
try:
ds.process(headers)
if ds.probability > 0.93 and self.dspam:
if ds.probability > 0.93 and self.dspam and not self.whitelist:
self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability)
self.setreply('550','5.7.1','Your Message looks spammy')
return Milter.REJECT
@@ -1268,8 +1429,8 @@ class bmsMilter(Milter.Milter):
ds.headerchange = self._headerChange
modified = False
for rcpt in self.recipients:
if dspam_users.has_key(rcpt):
user = dspam_users.get(rcpt)
if dspam_users.has_key(rcpt.lower()):
user = dspam_users.get(rcpt.lower())
if user:
try:
self.fp.seek(0)
@@ -1301,7 +1462,10 @@ class bmsMilter(Milter.Milter):
self.fp = None
if len(self.recipients) > 1:
self.log("HONEYPOT:",rcpt,'SCREENED')
if self.spf:
if self.whitelist:
# don't train when recipients includes honeypot
return False
if self.spf and self.mailfrom != '<>':
# check that sender accepts quarantine DSN
msg = mime.message_from_file(StringIO.StringIO(txt))
rc = self.send_dsn(self.spf,msg,'quarantine.txt')
@@ -1315,6 +1479,12 @@ class bmsMilter(Milter.Milter):
force_result=dspam.DSR_ISSPAM)
self.log("HONEYPOT:",rcpt)
return Milter.DISCARD
if self.whitelist:
# Sender whitelisted: tag, but force as ham.
# User can change if actually spam.
txt = ds.check_spam(user,txt,self.recipients,
force_result=dspam.DSR_ISINNOCENT)
else:
txt = ds.check_spam(user,txt,self.recipients)
if not txt:
# DISCARD if quarrantined for any recipient. It
@@ -1339,6 +1509,13 @@ class bmsMilter(Milter.Milter):
screener = dspam_screener[self.id % len(dspam_screener)]
if not ds.check_spam(screener,txt,self.recipients,
classify=True,quarantine=False):
if self.whitelist:
# messages is whitelisted but looked like spam, Train on Error
self.log("TRAIN:",screener,'X-Dspam-Score: %f' % ds.probability)
# user can't correct anyway if really spam, so discard tag
ds.check_spam(screener,txt,self.recipients,
force_result=dspam.DSR_ISINNOCENT)
return False
if self.reject_spam:
self.log("DSPAM:",screener,
'REJECT: X-DSpam-Score: %f' % ds.probability)
@@ -1346,7 +1523,7 @@ class bmsMilter(Milter.Milter):
self.fp = None
return Milter.REJECT
self.log("DSPAM:",screener,"SCREENED")
if self.spf:
if self.spf and self.mailfrom != '<>':
# check that sender accepts quarantine DSN
self.fp.seek(0)
msg = mime.message_from_file(self.fp)
@@ -1369,6 +1546,8 @@ class bmsMilter(Milter.Milter):
# analyze external mail for spam
spam_checked = self.check_spam() # tag or quarantine for spam
if not self.fp:
if gossip and self.umis:
gossip_node.feedback(self.umis,1)
return spam_checked
# analyze all mail for dangerous attachments and scripts
@@ -1431,8 +1610,25 @@ class bmsMilter(Milter.Milter):
self.addrcpt(self.mailfrom)
else:
self.alter_recipients(self.discard_list,self.redirect_list)
for name,val in self.new_headers:
self.addheader(name,val)
# auto whitelist original recipients
if not defanged and self.whitelist_sender:
for canon_to in self.recipients:
user,domain = canon_to.split('@')
if internal_domains:
for pat in internal_domains:
if fnmatchcase(domain,pat): break
else:
auto_whitelist[canon_to] = None
self.log('Auto-Whitelist:',canon_to)
else:
auto_whitelist[canon_to] = None
self.log('Auto-Whitelist:',canon_to)
for name,val,idx in self.new_headers:
try:
self.addheader(name,val,idx)
except:
self.addheader(name,val) # older sendmail can't insheader
if self.cbv_needed:
q = self.cbv_needed
@@ -1474,7 +1670,10 @@ class bmsMilter(Milter.Milter):
buf = out.read(8192)
if len(buf) == 0: break
self.replacebody(buf) # feed modified message to sendmail
if spam_checked: self.log("dspam")
if spam_checked:
if gossip and self.umis:
gossip_node.feedback(self.umis,0)
self.log("dspam")
return rc
finally:
out.close()
@@ -1507,9 +1706,6 @@ class bmsMilter(Milter.Milter):
self.setreply('550','5.7.1',*desc.splitlines())
return Milter.REJECT
cbv_cache[sender] = res
if not cached:
s = time.strftime(time_format,time.localtime())
print >>open('send_dsn.log','a'),sender,s # log who we sent DSNs to
return Milter.CONTINUE
def close(self):
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+173
View File
@@ -0,0 +1,173 @@
Title: Recent Changes
<h2> Recent Changes </h2>
Python milter has been moved to
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
project</a> for development and release downloads.
<h3> 0.8.5 </h3>
Release 0.8.5 fixes some build bugs reported by Stephen Figgins. It
fixes many small things, like not auto-whitelisting recipients of
outgoing mail when the subject contains "autoreply:". There is a
simple trusted forwarder implementation. If you have more than
2 or so forwarders, we will need a way to "compile" SPF records into an
IP set and TTL for it to be efficient (like libspf2 does).
<h3> GOSSiP </h3>
An alpha release of <a href="pygossip.html">pygossip</a> has been commited to
CVS, module pygossip. A version of the bms.py milter has been commited to CVS
which supports calling GOSSiP to track domain reputation in a local database.
<h3> New website design </h3>
Hey, I'm no artist, so I just used the
<a href="http://ht2html.sourceforge.net/">ht2html</a> package
by <a href="http://barry.wooz.org/">Barry Warsaw</a>. The mascot
is by <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>,
or maybe his wife. I chose Maxwell's daemon because it tirelessly
and invisibly sorts molecules, just as milters sort mail.
Christian has also provided a fun
<a href="http://alphard.ethz.ch/hafner/PPS/PPS2002/Maxwell/simulation.htm">
simulation</a> that lets you try your hand at sorting molecules.
<h3> 0.8.4 </h3>
Release 0.8.4 makes configuring SPF policy via access.db actually work.
The honeypot idea is enhanced by auto-whitelisting recipients of
email sent from selected domains. Whitelisted messages are then used
to train the honeypot. This makes the honeypot screener entirely self
training. The smfi_progress() API is now automatically supported when present.
An optional idx parameter to milter.addheader() invokes smfi_insheader().
<h3> 0.8.3 </h3>
Release 0.8.3 uses the standard logging module, and supports configuring
more detailed SPF policy via the sendmail access map. SMTP AUTH connections
are considered INTERNAL. Preventing forgery between internal domains is
just a matter of specifying the user-domain map - I'll define something
for the next version. We now send DSNs when mail is quarantined (rejecting
if DSN fails) and for SPF syntax errors (PermError). There is an
experimental option to add a Sender header when it is missing and the From
domain doesn't match the MAIL FROM domain. Next release, we may start
renaming and replacing an existing Sender header when neither it nor the
From domain matches MAIL FROM. Since bogus MAIL FROMs are rejected
(to varying degrees depending on the configured SPF policy), and
both Sender and From and displayed by default in many email clients,
this provides some phishing protection without rejecting mail based
on headers.
<h3> 0.8.2 </h3>
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
in line with the newly official RFC. It adds
<a href="http://ses.codeshare.ca/">SES</a>
support (the original SES without body hash) for pysrs-0.30.10, and honeypot
support for pydspam-1.1.9. There is a new method in the base milter module.
milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or
TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.
<h3> 0.8.0 </h3>
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
release. It supports Python-2.4, and provides an option to accept mail
that gets an SPF softfail or fails the 3 strikes rule, provided the
alleged sender accepts a DSN explaining the problem. Python-2.3 is
no longer supported by the reworked mime.py module, although API changes
could be backported. There are too many incompatible changes to the
python email package.
<h3> Older Releases </h3>
Release 0.7.2 tightens the authentication screws with a "3 strikes and
you're out" policy. A sender must have a valid PTR, HELO, or SPF record
to send email. Specific senders can be whitelisted using the
"delegate" option in the spf configuration section by adding a
default SPF record for them. The PTR and HELO are required
by RFC anyway, so this is not an unreasonable requirement.
There is now a coherent policy for an SPF softfail result. A softfail
is accepted if there is a valid PTR or HELO, or if the domain
is listed in the "accept_softfail" option of the spf configuration section.
A neutral result is accepted by default if there is a valid PTR or
HELO, (and the SPF record was not guessed), unless the domain is listed in the
"reject_neutral" option. Common forms of PTR records for dynamic IPs are
recognized, and do not count as a valid PTR. This does not prevent anyone
from sending mail from a dynamic IP - they just need to configure a
valid HELO name or publish an SPF record.
<p>
As SPF adoption continues to rise, forged spam is not getting through. So
spammers are publishing their SPF records as predicted. The 0.7.2 RPM
now provides the <code>rhsbl</code> sendmail hack so that spammer domains
can be blacklisted. With the RPM installed, add a line like the following
to your <code>sendmail.mc</code>.
<pre>
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
</pre>
<p>
Of course, spammers are now starting to register
throwaway domains. The next thing we need is a custom DNS server,
in Python, that
can recognize patterns. For instance, one spammer registers ded304.com,
ded305.com, ded306.com, etc. We also need the custom DNS server to
let SPF classic clients check SES (which will be part of pysrs).
The <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
framework provides a custom DNS server - but I
would like a smaller implementation for our use.
<p>
The RPM for release 0.7.0 moves the config file and socket locations to
/etc/mail and /var/run/milter respectively. We now parse Microsoft CID records
- but only hotmail.com uses them. They seem to have applied for a patent on
the brilliant idea of examining the mail headers to see who the message is
from. We aren't doing that here, so not to worry - but I am not a lawyer, so
if you are worried, change spf.py around line 626 to return None instead of
calling CIDParser(). There is a new option to reject mail with no PTR
and no SPF.
<p>
Microsoft is pushing an anti-opensource license for their pending patent
along with their sender-ID proposal before the IETF.
It is royalty free - but requires anyone distributing a binary they've
compiled from source to sign a license agreement. The Apache Software
Foundation <a
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
the problem with sender-ID</a>, and Debian <a
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
software in general</a> and the <a
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
particular</a>, Python milter will not be able to implement sender-ID in its
current form. This was, no doubt, Microsoft's intent all along.
<p>
Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers.
Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I
recommend ignoring it and continuing to implement and improve SPF until a
working and unencumbered proposal for RFC2822 headers surfaces.
<p>
<a href="http://openspf.com">
<img src="SPF.gif" align=left alt="SPF logo"></a>
Release 0.6.6 adds support for <a href="http://openspf.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>
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
<p>
Release 0.6.1 adds a full milter based dspam application.
<p>
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.0 offers a simple application of dspam I call "header triage",
which rejects messages with spammy headers.
To use header triage, you must have <a href="dspam.html">DSPAM</a> installed,
and select a dictionary that is well moderated by someone who gets
lots of spam. That dictionary can be used to block spam that is
obvious from the headers (e.g. X-Mailer and Subject) before it ties
up any more resources. I have yet to see any false positives from this
approach (check the milter log), but if there are, the sender will
get a REJECT with the message "Your message looks spammy."
+55
View File
@@ -0,0 +1,55 @@
Title: Credits
<h1> CREDITS </h1>
<a href="mailto:Jim Niemira <urmane@urmane.org>">Jim Niemira</a>
wrote the original C module and some quick
and dirty python to use it.
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">Stuart D. Gathman</a>
took that kludge and added threading and context objects to it, wrote a proper
OO wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a
real, usable Python extension.
<h2>Other contributors (in random order):</h2>
<dl>
<dt> <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>
<dd>for the pymilter mascot image of
<a href="http://maxwelld.netfirms.com/">
Maxwell's daemon</a>
<dt>Stephen Figgins
<dd>for reporting problems building with sendmail-8.12, and when
building milter.so for the first time.
<dt>Dave MacQuigg
<dd>for noticing that smfi_insheader wasn't supported, and creating
a template to help first time pymilter users create their own milter.
<dt>Terence Way
<dd>for providing a Python port of SPF
<dt>Scott Kitterman
<dd>for doing lots of testing and debugging of SPF against draft standard,
and for putting up a <a href="http://www.kitterman.com/spf/validate.html">
web page that validates SPF</a> records using spf.py
<dt>Alexander Kourakos
<dd>for plugging several memory leaks
<dt>George Graf at Vienna University of Economics and Business Administration
<dd>for handling None passed to setreply and chgheader.
<dt>Deron Meranda
<dd>for IPv6 patches
<dt>Jason Erikson
<dd>for handling NULL hostaddr in connect callback.
<dt>John Draper
<dd>for porting Python milter to OpenBSD, and starting to work on tutorials
then pointing out that it would be easier to just write the MTA in Python.
<dt>Eric S. Johansson
<dd>for helpful design discussions while working on camram
<dt>Alex Savguira
<dd>for finding bugs with international headers and
suggesting the scan_zip option.
<dt><a href="http://www.bmsi.com">Business Management Systems</a>
<dd>for hosting the website, and providing paying clients who need milter
service so I can work on it as part of my day job.
</dl>
If I have left anybody out, send me a reminder:
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">stuart@bmsi.com</a>
+14 -14
View File
@@ -1,8 +1,4 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Python Milter FAQ</title>
</head><body>
Title: Python Milter FAQ
<h1> Python Milter <a name=faq>FAQ</a> </h1>
@@ -22,7 +18,7 @@ shows you how to install libmilter with a separate invocation of make.
<li> Q. Why is mfapi.h not found when I try to compile Python milter on
RedHat 7.2?
<p> A. RedHat forgot to include the header in the RPM. See the
<a href="milter.html#rh72">RedHat 7.2 requirements</a>.
<a href="requirements.html#rh72">RedHat 7.2 requirements</a>.
<p>
<h3> Running Python Milter </h3>
@@ -103,13 +99,14 @@ If you are running bms.py, then the block_chinese option in
<li> Q. Why does sendmail coredump with milters on OpenBSD?
<p> A. Sendmail has a problem with unix sockets on old versions of OpenBSD.
Use an internet domain socket instead. For example, in
OpenBSD users report that this problem has been fixed, so upgrading
OpenBSD will fix this. Otherwise, you can
use an internet domain socket instead. For example, in
<code>sendmail.cf</code> use
<pre>
Xpythonfilter, S=inet:1234@localhost
</pre>
and change sample.py accordingly.
<p> OpenBSD users report that this problem has been fixed.
<p>
<li> Q. How can I change the bounce message for an invalid recipient?
@@ -143,10 +140,15 @@ Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
</pre>
<li> Q. There is a Python traceback in the log file! What happened to
my email?
<p> A. When the milter fails with an untrapped exception, a TEMPFAIL
result (451) is returned to the sender. The sender will then retry every
hour or so for several days. Hopefully, someone will notice the
traceback, and workaround or fix the problem.
<p> A. By default, when the milter fails with an untrapped exception, a
TEMPFAIL result (451) is returned to the sender. The sender will then retry
every hour or so for several days. Hopefully, someone will notice the
traceback, and workaround or fix the problem. Beginning with milter-0.8.2,
you can call <code>milter.set_exception_policy(milter.CONTINUE)</code>
to cause an untrapped exception to continue processing with the
next callback or milter instead. For
completeness, you can also set the exception policy to
<code>milter.REJECT</code>.
<li> Q. I read some notes such as "Check valid domains allowed by internal
senders to detect PCs infected with spam trojans." but could not
@@ -194,5 +196,3 @@ everything up for you. For other systems:
</ol>
</ol>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
Title: Python Milter Log Documentation
<style>
DT { font-weight: bolder; padding-top: 1em }
</style>
<h1> Milter Log Documentation </h1>
The milter log from the bms.py application has a variety of "tags" in it that
indicate what it did.
<dl>
<dt> DSPAM: honeypot SCREENED
<dd> message was quarantined to the honeypot quarantine
<dt> REJECT: hello SPF: fail 550 access denied
<dt> REJECT: hello SPF: softfail 550 domain in transition
<dt> REJECT: hello SPF: neutral 550 access neither permitted nor denied
<dd> message was rejected because there was an SPF policy for the
HELO name, and it did not pass.
<dt> CBV: sender-17-44662668-643@bluepenmagic.com
<dd> we performed a call back verification
<dt> dspam
<dd> dspam identifier was added to the message
<dt> REJECT: spam from self: jsconnor.com
<dd> message was reject because HELO was us (jsconnor.com)
<dt> INNOC: richh
<dd> message was used to update richh's dspam dictionary
<dt> HONEYPOT: michaelb@jsconnor.com
<dd> message was sent to a honeypot address (michaelb@jsconnor.com), the
message was added to the honeypot dspam dictionary as spam
<dt> REJECT: numeric hello name: 63.217.19.146
<dd> message was rejected because helo name was invalid (numeric)
<dt> eom
<dd> message was successfully received
<dt> TEMPFAIL: CBV: 450 No MX servers available
<dd> we tried to do a call back verification but could not look up
MX record, we told the sender to try again later
<dt> CBV: info@emailpizzahut.com (cached)
<dd> call back verification was needed, we had already done it recently
<dt> abort after 0 body chars
<dd> sender hung up on us
<dt> REJECT: SPF fail 550 SPF fail: see
http://openspf.com/why.html?sender=m.hendersonxk@163.net&ip=213.47.161.100
<dd> message was reject because its sender's spf policy said to
<dt> REJECT: Subject: Cialis - No prescription needed!
<dd> message was rejected because its subject contained a bad expression
<dt> DSPAM: tonyc tonyc@jsconnor.com
<dd> message was sent to tonyc@jsconnor.com and it was identified as spam
and placed in the tonyc dspam quarantine
<dt> REJECT: CBV: 550 calvinalstonis@ix.netcom.com...User unknown
<dt> REJECT: CBV: 553 sorry, that domain isn't in my list
<dt> REJECT: CBV: 554 delivery error: dd This user doesn't have an account
<dd> message was rejected because call back verification gave us a fatal
error
</dl>
Please add more tags to this list if you know of any. Thanks.
+56 -259
View File
@@ -1,33 +1,29 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Python Milters</title>
</head><body>
Title: Python Milters
<P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/">
<IMG SRC="/art/brain1.gif"
<IMG SRC="http://bmsi.com/art/brain1.gif"
ALT="Viewable With Any Browser" BORDER="0"></A>
<img src="/art/banner_4.gif" width="468" height="60" border="0"
<img src="http://bmsi.com/art/banner_4.gif" width="468" height="60" border="0"
usemap="#banner_4" alt="Your vote?">
<map name="banner_4">
<area shape="rect" coords="330,25,426,59"
href="http://education-survey.org/" alt="I Disagree">
<area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree">
</map>
</P>
<img src="Maxwells.gif" alt="Maxwell's Daemon: pymilter mascot" align=left>
<h1 align=center>Sendmail Milters in Python</h1>
<h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a>
and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
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 Aug 28, 2005</h4>
Last updated Dec 29, 2005</h4>
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
<a href="http://bmsi.com/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
<a href="#overview">Overview</a> |
<a href="/python/dspam.html">pydspam</a> |
<a href="/libdspam/dspam.html">libdspam</a>
@@ -45,159 +41,6 @@ separation features to enhance security. Even better, sendmail 8.13
supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more
efficient and secure. I recommend upgrading.
<h2> Recent Changes </h2>
Python milter is being moved to
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
project</a> for development and release downloads.
<p>
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
in line with the newly official RFC. It adds
<a href="http://ses.codeshare.ca/">SES</a>
support (the original SES without body hash) for pysrs-0.30.10, and honeypot
support for pydspam-1.1.9. There is a new method in the base milter module.
milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or
TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.
<p>
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
release. It supports Python-2.4, and provides an option to accept mail
that gets an SPF softfail or fails the 3 strikes rule, provided the
alleged sender accepts a DSN explaining the problem. Python-2.3 is
no longer supported by the reworked mime.py module, although API changes
could be backported. There are too many incompatible changes to the
python email package.
<p>
Release 0.7.2 tightens the authentication screws with a "3 strikes and
you're out" policy. A sender must have a valid PTR, HELO, or SPF record
to send email. Specific senders can be whitelisted using the
"delegate" option in the spf configuration section by adding a
default SPF record for them. The PTR and HELO are required
by RFC anyway, so this is not an unreasonable requirement.
There is now a coherent policy for an SPF softfail result. A softfail
is accepted if there is a valid PTR or HELO, or if the domain
is listed in the "accept_softfail" option of the spf configuration section.
A neutral result is accepted by default if there is a valid PTR or
HELO, (and the SPF record was not guessed), unless the domain is listed in the
"reject_neutral" option. Common forms of PTR records for dynamic IPs are
recognized, and do not count as a valid PTR. This does not prevent anyone
from sending mail from a dynamic IP - they just need to configure a
valid HELO name or publish an SPF record.
<p>
As SPF adoption continues to rise, forged spam is not getting through. So
spammers are publishing their SPF records as predicted. The 0.7.2 RPM
now provides the <code>rhsbl</code> sendmail hack so that spammer domains
can be blacklisted. With the RPM installed, add a line like the following
to your <code>sendmail.mc</code>.
<pre>
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
</pre>
<p>
Of course, spammers are now starting to register
throwaway domains. The next thing we need is a custom DNS server,
in Python, that
can recognize patterns. For instance, one spammer registers ded304.com,
ded305.com, ded306.com, etc. We also need the custom DNS server to
let SPF classic clients check SES (which will be part of pysrs).
The <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
framework provides a custom DNS server - but I
would like a smaller implementation for our use.
<p>
The RPM for release 0.7.0 moves the config file and socket locations to
/etc/mail and /var/run/milter respectively. We now parse Microsoft CID records
- but only hotmail.com uses them. They seem to have applied for a patent on
the brilliant idea of examining the mail headers to see who the message is
from. We aren't doing that here, so not to worry - but I am not a lawyer, so
if you are worried, change spf.py around line 626 to return None instead of
calling CIDParser(). There is a new option to reject mail with no PTR
and no SPF.
<p>
Microsoft is pushing an anti-opensource license for their pending patent
along with their sender-ID proposal before the IETF.
It is royalty free - but requires anyone distributing a binary they've
compiled from source to sign a license agreement. The Apache Software
Foundation <a
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
the problem with sender-ID</a>, and Debian <a
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
software in general</a> and the <a
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
particular</a>, Python milter will not be able to implement sender-ID in its
current form. This was, no doubt, Microsoft's intent all along.
<p>
Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers.
Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I
recommend ignoring it and continuing to implement and improve SPF until a
working and unencumbered proposal for RFC2822 headers surfaces.
<p>
<a href="http://openspf.com">
<img src="SPF.gif" align=left alt="SPF logo"></a>
Release 0.6.6 adds support for <a href="http://openspf.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>
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
<p>
Release 0.6.1 adds a full milter based dspam application.
<p>
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.0 offers a simple application of dspam I call "header triage",
which rejects messages with spammy headers.
To use header triage, you must have <a href="dspam.html">DSPAM</a> installed,
and select a dictionary that is well moderated by someone who gets
lots of spam. That dictionary can be used to block spam that is
obvious from the headers (e.g. X-Mailer and Subject) before it ties
up any more resources. I have yet to see any false positives from this
approach (check the milter log), but if there are, the sender will
get a REJECT with the message "Your message looks spammy."
<h2> Enough Already! </h2>
Nearly a dozen people have emailed me begging for a feature to copy
outgoing and/or incoming mail to a backup directory by user. Ok, it
looks like this is a most requested feature for 0.5.6. In the meantime,
here are some things to consider:
<ul>
<li> If you want to equivalent of a Bcc added to each message, this
is very easy to do in the python code for bms.py. See below.
<li> If you want to copy to a file in a directory (thus avoiding having to
set up aliases), this is slightly more involved. The bms.py milter already
copies the message to a temporary file for use in replacing the message body
when banned attachments are found. You have to open a file, and copy the
Mesage object to it in eom().
<li> Finally, you are probably aware that most email clients already
keep a copy of outgoing mail? Presumably there is a good reason for
keeping another copy on the server.
</ul>
<p>
To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after
determining whether you want to copy (e.g. whether the sender is local). For
example,
<pre>
def envfrom(...
...
if len(t) == 2:
self.rejectvirus = t[1] in reject_virus_from
if t[0] in wiretap_users.get(t[1],()):
self.add_recipient(wiretap_dest)
if t[1] == 'mydomain.com':
self.add_recipient('&lt;copy-%s&gt;' % t[0])
...
</pre>
<p>
To make this a generic feature requires thinking about how the configuration
would look. Feel free to make specific suggestions about config file
entries. Be sure to handle both Bcc and file copies, and designating what
mail should be copied. How should "outgoing" be defined? Implementing it is
easy once the configuration is designed.
<h3><a name=overview>Overview</a></h3>
This package provides a robust toolkit for Python <a
@@ -245,6 +88,8 @@ content filtering. SPF checking
requires <a href="http://pydns.sourceforge.net/">
pydns</a>. Configuration documentation is currently included as comments
in the <a href="milter.cfg">sample config file</a> for the bms.py milter.
See also the <a href="HOWTO">HOWTO</a> and <a href="logmsgs.html">
Milter Log Message Tags</a>.
<p>
Python milter is under GPL. The authors can probably be convinced to
change this to LGPL if needed.
@@ -376,8 +221,14 @@ me if you successfully install milter on a system not mentioned below.
<td>0.5.5</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.3.3</td><td>8.13.1</td>
<td>0.7.2</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.4.1</td><td>8.13.5</td>
<td>0.8.4</td><tr>
<td>RedHat 8.0</td><td>gcc-3.2</td><td>2.2.1</td><td>8.12.6</td>
<td>0.5.2</td><tr>
<td>RedHat 9.0</td><td>gcc-3.2.2</td><td>2.4.1</td><td>8.13.1</td>
<td>0.8.2</td><tr>
<td>RedHat EL3</td><td>gcc-3.2.3</td><td>2.4.1</td><td>8.13.5</td>
<td>0.8.4</td><tr>
<td>Debian Linux</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.0</td>
<td>0.3.7</td><tr>
<td>Debian Linux</td><td>gcc-3.2.2</td><td>2.2.2</td><td>8.12.7</td>
@@ -388,8 +239,8 @@ me if you successfully install milter on a system not mentioned below.
<td>0.3.4</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.3</td><td>8.12.3</td>
<td>0.4.2</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.2.3</td><td>8.13.1</td>
<td>0.7.1</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.4.1</td><td>8.13.1</td>
<td>0.8.4</td><tr>
<td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td>
<td>0.3.8</td><tr>
<td>Slackware 9.0</td><td>gcc-3.2.2</td><td>2.2.3</td><td>8.12.9</td>
@@ -403,108 +254,54 @@ me if you successfully install milter on a system not mentioned below.
<td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.2</td><td>?</td>
<td>0.5.5</td><tr>
<td>FreeBSD 4.4</td><td>gcc-2.95.3</td><td>?</td><td>8.12.10</td>
<td>0.6.6</td><tr>
<td>0.6.6</td>
</table>
<h3> Requirements </h3>
<h2> Enough Already! </h2>
<menu>
<li> While the miltermodule will work with python 1.5, you probably
want to use python 2.0 or better. The python code uses a number of
python 2 features.
<li> Python must be configured with thread support. This is because
sendmail's libmilter requires thread support.
<li> You must compile sendmail with libmilter enabled. In versions of
sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and
is not installed by default.
Sendmail 8.12 still does not enable libmilter by default. You must
explicitly select the "MILTER" option when compiling.
<li> Python milter has been tested against sendmail-8.11 and sendmail-8.12.
<li> Python milter must be compiled for the specific version of sendmail
it will run with. (Since the result is dynamically loaded, there could
conceivably be multiple versions available and selected at startup - but
that will have to wait.) This situation may only exist for sendmail
versions prior to 8.12. The protocol seems designed for backward
compatibility - and 8.12 is the first official milter release.
<li> Mea Culpa! After reading the Python Style guide, I realize that
my Python code is not up to snuff. Apparently mixed tabs and spaces
are anathema to those using Windows editors, where tabs can be expanded using
any arbitrary algorithm. Other than that, my
intuition matched Guido's pretty well - although I like to indent by 2
rather than 4. I will arrange to have tabs expanded to spaces when
exporting new versions. Until then, beware!
</menu>
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
to represent the stuff that comes with AIX. You might also want
my <a href="/aix/python.spec">python-2.1.1</a> spec file for AIX. It
does not include Tk or curses modules, sorry. If y'all trust me, you can
download rpms for AIX 4.x from my <a href="/aix">AIX RPM directory</a>.
Nearly a dozen people have emailed me begging for a feature to copy
outgoing and/or incoming mail to a backup directory by user. Ok, it
looks like this is a most requested feature for 0.5.6. In the meantime,
here are some things to consider:
<ul>
<li> If you want to equivalent of a Bcc added to each message, this
is very easy to do in the python code for bms.py. See below.
<li> If you want to copy to a file in a directory (thus avoiding having to
set up aliases), this is slightly more involved. The bms.py milter already
copies the message to a temporary file for use in replacing the message body
when banned attachments are found. You have to open a file, and copy the
Mesage object to it in eom().
<li> Finally, you are probably aware that most email clients already
keep a copy of outgoing mail? Presumably there is a good reason for
keeping another copy on the server.
</ul>
<p>
Sendmail-8.12 renames
libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system
shared library. Therefore, I rename libsm.a back to libsmutil.a for
AIX. This presents a problem for setup.py.
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
If you are running Redhat 7.2, the distributed version of sendmail
now enables libmilter by default. RedHat 7.2 bundles
the development libraries with the main sendmail package, so
there is no sendmail-devel package. However, they forgot to include the
headers! So you'll have to get the SRPM and modify it. I suggest
moving the static libs to a devel package and adding the headers. If
this is too much trouble, you can get the <a href="mfapi.h">mfapi.h</a>
header for sendmail-8.6.11 from here and manually install it as
<code>/usr/include/libmilter/mfapi.h</code>.
To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after
determining whether you want to copy (e.g. whether the sender is local). For
example,
<pre>
def envfrom(...
...
if len(t) == 2:
self.rejectvirus = t[1] in reject_virus_from
if t[0] in wiretap_users.get(t[1],()):
self.add_recipient(wiretap_dest)
if t[1] == 'mydomain.com':
self.add_recipient('&lt;copy-%s&gt;' % t[0])
...
</pre>
<p>
If you do modify the SRPM, I suggest renaming libsmutil.a
to libsm.a - just like sendmail-8.12 will. If you manually install
mfapi.h or don't rename libsmutil.a, you'll
need to force <code>libs = ["milter", "smutil"]</code> in setup.py.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
To make this a generic feature requires thinking about how the configuration
would look. Feel free to make specific suggestions about config file
entries. Be sure to handle both Bcc and file copies, and designating what
mail should be copied. How should "outgoing" be defined? Implementing it is
easy once the configuration is designed.
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
If you are running Redhat 6.2, the distributed version of sendmail
does not enable libmilter. You can download the Redhat 7.2 sendmail.spec
modified to compile on RedHat 6.2:
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
sendmail-rhmilter.spec</a>. The <a
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
SRPM for sendmail-8.11.6</a> is available from
<a href="http://www.redhat.com">Redhat</a> under
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
Errata for RH6.2</a>. But that doesn't include the latest security
patches since RH6.2 is no longer supported.
<p>
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> directory.
The lastest RPMs were built by taking a RH7.2 SRPMS and removing some
RPM features from the spec file that RH6.2 doesn't support, then
recompiling on RH6.2. You can check this by installing the RH7.2 SRPM,
then diffing my sendmail.spec with theirs. Then run
"rpm -bb sendmail-rhmilter.spec" when you are satisfied.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
You'll need to install the sendmail-devel package to compile milter.
<hr>
<p>
<a href="http://validator.w3.org/check/referer">
<img border=0 src="/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
<img border=0 src="http://bmsi.com/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
<a href="http://www.redhat.com">
<img src="/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
<img src="http://bmsi.com/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
</p>
</body></html>
+9 -9
View File
@@ -1,11 +1,12 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Python Milter Mail Policy </title>
</head><body>
Title: Python Milter Mail Policy
<h1> Python Milter Mail Policy </h1>
These are the policies implemented by the <code>bms.py</code> milter
application. The milter and Milter modules do not implement any policies
by themselves. Eventually, I'll get the bms.py milter moved to its
own package.
<h3> Classify connection </h3>
When the SMTP client connects, the connection IP address is
@@ -72,7 +73,9 @@ The wiretap feature can screen and/or monitor mail to/from certain
users. If the MAIL FROM is being wiretapped, the recipients are
altered accordingly.
<h4> SPF check </h4>
<!--table-stop-->
<h2> SPF check </h2>
Finally, the MAIL FROM, connect IP, and HELO name are checked against
any SPF records published via DNS for the alleged sender (MAIL FROM).
@@ -232,6 +235,3 @@ should retry the message at a later time.
</td></tr>
</table>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+99
View File
@@ -0,0 +1,99 @@
Title: Requirements
<h2> Requirements </h2>
<menu>
<li> While the miltermodule will work with python 1.5, you probably
want to use python 2.0 or better. The python code uses a number of
python 2 features. The email support requires python 2.4.
<li> Python must be configured with thread support. This is because
pymilter uses sendmail's libmilter which requires thread support.
<li> You must compile sendmail with libmilter enabled. In versions of
sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and
is not installed by default.
Sendmail 8.12 still does not enable libmilter by default. You must
explicitly select the "MILTER" option when compiling.
<li> When compiling Python milter against sendmail versions earlier than
8.13, you must set MAX_ML_REPLY to 1 in setup.py. There is no way to tell from
the libmilter includes that smfi_setmlreply is not supported.
<li> You probably want to use sendmail-8.13, since that supports multi-line
SMTP error descriptions and SOCKETMAP. You want SOCKETMAP for use with
pysrs.
<li> Python milter has been tested against sendmail-8.11 through sendmail-8.13.
<li> Python milter must be compiled for the specific version of sendmail
it will run with. (Since the result is dynamically loaded, there could
conceivably be multiple versions available and selected at startup - but
that will have to wait.) This situation may only exist for sendmail
versions prior to 8.12. The protocol seems designed for backward
compatibility - and 8.12 is the first official milter release.
<li> Mea Culpa! After reading the Python Style guide, I realize that
my Python code is not up to snuff. Apparently mixed tabs and spaces
are anathema to those using Windows editors, where tabs can be expanded using
any arbitrary algorithm. Other than that, my
intuition matched Guido's pretty well - although I like to indent by 2
rather than 4. I will arrange to have tabs expanded to spaces when
exporting new versions. Until then, beware!
</menu>
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
to represent the stuff that comes with AIX. You might also want
my <a href="/aix/python.spec">python-2.1.1</a> spec file for AIX. It
does not include Tk or curses modules, sorry. If y'all trust me, you can
download rpms for AIX 4.x from my <a href="/aix">AIX RPM directory</a>.
<p>
Sendmail-8.12 renames
libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system
shared library. Therefore, I rename libsm.a back to libsmutil.a for
AIX. This presents a problem for setup.py.
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
If you are running Redhat 7.2, the distributed version of sendmail
now enables libmilter by default. RedHat 7.2 bundles
the development libraries with the main sendmail package, so
there is no sendmail-devel package. However, they forgot to include the
headers! So you'll have to get the SRPM and modify it. I suggest
moving the static libs to a devel package and adding the headers. If
this is too much trouble, you can get the <a href="mfapi.h">mfapi.h</a>
header for sendmail-8.6.11 from here and manually install it as
<code>/usr/include/libmilter/mfapi.h</code>.
<p>
If you do modify the SRPM, I suggest renaming libsmutil.a
to libsm.a - just like sendmail-8.12 will. If you manually install
mfapi.h or don't rename libsmutil.a, you'll
need to force <code>libs = ["milter", "smutil"]</code> in setup.py.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
If you are running Redhat 6.2, the distributed version of sendmail
does not enable libmilter. You can download the Redhat 7.2 sendmail.spec
modified to compile on RedHat 6.2:
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
sendmail-rhmilter.spec</a>. The <a
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
SRPM for sendmail-8.11.6</a> is available from
<a href="http://www.redhat.com">Redhat</a> under
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
Errata for RH6.2</a>. But that doesn't include the latest security
patches since RH6.2 is no longer supported.
<p>
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> directory.
The lastest RPMs were built by taking a RH7.2 SRPMS and removing some
RPM features from the spec file that RH6.2 doesn't support, then
recompiling on RH6.2. You can check this by installing the RH7.2 SRPM,
then diffing my sendmail.spec with theirs. Then run
"rpm -bb sendmail-rhmilter.spec" when you are satisfied.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
You'll need to install the sendmail-devel package to compile milter.
+14 -3
View File
@@ -8,7 +8,7 @@ tempdir = /var/log/milter/save
log_headers = 0
# connection ips and hostnames are matched against this glob style list
# to recognize internal senders.
;internal_connect = 192.168.*.*
;internal_connect = 192.168.*.*,127.*
# mail that is not an internal_connect and claims to be from an
# internal domain is rejected. Furthermore, internal mail that
@@ -17,7 +17,7 @@ log_headers = 0
# flexible. However, SPF is not currently checked for outgoing
# (internal_connect) mail because it doesn't yet handle authorizing
# internal IPs locally.
;internal_domains = mycorp.com
;internal_domains = mycorp.com,localhost.localdomain
# connections from a trusted relay can trust the first Received header
# SPF checks are bypassed for internal connections and trusted relays.
@@ -99,6 +99,11 @@ reject_spoofed = 0
# doesn't match MAIL FROM. Outlook and other email clients will then display
# something like: "Sent by sender@domain.com on behalf of from@example.com"
;supply_sender = 0
# Connections that get an SPF pass for a pretend MAIL FROM of
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
# This is for non-SRS forwarders. It is a simple implementation that
# is inefficient for more than a few entries.
;trusted_forwarder = careerbuilder.com
# features intended to clean up outgoing mail
[scrub]
@@ -146,7 +151,13 @@ blind = 1
# only EXTERNAL messages are dspam filtered
;dspam_dict=/var/lib/dspam/moderator.dict
# Opt-opt recipients from dspam screening and header triage
# Recipients of mail sent from these senders are added to the auto_whitelist.
# Auto_whitelisted senders with an SPF PASS are never rejected by dspam, and
# messages from auto_whitelisted senders will be used to train screener
# dictionaries as innocent mail.
;whitelist_senders = @mycorp.com
# Opt-out recipients entirely from dspam screening and header triage
;dspam_exempt=getitall@mycorp.com
# Do not scan mail (ostensibly) from these senders
;dspam_whitelist=getitall@sender.com
+1 -1
View File
@@ -5,7 +5,7 @@
# chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail.
# processname: milter
# config: /var/log/milter/bms.py
# config: /etc/mail/pymilter.cfg
# pidfile: /var/run/milter/milter.pid
python="python2.3"
+1 -1
View File
@@ -5,7 +5,7 @@
# chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail.
# processname: milter
# config: /var/log/milter/bms.py
# config: /etc/mail/pymilter.cfg
# pidfile: /var/run/milter/milter.pid
python="python2.3"
+30 -6
View File
@@ -1,5 +1,5 @@
%define name milter
%define version 0.8.3
%define version 0.8.5
%define release 1.RH7
# what version of RH are we building for?
%define redhat9 0
@@ -39,11 +39,11 @@ Prefix: %{_prefix}
Vendor: Stuart D. Gathman <stuart@bmsi.com>
Packager: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html
Requires: %{python} >= 2.4, sendmail >= 8.12.10
Requires: %{python} >= 2.4, sendmail >= 8.13
%ifos Linux
Requires: chkconfig
%endif
BuildRequires: %{python}-devel , sendmail-devel >= 8.12.10
BuildRequires: %{python}-devel , sendmail-devel >= 8.13
%description
This is a python extension module to enable python scripts to
@@ -52,10 +52,15 @@ modules provide for navigating and modifying MIME parts.
%prep
%setup
#%patch -p1
#%patch -p0 -b .bms
%build
env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build
if %{redhat9}; then
LDFLAGS="-g"
else
LDFLAGS="-s"
fi
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
@@ -169,7 +174,26 @@ rm -rf $RPM_BUILD_ROOT
/usr/share/sendmail-cf/hack/rhsbl.m4
%changelog
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-1
* Thu Dec 29 2005 Stuart Gathman <stuart@bmsi.com> 0.8.5-1
- Simple trusted_forwarder implementation.
- Fix access_file neutral policy
- Move Received-SPF header to beginning of headers
- Supply keyword info for all results in Received-SPF header.
- Move guessed SPF result to separate header
- Activate smfi_insheader only when SMFIR_INSHEADER defined
- Handle NULL MX in spf.py
- in-process GOSSiP server support (to be extended later)
- Expire CBV cache and renew auto-whitelist entries
* Fri Oct 21 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-2
- Don't supply sender when MFROM is subdomain of header from/sender.
- Don't send quarantine DSN for DSNs
- Skip dspam for replies/DSNs to signed MFROM
* Thu Oct 20 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-1
- Fix SPF policy via sendmail access map (case insensitive keys).
- Auto whitelist senders, train screener on whitelisted messages
- Optional idx parameter to addheader to invoke smfi_insheader
- Activate progress when SMFIR_PROGRESS defined
* Wed Oct 12 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-1
- Keep screened honeypot mail, but optionally discard honeypot only mail.
- spf_accept_fail option for braindead SPF senders (treats fail like softfail)
- Consider SMTP AUTH connections internal.
+25 -5
View File
@@ -34,6 +34,15 @@ $ python setup.py help
libraries=["milter","smutil","resolv"]
* $Log$
* Revision 1.8 2005/10/20 23:23:36 customdesigned
* Include smfi_progress is SMFIR_PROGRESS defined
*
* Revision 1.7 2005/10/20 23:04:46 customdesigned
* Add optional idx for position of added header.
*
* Revision 1.6 2005/07/15 22:18:17 customdesigned
* Support callback exception policy
*
* Revision 1.5 2005/06/24 04:20:07 customdesigned
* Report context allocation error.
*
@@ -967,28 +976,39 @@ milter_setreply(PyObject *self, PyObject *args) {
}
static char milter_addheader__doc__[] =
"addheader(field, value) -> None\n\
"addheader(field, value, idx=-1) -> None\n\
Add a header to the message. This header is not passed to other\n\
filters. It is not checked for standards compliance;\n\
the mail filter must ensure that no protocols are violated\n\
as a result of adding this header.\n\
field - header field name\n\
value - header field value\n\
idx - optional position in internal header list to insert new header\n\
Both are strings. This function can only be called from the EOM callback.";
static PyObject *
milter_addheader(PyObject *self, PyObject *args) {
char *headerf;
char *headerv;
int idx = -1;
SMFICTX *ctx;
PyThreadState *t;
if (!PyArg_ParseTuple(args, "ss:addheader", &headerf, &headerv)) return NULL;
if (!PyArg_ParseTuple(args, "ss|i:addheader", &headerf, &headerv, &idx))
return NULL;
ctx = _find_context(self);
if (ctx == NULL) return NULL;
t = PyEval_SaveThread();
return _thread_return(t,smfi_addheader(ctx, headerf, headerv),
#ifdef SMFIR_INSHEADER
return _thread_return(t, (idx < 0) ? smfi_addheader(ctx, headerf, headerv) :
smfi_insheader(ctx, idx, headerf, headerv), "cannot add header");
#else
if (idx < 0)
return _thread_return(t, smfi_addheader(ctx, headerf, headerv),
"cannot add header");
PyErr_SetString(MilterError, "insheader not supported");
return NULL;
#endif
}
static char milter_chgheader__doc__[] =
@@ -1143,7 +1163,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
}
#endif
#if _FFR_SMFI_PROGRESS
#ifdef SMFIR_PROGRESS
static char milter_progress__doc__[] =
"progress() -> None\n\
Notify the MTA that we are working on a message so it will reset timeouts.";
@@ -1174,7 +1194,7 @@ static PyMethodDef context_methods[] = {
#ifdef SMFIF_QUARANTINE
{ "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__},
#endif
#if _FFR_SMFI_PROGRESS
#ifdef SMFIR_PROGRESS
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
#endif
{ NULL, NULL }
+1 -1
View File
@@ -18,7 +18,7 @@ sent via an authorized SMTP server, but may still be legitimate. Since there
is no positive confirmation that the message is really from you, we have
to give it extra scrutiny - including verifying that the sender really
exists by sending you this DSN. We will remember this sender and not
bother you again for while. You can avoid this message entirely for
bother you again for a while. You can avoid this message entirely for
legitimate mail by using an authorized SMTP server. Contact your mail
administrator and ask how to configure your email client to use an
authorized server.
+138
View File
@@ -0,0 +1,138 @@
# Analyze milter log to find abusers
import traceback
import sys
def parse_addr(a):
beg = a.find('<')
end = a.find('>')
if beg >= 0:
if end > beg: return a[beg+1:end]
return a
class Connection(object):
def __init__(self,dt,tm,id,ip=None,conn=None):
self.dt = dt
self.tm = tm
self.id = id
if ip:
_,self.host,self.ip = ip.split(None,2)
elif conn:
self.ip = conn.ip
self.host = conn.host
self.helo = conn.helo
self.subject = None
self.rcpt = []
self.mfrom = None
self.helo = None
self.innoc = []
self.whitelist = False
def connections(fp):
conndict = {}
termdict = {}
for line in fp:
if line.startswith('{'): continue
a = line.split(None,4)
if len(a) < 4: continue
dt,tm,id,op = a[:4]
if (id,op) == ('bms','milter'):
# FIXME: optionally yield all partial connections in conndict
conndict = {}
termdict = {}
continue
if id[0] == '[' and id[-1] == ']':
try:
key = int(id[1:-1])
except:
print >>sys.stderr,'bad id:',line.rstrip()
continue
else: continue
if op == 'connect':
ip = a[4].rstrip()
conn = Connection(dt,tm,id,ip=ip)
conndict[key] = conn
elif op in (
'DISCARD:','TAG:','CBV:','Large','No',
'NOTE:','From:','Sender:','TRAIN:'):
continue
else:
op = op.lower()
try:
conn = conndict[key]
except KeyError:
try:
conn = termdict[key]
del termdict[key]
conndict[key] = conn
except KeyError:
print >>sys.stderr,'key error:',line.rstrip()
continue
try:
if op == 'subject:':
if len(a) > 4:
conn.subject = a[4].rstrip()
elif op == 'innoc:':
conn.innoc.append(a[4].rstrip())
elif op == 'whitelist':
conn.whitelist = True
elif op == 'x-mailer:':
if len(a) > 4:
conn.mailer = a[4].rstrip()
elif op == 'x-guessed-spf:':
conn.spfguess = a[4]
elif op == 'received-spf:':
conn.spfres,conn.spfmsg = a[4].rstrip().split(None,1)
elif op == 'received:':
conn.received = a[4].rstrip()
elif op == 'temp':
_,conn.tempfile = a[4].rstrip().split(None,1)
elif op == 'srs':
_,conn.srsrcpt = a[4].rstrip().split(None,1)
elif op == 'mail':
_,conn.mfrom = a[4].rstrip().split(None,1)
elif op == 'rcpt':
_,rcpt = a[4].rstrip().split(None,1)
conn.rcpt.append(rcpt)
elif op == 'hello':
_,conn.helo = a[4].rstrip().split(None,1)
elif op in ('eom','dspam','abort'):
del conndict[key]
conn.enddt = dt
conn.endtm = tm
conn.result = op
yield conn
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
elif op in ('reject:','dspam:','tempfail:','reject','fail:','honeypot:'):
del conndict[key]
conn.enddt = dt
conn.endtm = tm
conn.result = op
conn.resmsg = a[4].rstrip()
yield conn
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
elif op in ('fp:','spam:'):
del conndict[key]
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
else:
print >>sys.stderr,'unknown op:',line.rstrip()
except Exception:
print >>sys.stderr,'error:',line.rstrip()
traceback.print_exc()
if __name__ == '__main__':
import gzip
for fn in sys.argv[1:]:
if fn.endswith('.gz'):
fp = gzip.open(fn)
else:
fp = open(fn)
for conn in connections(fp):
if conn.rcpt and conn.mfrom:
for r in conn.rcpt:
if r.lower().find('iancarter') > 0: break
else:
if conn.mfrom.lower().find('iancarter') < 0: continue
print >>sys.stderr,conn.result,conn.dt,conn.tm,conn.id,conn.subject,parse_addr(conn.mfrom),
for a in conn.rcpt:
print parse_addr(a),
print
+3 -2
View File
@@ -1,7 +1,6 @@
import os
import sys
from distutils.core import setup, Extension
import Milter
# FIXME: on some versions of sendmail, smutil is renamed to sm
libs = ["milter", "smutil"]
@@ -13,7 +12,8 @@ if sys.version < '2.2.3':
DistributionMetadata.classifiers = None
DistributionMetadata.download_url = None
setup(name = "milter", version = Milter.__version__,
# NOTE: importing Milter to obtain version fails when milter.so not built
setup(name = "milter", version = '0.8.5',
description="Python interface to sendmail milter API",
long_description="""\
This is a python extension module to enable python scripts to
@@ -32,6 +32,7 @@ querying SPF records.
ext_modules=[
Extension("milter", ["miltermodule.c"],
libraries=libs,
# set MAX_ML_REPLY to 1 for sendmail < 8.13
define_macros = [ ('MAX_ML_REPLY',32) ]
),
],
+40 -14
View File
@@ -19,7 +19,7 @@ AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
For more information about SPF, a tool against email forgery, see
http://spf.pobox.com/
http://openspf.org/
For news, bugfixes, etc. visit the home page for this implementation at
http://www.wayforward.net/spf/
@@ -47,6 +47,20 @@ For news, bugfixes, etc. visit the home page for this implementation at
# Terrence is not responding to email.
#
# $Log$
# Revision 1.17 2005/12/23 21:44:15 customdesigned
# Always include keyword data in Received-SPF header.
#
# Revision 1.16 2005/12/01 22:42:32 customdesigned
# improve gossip support.
# Initialize srs_domain from srs.srs config property. Should probably
# always block unsigned DSN when signing all.
#
# Revision 1.15 2005/10/30 01:08:14 customdesigned
# Ignore records missing spaces.
#
# Revision 1.14 2005/08/12 17:36:51 customdesigned
# Trean non-existant include as no match in "lax" mode.
#
# Revision 1.13 2005/07/22 16:00:23 customdesigned
# Limit CNAME chains independently of DNS lookup limit
#
@@ -285,6 +299,10 @@ def DNSLookup(name,qtype):
except DNS.DNSError,x:
raise TempError,'DNS ' + str(x)
def isSPF(txt):
"Return True if txt has SPF record signature."
return txt.startswith('v=spf1 ') or txt == 'v=spf1'
# 32-bit IPv4 address mask
MASK = 0xFFFFFFFFL
@@ -589,8 +607,15 @@ class query(object):
return ('none', 250, EXPLANATIONS['none'])
# split string by whitespace, drop the 'v=spf1'
#
spf = spf.split()[1:]
spf = spf.split()
# Catch case where SPF record has no spaces
# Can never happen with conforming dns_spf(), however
# in the future we might want to give permerror
# for common mistakes like IN TXT "v=spf1" "mx" "-all"
# in relaxed mode.
if spf[0] != 'v=spf1':
raise PermError('Invalid SPF record in', self.d)
spf = spf[1:]
# copy of explanations to be modified by exp=
exps = self.exps
@@ -815,11 +840,11 @@ class query(object):
is found.
"""
# for performance, check for most common case of TXT first
a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')]
a = [t for t in self.dns_txt(domain) if isSPF(t)]
if len(a) == 1 and self.strict < 2:
return a[0]
# check official SPF type first when it becomes more popular
b = [t for t in self.dns_99(domain) if t.startswith('v=spf1')]
b = [t for t in self.dns_99(domain) if isSPF(t)]
if len(b) == 1:
# FIXME: really must fully parse each record
# and compare with appropriate parts case insensitive.
@@ -832,7 +857,7 @@ class query(object):
if DELEGATE: # use local record if neither found
a = [t
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
if t.startswith('v=spf1')
if isSPF(t)
]
if len(a) == 1: return a[0]
return None
@@ -863,7 +888,9 @@ class query(object):
def dns_a(self, domainname):
"""Get a list of IP addresses for a domainname."""
if domainname:
return self.dns(domainname, 'A')
return []
def dns_aaaa(self, domainname):
"""Get a list of IPv6 addresses for a domainname."""
@@ -921,14 +948,13 @@ class query(object):
def get_header(self,res,receiver=None):
if not receiver:
receiver = self.r
if res in ('pass','fail','softfail'):
if res in ('unknown','permerror'):
txt = ' '.join([res] + self.mech)
else:
txt = res
return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
res,receiver,self.get_header_comment(res),self.i,
txt,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.
@@ -950,10 +976,10 @@ class query(object):
"%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 \
elif res in ('unknown','permerror'): return \
"permanent error in processing domain of %s: %s" \
% (sender, self.prob)
elif res == 'error': return \
elif res in ('error','temperror'): return \
"temporary error in processing during lookup of %s" % sender
elif res == 'fail': return \
"domain of %s does not designate %s as permitted sender" \
+1 -1
View File
@@ -23,7 +23,7 @@ SMTP (email) servers to prevent criminals from forging your
domain. The simplest step is usually to publish an SPF record
with your Sender Policy.
For more information, see: http://openspf.com
For more information, see: http://openspf.org
I hate to annoy you with a DSN (Delivery Status
Notification) from a possibly forged email, but since you