Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 802dc01c84 |
@@ -8,6 +8,7 @@ include testsample.py
|
|||||||
include testmime.py
|
include testmime.py
|
||||||
include testbms.py
|
include testbms.py
|
||||||
include testdspam.py
|
include testdspam.py
|
||||||
|
include rejects.py
|
||||||
include bms.py
|
include bms.py
|
||||||
include spf.py
|
include spf.py
|
||||||
include spfquery.py
|
include spfquery.py
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
Here is a history of user visible changes to Python milter.
|
Here is a history of user visible changes to Python milter.
|
||||||
|
|
||||||
|
0.7.0 SPF check hello name
|
||||||
|
Move pythonsock to /var/run/milter
|
||||||
|
Move milter.cfg to /etc/mail/pymilter.cfg
|
||||||
|
Check M$ style XML CID records by converting to SPF
|
||||||
|
Recognize, but never match ip6 - until we properly support it.
|
||||||
|
Option to reject when no PTR and no SPF
|
||||||
0.6.9 Reject invalid SRS immediately for benefit of callback verifiers
|
0.6.9 Reject invalid SRS immediately for benefit of callback verifiers
|
||||||
Fix include bug in spf.py
|
Fix include bug in spf.py
|
||||||
Fix check_header bug
|
Fix check_header bug
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
|
Message not saved for following traceback:
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/usr/lib/python2.3/site-packages/Milter.py", line 188, in <lambda>
|
||||||
|
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
|
||||||
|
File "bms.py", line 935, in eom
|
||||||
|
msg.dump(out)
|
||||||
|
File "/usr/lib/python2.3/site-packages/mime.py", line 347, in dump
|
||||||
|
g.flatten(self,unixfrom=unixfrom)
|
||||||
|
File "/var/tmp/python2.3-2.3.3-root/usr/lib/python2.3/email/Generator.py", line 102, in flatten
|
||||||
|
File "/var/tmp/python2.3-2.3.3-root/usr/lib/python2.3/email/Generator.py", line 130, in _write
|
||||||
|
File "/var/tmp/python2.3-2.3.3-root/usr/lib/python2.3/email/Generator.py", line 156, in _dispatch
|
||||||
|
File "/var/tmp/python2.3-2.3.3-root/usr/lib/python2.3/email/Generator.py", line 199, in _handle_text
|
||||||
|
TypeError: string payload expected: <type 'list'>
|
||||||
|
------------
|
||||||
|
spf.py has no recursion bound on CNAME lookup
|
||||||
|
Support SMTP AUTH and disable SPF checks when connection is authorized.
|
||||||
Web admin interface
|
Web admin interface
|
||||||
RHBL
|
RHSBL
|
||||||
Check valid domains allowed by internal senders to detect PCs infected
|
Check valid domains allowed by internal senders to detect PCs infected
|
||||||
with spam trojans.
|
with spam trojans.
|
||||||
Do CBV (callback verification) for mail with no published SPF record.
|
Do CBV (callback verification) for mail with no published SPF record.
|
||||||
message log for automated stats and blacklisting
|
message log for automated stats and blacklisting
|
||||||
adapt init script to work on RH9
|
|
||||||
Skip dspam when SPF pass?
|
Skip dspam when SPF pass?
|
||||||
Report 551 with rcpt on SPF fail?
|
Report 551 with rcpt on SPF fail?
|
||||||
check spam keywords with character classes, e.g.
|
check spam keywords with character classes, e.g.
|
||||||
|
|||||||
@@ -1,6 +1,37 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# A simple milter.
|
# A simple milter.
|
||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.114 2004/07/27 00:40:12 stuart
|
||||||
|
# Make reject on no PTR optional.
|
||||||
|
#
|
||||||
|
# Revision 1.113 2004/07/23 23:11:14 stuart
|
||||||
|
# Log known malformed messages differently than general processing exceptions.
|
||||||
|
#
|
||||||
|
# Revision 1.112 2004/07/21 19:18:33 stuart
|
||||||
|
# Punt on UnicodeDecodeError when decoding headers.
|
||||||
|
# Accept a pass with default SPF for missing reverse IP.
|
||||||
|
#
|
||||||
|
# Revision 1.111 2004/07/18 13:13:31 stuart
|
||||||
|
# Reject invalid SRS only for SRS domain (which is the only one we
|
||||||
|
# know the key for).
|
||||||
|
# Reject senders that have neither reverse IP nor SPF.
|
||||||
|
#
|
||||||
|
# Revision 1.110 2004/06/12 03:13:18 stuart
|
||||||
|
# Block bounces only for SRS domain. Also treat mail from
|
||||||
|
# postmaster or mailer-daemon as DSN for SRS/SES checking purposes.
|
||||||
|
#
|
||||||
|
# Revision 1.109 2004/05/01 02:56:55 stuart
|
||||||
|
# Let multiple screeners share work.
|
||||||
|
#
|
||||||
|
# Revision 1.108 2004/04/29 20:36:23 stuart
|
||||||
|
# Require HELO name
|
||||||
|
#
|
||||||
|
# Revision 1.107 2004/04/24 22:55:29 stuart
|
||||||
|
# Move some files to make the RPM more standard.
|
||||||
|
#
|
||||||
|
# Revision 1.106 2004/04/21 18:29:08 stuart
|
||||||
|
# Validate hello name with SPF.
|
||||||
|
#
|
||||||
# Revision 1.105 2004/04/20 15:16:00 stuart
|
# Revision 1.105 2004/04/20 15:16:00 stuart
|
||||||
# Release 0.6.9
|
# Release 0.6.9
|
||||||
#
|
#
|
||||||
@@ -242,14 +273,16 @@ dspam_users = {}
|
|||||||
dspam_userdir = None
|
dspam_userdir = None
|
||||||
dspam_exempt = {}
|
dspam_exempt = {}
|
||||||
dspam_whitelist = {}
|
dspam_whitelist = {}
|
||||||
dspam_screener = None
|
dspam_screener = ()
|
||||||
dspam_internal = True # True if internal mail should be dspammed
|
dspam_internal = True # True if internal mail should be dspammed
|
||||||
dspam_reject = ()
|
dspam_reject = ()
|
||||||
dspam_sizelimit = 180000
|
dspam_sizelimit = 180000
|
||||||
srs = None
|
srs = None
|
||||||
srs_reject_spoofed = False
|
srs_reject_spoofed = False
|
||||||
|
srs_fwdomain = None
|
||||||
spf_reject_neutral = ()
|
spf_reject_neutral = ()
|
||||||
spf_best_guess = False
|
spf_best_guess = False
|
||||||
|
spf_reject_noptr = False
|
||||||
timeout = 600
|
timeout = 600
|
||||||
|
|
||||||
class MilterConfigParser(ConfigParser.ConfigParser):
|
class MilterConfigParser(ConfigParser.ConfigParser):
|
||||||
@@ -300,7 +333,7 @@ class MilterConfigParser(ConfigParser.ConfigParser):
|
|||||||
def read_config(list):
|
def read_config(list):
|
||||||
cp = MilterConfigParser({
|
cp = MilterConfigParser({
|
||||||
'tempdir': "/var/log/milter/save",
|
'tempdir': "/var/log/milter/save",
|
||||||
'socket': "/var/log/milter/pythonsock",
|
'socket': "/var/run/milter/pythonsock",
|
||||||
'timeout': '600',
|
'timeout': '600',
|
||||||
'scan_html': 'no',
|
'scan_html': 'no',
|
||||||
'scan_rfc822': 'yes',
|
'scan_rfc822': 'yes',
|
||||||
@@ -310,6 +343,7 @@ def read_config(list):
|
|||||||
'maxage': '8',
|
'maxage': '8',
|
||||||
'hashlength': '8',
|
'hashlength': '8',
|
||||||
'reject_spoofed': 'no',
|
'reject_spoofed': 'no',
|
||||||
|
'reject_noptr': 'no',
|
||||||
'best_guess': 'no'
|
'best_guess': 'no'
|
||||||
})
|
})
|
||||||
cp.read(list)
|
cp.read(list)
|
||||||
@@ -355,13 +389,13 @@ def read_config(list):
|
|||||||
|
|
||||||
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt
|
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt
|
||||||
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
|
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
|
||||||
global spf_reject_neutral,spf_best_guess,SRS
|
global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr
|
||||||
dspam_dict = cp.getdefault('dspam','dspam_dict')
|
dspam_dict = cp.getdefault('dspam','dspam_dict')
|
||||||
dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
|
dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
|
||||||
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
|
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
|
||||||
dspam_users = cp.getaddrdict('dspam','dspam_users')
|
dspam_users = cp.getaddrdict('dspam','dspam_users')
|
||||||
dspam_userdir = cp.getdefault('dspam','dspam_userdir')
|
dspam_userdir = cp.getdefault('dspam','dspam_userdir')
|
||||||
dspam_screener = cp.getdefault('dspam','dspam_screener')
|
dspam_screener = cp.getlist('dspam','dspam_screener')
|
||||||
dspam_reject = cp.getlist('dspam','dspam_reject')
|
dspam_reject = cp.getlist('dspam','dspam_reject')
|
||||||
if cp.has_option('dspam','dspam_sizelimit'):
|
if cp.has_option('dspam','dspam_sizelimit'):
|
||||||
dspam_sizelimit = cp.getint('dspam','dspam_sizelimit')
|
dspam_sizelimit = cp.getint('dspam','dspam_sizelimit')
|
||||||
@@ -370,11 +404,12 @@ def read_config(list):
|
|||||||
spf.DELEGATE = cp.getdefault('spf','delegate')
|
spf.DELEGATE = cp.getdefault('spf','delegate')
|
||||||
spf_reject_neutral = cp.getlist('spf','reject_neutral')
|
spf_reject_neutral = cp.getlist('spf','reject_neutral')
|
||||||
spf_best_guess = cp.getboolean('spf','best_guess')
|
spf_best_guess = cp.getboolean('spf','best_guess')
|
||||||
|
spf_reject_noptr = cp.getboolean('spf','reject_noptr')
|
||||||
srs_config = cp.getdefault('srs','config')
|
srs_config = cp.getdefault('srs','config')
|
||||||
if srs_config: cp.read([srs_config])
|
if srs_config: cp.read([srs_config])
|
||||||
srs_secret = cp.getdefault('srs','secret')
|
srs_secret = cp.getdefault('srs','secret')
|
||||||
if SRS and srs_secret:
|
if SRS and srs_secret:
|
||||||
global srs,srs_reject_spoofed
|
global srs,srs_reject_spoofed,srs_fwdomain
|
||||||
database = cp.getdefault('srs','database')
|
database = cp.getdefault('srs','database')
|
||||||
srs_reject_spoofed = cp.getboolean('srs','reject_spoofed')
|
srs_reject_spoofed = cp.getboolean('srs','reject_spoofed')
|
||||||
maxage = cp.getint('srs','maxage')
|
maxage = cp.getint('srs','maxage')
|
||||||
@@ -387,7 +422,7 @@ def read_config(list):
|
|||||||
else:
|
else:
|
||||||
srs = SRS.Guarded.Guarded(secret=srs_secret,
|
srs = SRS.Guarded.Guarded(secret=srs_secret,
|
||||||
maxage=maxage,hashlength=hashlength,separator=separator)
|
maxage=maxage,hashlength=hashlength,separator=separator)
|
||||||
|
srs_fwdomain = cp.getdefault('srs','fwdomain')
|
||||||
|
|
||||||
def parse_addr(t):
|
def parse_addr(t):
|
||||||
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
|
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
|
||||||
@@ -408,6 +443,8 @@ def parse_header(val):
|
|||||||
try:
|
try:
|
||||||
return u.encode(enc)
|
return u.encode(enc)
|
||||||
except UnicodeError: continue
|
except UnicodeError: continue
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return val
|
||||||
except LookupError:
|
except LookupError:
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@@ -448,6 +485,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log('%s: %s' % (name,val))
|
self.log('%s: %s' % (name,val))
|
||||||
|
|
||||||
def connect(self,hostname,unused,hostaddr):
|
def connect(self,hostname,unused,hostaddr):
|
||||||
|
self.missing_ptr = hostname.startswith('[') and hostname.endswith(']')
|
||||||
self.internal_connection = False
|
self.internal_connection = False
|
||||||
self.trusted_relay = False
|
self.trusted_relay = False
|
||||||
self.receiver = self.getsymval('j')
|
self.receiver = self.getsymval('j')
|
||||||
@@ -475,6 +513,8 @@ class bmsMilter(Milter.Milter):
|
|||||||
if self.trusted_relay:
|
if self.trusted_relay:
|
||||||
connecttype += ' TRUSTED'
|
connecttype += ' TRUSTED'
|
||||||
self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype))
|
self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype))
|
||||||
|
self.hello_name = None
|
||||||
|
self.connecthost = hostname
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def hello(self,hostname):
|
def hello(self,hostname):
|
||||||
@@ -531,6 +571,10 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.dspam = False
|
self.dspam = False
|
||||||
else:
|
else:
|
||||||
self.rejectvirus = False
|
self.rejectvirus = False
|
||||||
|
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
|
||||||
if not (self.internal_connection or self.trusted_relay) \
|
if not (self.internal_connection or self.trusted_relay) \
|
||||||
and self.connectip and spf:
|
and self.connectip and spf:
|
||||||
return self.check_spf()
|
return self.check_spf()
|
||||||
@@ -543,12 +587,27 @@ class bmsMilter(Milter.Milter):
|
|||||||
q.set_default_explanation('SPF fail: see http://spf.pobox.com/why.html')
|
q.set_default_explanation('SPF fail: see http://spf.pobox.com/why.html')
|
||||||
res,code,txt = q.check()
|
res,code,txt = q.check()
|
||||||
receiver = self.receiver
|
receiver = self.receiver
|
||||||
if res == 'none' and spf_best_guess:
|
if res == 'none':
|
||||||
#self.log('SPF: no record published, guessing')
|
if self.mailfrom != '<>':
|
||||||
q.set_default_explanation('SPF guess: see http://spf.pobox.com/why.html')
|
# check hello name via spf
|
||||||
# best_guess should not result in fail
|
hres,hcode,htxt = spf.check(self.connectip,'',self.hello_name)
|
||||||
res,code,txt = q.best_guess()
|
if hres in ('deny','fail','neutral','softfail'):
|
||||||
receiver += ': guessing'
|
self.log('REJECT: hello SPF: %s %i %s' % (hres,hcode,htxt))
|
||||||
|
self.setreply('550','5.7.1',htxt)
|
||||||
|
return Milter.REJECT
|
||||||
|
if 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 self.missing_ptr and res in ('neutral', 'none') and spf_reject_noptr:
|
||||||
|
self.log('REJECT: no PTR or SPF')
|
||||||
|
self.setreply('550','5.7.1',
|
||||||
|
'You must have a reverse lookup or publish SPF: http://spf.pobox.com'
|
||||||
|
)
|
||||||
|
return Milter.REJECT
|
||||||
if res in ('deny', 'fail'):
|
if res in ('deny', 'fail'):
|
||||||
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
||||||
self.setreply(str(code),'5.7.1',txt)
|
self.setreply(str(code),'5.7.1',txt)
|
||||||
@@ -576,15 +635,19 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log("rcpt to",to,str)
|
self.log("rcpt to",to,str)
|
||||||
t = parse_addr(to.lower())
|
t = parse_addr(to.lower())
|
||||||
if len(t) == 2:
|
if len(t) == 2:
|
||||||
if self.mailfrom == '<>':
|
user,domain = t
|
||||||
|
if self.mailfrom == '<>' or self.canon_from.startswith('postmaster@') \
|
||||||
|
or self.canon_from.startswith('mailer-daemon@'):
|
||||||
if self.recipients:
|
if self.recipients:
|
||||||
self.log('REJECT: Multiple bounce recipients')
|
self.log('REJECT: Multiple bounce recipients')
|
||||||
self.setreply('550','5.7.1','Multiple bounce recipients')
|
self.setreply('550','5.7.1','Multiple bounce recipients')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if srs and not (self.internal_connection or self.trusted_relay):
|
if srs and not (self.internal_connection or self.trusted_relay) \
|
||||||
|
and domain == srs_fwdomain:
|
||||||
oldaddr = '@'.join(parse_addr(to))
|
oldaddr = '@'.join(parse_addr(to))
|
||||||
try:
|
try:
|
||||||
newaddr = srs.reverse(oldaddr)
|
newaddr = srs.reverse(oldaddr)
|
||||||
|
# Currently, a sendmail map reverses SRS. We just log it here.
|
||||||
self.log("srs rcpt:",newaddr)
|
self.log("srs rcpt:",newaddr)
|
||||||
except:
|
except:
|
||||||
if srsre.match(oldaddr):
|
if srsre.match(oldaddr):
|
||||||
@@ -592,13 +655,13 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.setreply('550','5.7.1','Invalid SRS signature')
|
self.setreply('550','5.7.1','Invalid SRS signature')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
self.data_allowed = not srs_reject_spoofed
|
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))
|
self.recipients.append('@'.join(t))
|
||||||
user,domain = t
|
|
||||||
users = check_user.get(domain)
|
users = check_user.get(domain)
|
||||||
if self.discard:
|
if self.discard:
|
||||||
self.del_recipient(to)
|
self.del_recipient(to)
|
||||||
if users and not user in users:
|
if users and not user in users:
|
||||||
self.log('REJECT: RCPT TO:',to,str)
|
self.log('REJECT: RCPT TO:',to)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if user in block_forward.get(domain,()):
|
if user in block_forward.get(domain,()):
|
||||||
self.forward = False
|
self.forward = False
|
||||||
@@ -686,7 +749,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
def header(self,name,hval):
|
def header(self,name,hval):
|
||||||
if not self.data_allowed:
|
if not self.data_allowed:
|
||||||
self.log('REJECT: bounce with no SRS encoding')
|
self.log('REJECT: bounce with no SRS encoding')
|
||||||
self.setreply('550','5.7.1',"spoofed reply address")
|
self.setreply('550','5.7.1',"I did not send you this message.")
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
lname = name.lower()
|
lname = name.lower()
|
||||||
# decode near ascii text to unobfuscate
|
# decode near ascii text to unobfuscate
|
||||||
@@ -832,21 +895,22 @@ class bmsMilter(Milter.Milter):
|
|||||||
print x
|
print x
|
||||||
# screen if no recipients are dspam_users
|
# screen if no recipients are dspam_users
|
||||||
if not modified and dspam_screener and not self.internal_connection \
|
if not modified and dspam_screener and not self.internal_connection \
|
||||||
and (self.dspam or self.reject_spam):
|
and self.dspam:
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
txt = self.fp.read()
|
txt = self.fp.read()
|
||||||
if len(txt) > dspam_sizelimit:
|
if len(txt) > dspam_sizelimit:
|
||||||
self.log("Large message:",len(txt))
|
self.log("Large message:",len(txt))
|
||||||
return False
|
return False
|
||||||
if not ds.check_spam(dspam_screener,txt,self.recipients,
|
screener = dspam_screener[self.id % len(dspam_screener)]
|
||||||
|
if not ds.check_spam(screener,txt,self.recipients,
|
||||||
classify=True,quarantine=not self.reject_spam):
|
classify=True,quarantine=not self.reject_spam):
|
||||||
self.fp = None
|
self.fp = None
|
||||||
if self.reject_spam:
|
if self.reject_spam:
|
||||||
self.log("DSPAM:",dspam_screener,
|
self.log("DSPAM:",screener,
|
||||||
'REJECT: X-DSpam-Score: %f' % ds.probability)
|
'REJECT: X-DSpam-Score: %f' % ds.probability)
|
||||||
self.setreply('550','5.7.1','Your Message looks spammy')
|
self.setreply('550','5.7.1','Your Message looks spammy')
|
||||||
return True
|
return True
|
||||||
self.log("DSPAM:",dspam_screener,"SCREENED")
|
self.log("DSPAM:",screener,"SCREENED")
|
||||||
return modified
|
return modified
|
||||||
|
|
||||||
def eom(self):
|
def eom(self):
|
||||||
@@ -881,16 +945,18 @@ class bmsMilter(Milter.Milter):
|
|||||||
fname = tempfile.mktemp(".fail") # save message that caused crash
|
fname = tempfile.mktemp(".fail") # save message that caused crash
|
||||||
os.rename(self.tempname,fname)
|
os.rename(self.tempname,fname)
|
||||||
self.tempname = None
|
self.tempname = None
|
||||||
self.log("FAIL: %s" % fname) # log filename
|
|
||||||
if exc_type == email.Errors.BoundaryError:
|
if exc_type == email.Errors.BoundaryError:
|
||||||
|
self.log("MALFORMED: %s" % fname) # log filename
|
||||||
self.setreply('554','5.7.7',
|
self.setreply('554','5.7.7',
|
||||||
'Boundary error in your message, are you a spammer?')
|
'Boundary error in your message, are you a spammer?')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if exc_type == email.Errors.HeaderParseError:
|
if exc_type == email.Errors.HeaderParseError:
|
||||||
|
self.log("MALFORMED: %s" % fname) # log filename
|
||||||
self.setreply('554','5.7.7',
|
self.setreply('554','5.7.7',
|
||||||
'Header parse error in your message, are you a spammer?')
|
'Header parse error in your message, are you a spammer?')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
# let default exception handler print traceback and return 451 code
|
# let default exception handler print traceback and return 451 code
|
||||||
|
self.log("FAIL: %s" % fname) # log filename
|
||||||
raise
|
raise
|
||||||
if rc == Milter.REJECT: return rc;
|
if rc == Milter.REJECT: return rc;
|
||||||
if rc == Milter.DISCARD: return rc;
|
if rc == Milter.DISCARD: return rc;
|
||||||
@@ -967,13 +1033,13 @@ def main():
|
|||||||
if srs or len(discard_users) > 0 or smart_alias or dspam_userdir:
|
if srs or len(discard_users) > 0 or smart_alias or dspam_userdir:
|
||||||
flags = flags + Milter.DELRCPT
|
flags = flags + Milter.DELRCPT
|
||||||
Milter.set_flags(flags)
|
Milter.set_flags(flags)
|
||||||
print "bms milter startup"
|
print "%s bms milter startup" % time.strftime('%Y%b%d %H:%M:%S')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
Milter.runmilter("pythonfilter",socketname,timeout)
|
Milter.runmilter("pythonfilter",socketname,timeout)
|
||||||
print "bms milter shutdown"
|
print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
read_config(["milter.cfg"])
|
read_config(["/etc/mail/pymilter.cfg","milter.cfg"])
|
||||||
if dspam_dict:
|
if dspam_dict:
|
||||||
import dspam # low level spam check
|
import dspam # low level spam check
|
||||||
if dspam_userdir:
|
if dspam_userdir:
|
||||||
|
|||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/python2.3
|
||||||
|
|
||||||
|
# Convert a MS Caller-ID entry (XML) to a SPF entry
|
||||||
|
#
|
||||||
|
# (c) 2004 by Ernesto Baschny
|
||||||
|
# (c) 2004 Python version by Stuart Gathman
|
||||||
|
#
|
||||||
|
# Date: 2004-02-25
|
||||||
|
# Version: 1.0
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./cid2spf.pl "<ep xmlns='http://ms.net/1'>...</ep>"
|
||||||
|
#
|
||||||
|
# Note that the 'include' directives will also have to be checked and
|
||||||
|
# "translated". Future versions of this script might be able to get a
|
||||||
|
# domain name as an argument and "crawl" the DNS for the necessary
|
||||||
|
# information.
|
||||||
|
#
|
||||||
|
# A complete reverse translation (SPF -> CID) might be impossible, since
|
||||||
|
# there are no way to handle:
|
||||||
|
# - PTR and EXISTS mechanism
|
||||||
|
# - MX mechanism with an different domain as argument
|
||||||
|
# - macros
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# http://www.microsoft.com/mscorp/twc/privacy/spam_callerid.mspx
|
||||||
|
# http://spf.pobox.com/
|
||||||
|
#
|
||||||
|
# Known bugs:
|
||||||
|
# - Currently it won't handle the exclusions provided in the A and R
|
||||||
|
# tags (prefix '!'). They will show up "as-is" in the SPF record
|
||||||
|
# - I really haven't read the MS-CID specs in-depth, so there are probably
|
||||||
|
# other bugs too :)
|
||||||
|
#
|
||||||
|
# Ernesto Baschny <ernst@baschny.de>
|
||||||
|
#
|
||||||
|
|
||||||
|
import xml.sax
|
||||||
|
import spf
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
class CIDParser(xml.sax.ContentHandler):
|
||||||
|
"Convert a MS Caller-ID entry (XML) to a SPF entry"
|
||||||
|
|
||||||
|
def __init__(self,q=None):
|
||||||
|
self.spf = []
|
||||||
|
self.action = '-all'
|
||||||
|
self.has_servers = None
|
||||||
|
self.spf_entry = None
|
||||||
|
if q:
|
||||||
|
self.spf_query = q
|
||||||
|
else:
|
||||||
|
self.spf_query = spf.query(i='127.0.0.1', s='localhost', h='unknown')
|
||||||
|
|
||||||
|
def startElement(self,tag,attr):
|
||||||
|
if tag == 'm':
|
||||||
|
if self.has_servers != None and not self.has_servers:
|
||||||
|
raise ValueError(
|
||||||
|
"Declared <noMailServers\> and later <m>, this CID entry is not valid."
|
||||||
|
)
|
||||||
|
self.has_servers = True
|
||||||
|
elif tag == 'noMailServers':
|
||||||
|
if self.has_servers:
|
||||||
|
raise ValueError(
|
||||||
|
"Declared <m> and later <noMailServers\>, this CID entry is not valid."
|
||||||
|
)
|
||||||
|
self.has_servers = False
|
||||||
|
elif tag == 'ep':
|
||||||
|
if attr.has_key('testing') and attr.getValue('testing') == 'true':
|
||||||
|
# A CID with 'testing' found:
|
||||||
|
# From the MS-specs:
|
||||||
|
# "Documents in which such attribute is present with a true
|
||||||
|
# value SHOULD be entirely ignored (one should act as if the
|
||||||
|
# document were absent)"
|
||||||
|
# From the SPF-specs:
|
||||||
|
# "Neutral (?): The SPF client MUST proceed as if a domain did
|
||||||
|
# not publish SPF data."
|
||||||
|
# So we set SPF action to "neutral":
|
||||||
|
self.action = '?all'
|
||||||
|
elif tag == 'mx':
|
||||||
|
# The empty MX-tag, same as SPF's MX-mechanism
|
||||||
|
self.spf.append('mx')
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
def characters(self,text):
|
||||||
|
tag = self.tag
|
||||||
|
# Remove starting and trailing spaces from text:
|
||||||
|
text = text.strip()
|
||||||
|
|
||||||
|
if tag == 'a' or tag == 'r':
|
||||||
|
# The A and R tags from MS-CID are both handled by the
|
||||||
|
# ipv4/6-mechanisms from SPF:
|
||||||
|
if text.find(':') < 0:
|
||||||
|
mechanism = 'ip4'
|
||||||
|
else:
|
||||||
|
mechanism = 'ip6'
|
||||||
|
self.spf.append(mechanism + ':' + text)
|
||||||
|
elif tag == 'indirect':
|
||||||
|
# MS-CID's indirect is "sort of" the include from SPF:
|
||||||
|
# Not really true, because the <indirect> tag from MS-CID also
|
||||||
|
# provides a fallback in case the included domain doesn't provide
|
||||||
|
# _ep-records: The inbound MX-servers of the included domains
|
||||||
|
# are added to the list of allowed outgoing mailservers for the
|
||||||
|
# domain that declared the _ep-record with the <indirect> tag.
|
||||||
|
# In SPF you would use the 'mx:domain' to handle this, but this
|
||||||
|
# wouldn't depend on referred domain having or not SPF-records.
|
||||||
|
cid_xml = self.cid_txt(text)
|
||||||
|
if cid_xml:
|
||||||
|
p = CIDParser()
|
||||||
|
xml.sax.parseString(cid_xml,p)
|
||||||
|
if p.has_servers != False:
|
||||||
|
self.spf += p.spf
|
||||||
|
else:
|
||||||
|
self.spf.append('mx:' + text)
|
||||||
|
|
||||||
|
def cid_txt(self,domain):
|
||||||
|
q = self.spf_query
|
||||||
|
domain='_ep.' + domain
|
||||||
|
a = q.dns_txt(domain)
|
||||||
|
if not a: return None
|
||||||
|
if a[0].lower().startswith('<ep ') and a[-1].lower().endswith('</ep>'):
|
||||||
|
return ''.join(a)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def endElement(self,tag):
|
||||||
|
if tag == 'ep':
|
||||||
|
# This is the end... assemble what we've got
|
||||||
|
spf_entry = ['v=spf1']
|
||||||
|
if self.has_servers != False:
|
||||||
|
spf_entry += self.spf
|
||||||
|
spf_entry.append(self.action)
|
||||||
|
self.spf_entry = ' '.join(spf_entry)
|
||||||
|
|
||||||
|
def spf_txt(self,cid_xml):
|
||||||
|
if not cid_xml.startswith('<'):
|
||||||
|
cid_xml = self.cid_txt(cid_xml)
|
||||||
|
if not cid_xml: return None
|
||||||
|
# Parse the beast. Any XML-problem will be reported by xlm.sax
|
||||||
|
self.spf_entry = None
|
||||||
|
xml.sax.parseString(cid_xml,self)
|
||||||
|
return self.spf_entry
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print >>sys.stderr, \
|
||||||
|
"""Usage: %s "<ep xmlns='http://ms.net/1'>...</ep>" """ % sys.argv[0]
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cid_xml = sys.argv[1]
|
||||||
|
|
||||||
|
p = CIDParser()
|
||||||
|
print p.spf_txt(cid_xml)
|
||||||
@@ -134,5 +134,26 @@ is a milter declaration for sendmail.cf with all timeouts specified:
|
|||||||
Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
|
Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
<a name="spf">
|
||||||
|
<li> Q. So how do I use the SPF support? The sample.py milter doesn't seem
|
||||||
|
to use it.
|
||||||
|
<p> A. The bms.py milter supports spf. The RedHat RPMs will set almost
|
||||||
|
everything up for you. For other systems:
|
||||||
|
<ol type=i>
|
||||||
|
<li> Arrange to run bms.py in the background (as a service perhaps) and
|
||||||
|
redirect output and errors to a logfile. For instance, on AIX you'll want
|
||||||
|
to use SRC (System Resource Controller).
|
||||||
|
<li> Copy milter.cfg to the directory you run bms.py in, and edit it. The
|
||||||
|
comments should explain the options.
|
||||||
|
<li> Start bms.py in the background as arranged.
|
||||||
|
<li> Add Xpythonfilter to sendmail.cf or add an INPUT_MAIL_FILTER to
|
||||||
|
sendmail.mc. Regen sendmail.cf if you use sendmail.mc and restart
|
||||||
|
sendmail.
|
||||||
|
<li> Arrange to rotate log files and remove old defang files in
|
||||||
|
<code>tempdir</code>. The RedHat RPM uses <code>logrotate</code> for
|
||||||
|
logfiles and a simple cron script using <code>find</code> to clean
|
||||||
|
<code>tempdir</code>.
|
||||||
|
</ol>
|
||||||
|
|
||||||
</ol>
|
</ol>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
# features intended to filter or block incoming mail
|
# features intended to filter or block incoming mail
|
||||||
[milter]
|
[milter]
|
||||||
;socket=/var/log/milter/pythonsock
|
;socket=/var/run/milter/pythonsock
|
||||||
tempdir = /var/log/milter/save
|
tempdir = /var/log/milter/save
|
||||||
;timeout=600
|
;timeout=600
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
|
|||||||
vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck,
|
vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck,
|
||||||
p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam,
|
p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam,
|
||||||
v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra,
|
v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra,
|
||||||
x@n3x, vicod3n, penís, v|c0d1n, phentermine, en1arge, dip1oma, v1codin
|
x@n3x, vicod3n, penís, c0d1n, phentermine, en1arge, dip1oma, v1codin
|
||||||
# spam words are case sensitive
|
# spam words are case sensitive
|
||||||
spam_words = $$$, !!!, XXX, FREE, HGH
|
spam_words = $$$, !!!, XXX, FREE, HGH
|
||||||
|
|
||||||
@@ -52,6 +52,8 @@ reject_spoofed = 0
|
|||||||
;reject_neutral = aol.com
|
;reject_neutral = aol.com
|
||||||
# use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published
|
# use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published
|
||||||
;best_guess = 0
|
;best_guess = 0
|
||||||
|
# reject senders that have neither PTR nor SPF records
|
||||||
|
;reject_noptr = 0
|
||||||
|
|
||||||
# features intended to clean up outgoing mail
|
# features intended to clean up outgoing mail
|
||||||
[scrub]
|
[scrub]
|
||||||
|
|||||||
+8
-11
@@ -24,7 +24,7 @@ ALT="Viewable With Any Browser" BORDER="0"></A>
|
|||||||
Stuart D. Gathman</a><br>
|
Stuart D. Gathman</a><br>
|
||||||
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
|
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
|
||||||
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
|
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
|
||||||
Last updated Apr 21, 2004</h4>
|
Last updated Jun 08, 2004</h4>
|
||||||
|
|
||||||
See the <a href="faq.html">FAQ</a> | <a href="#download">Download now</a> |
|
See the <a href="faq.html">FAQ</a> | <a href="#download">Download now</a> |
|
||||||
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a>
|
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a>
|
||||||
@@ -40,11 +40,8 @@ Version 8.12 seems to be more robust, and includes new privilege
|
|||||||
separation features to enhance security.
|
separation features to enhance security.
|
||||||
I recommend upgrading.
|
I recommend upgrading.
|
||||||
|
|
||||||
<h2> <a name=dspam>Bayesian Filtering</a> </h2>
|
<h2> Recent Changes </h2>
|
||||||
|
|
||||||
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>,
|
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.
|
a protocol to prevent forging of the envelope from address.
|
||||||
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
|
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
|
||||||
@@ -52,15 +49,15 @@ 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>.
|
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
|
||||||
The updated version tracks the draft RFC and test suite.
|
The updated version tracks the draft RFC and test suite.
|
||||||
<p>
|
<p>
|
||||||
Release 0.6.0 offers a simple application of dspam I call "header triage",
|
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
|
||||||
which rejects messages with spammy headers. Since sendmail has to
|
|
||||||
read the entire message anyway once we start reading headers, it
|
|
||||||
would probably be better to scan the whole message - except that
|
|
||||||
we replace dangerous attachments elsewhere in the milter - which screws up the
|
|
||||||
body statistics for messages with dangerous attachments.
|
|
||||||
<p>
|
<p>
|
||||||
Release 0.6.1 adds a full milter based dspam application.
|
Release 0.6.1 adds a full milter based dspam application.
|
||||||
<p>
|
<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,
|
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
|
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
|
lots of spam. That dictionary can be used to block spam that is
|
||||||
|
|||||||
+15
-3
@@ -1,5 +1,5 @@
|
|||||||
%define name milter
|
%define name milter
|
||||||
%define version 0.6.9
|
%define version 0.7.0
|
||||||
%define release 1
|
%define release 1
|
||||||
# Redhat 7.x and earlier (multiple ps lines per thread)
|
# Redhat 7.x and earlier (multiple ps lines per thread)
|
||||||
%define sysvinit milter.rc7
|
%define sysvinit milter.rc7
|
||||||
@@ -43,8 +43,10 @@ env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build
|
|||||||
rm -rf $RPM_BUILD_ROOT
|
rm -rf $RPM_BUILD_ROOT
|
||||||
%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
|
%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
|
||||||
mkdir -p $RPM_BUILD_ROOT/var/log/milter
|
mkdir -p $RPM_BUILD_ROOT/var/log/milter
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/etc/mail
|
||||||
mkdir $RPM_BUILD_ROOT/var/log/milter/save
|
mkdir $RPM_BUILD_ROOT/var/log/milter/save
|
||||||
cp bms.py milter.cfg $RPM_BUILD_ROOT/var/log/milter
|
cp bms.py $RPM_BUILD_ROOT/var/log/milter
|
||||||
|
cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg
|
||||||
|
|
||||||
# logfile rotation
|
# logfile rotation
|
||||||
mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d
|
mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d
|
||||||
@@ -103,6 +105,9 @@ mkssys -s milter -p /var/log/milter/start.sh -u 25 -S -n 15 -f 9 -G mail || :
|
|||||||
if [ $1 = 0 ]; then
|
if [ $1 = 0 ]; then
|
||||||
rmssys -s milter || :
|
rmssys -s milter || :
|
||||||
fi
|
fi
|
||||||
|
%else
|
||||||
|
%post
|
||||||
|
echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
%clean
|
%clean
|
||||||
@@ -124,9 +129,16 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
%dir /var/log/milter/save
|
%dir /var/log/milter/save
|
||||||
%config /var/log/milter/start.sh
|
%config /var/log/milter/start.sh
|
||||||
%config /var/log/milter/bms.py
|
%config /var/log/milter/bms.py
|
||||||
%config /var/log/milter/milter.cfg
|
%config(noreplace) /etc/mail/pymilter.cfg
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Fri Jul 23 2004 Stuart Gathman <stuart@bmsi.com> 0.7.0-1
|
||||||
|
- SPF check hello name
|
||||||
|
- Move pythonsock to /var/run/milter
|
||||||
|
- Move milter.cfg to /etc/mail/pymilter.cfg
|
||||||
|
- Check M$ style XML CID records by converting to SPF
|
||||||
|
- Recognize, but never match ip6 until we properly support it.
|
||||||
|
- Option to reject when no PTR and no SPF
|
||||||
* Fri Apr 09 2004 Stuart Gathman <stuart@bmsi.com> 0.6.9-1
|
* 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
|
- Validate spf.py against test suite, and add Received-SPF support to spf.py
|
||||||
- Support best_guess for SPF
|
- Support best_guess for SPF
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.53 2004/04/24 22:53:20 stuart
|
||||||
|
# Rename some local variables to avoid shadowing builtins
|
||||||
|
#
|
||||||
|
# Revision 1.52 2004/04/24 22:47:13 stuart
|
||||||
|
# Convert header values to str
|
||||||
|
#
|
||||||
# Revision 1.51 2004/03/25 03:19:10 stuart
|
# Revision 1.51 2004/03/25 03:19:10 stuart
|
||||||
# Correctly defang rfc822 attachments when boundary specified with
|
# Correctly defang rfc822 attachments when boundary specified with
|
||||||
# content-type message/rfc822.
|
# content-type message/rfc822.
|
||||||
@@ -192,19 +198,19 @@ class MimeParser(Parser):
|
|||||||
text = firstbodyline + '\n' + text
|
text = firstbodyline + '\n' + text
|
||||||
container.set_payload(text)
|
container.set_payload(text)
|
||||||
|
|
||||||
def unquote(str):
|
def unquote(s):
|
||||||
"""Remove quotes from a string."""
|
"""Remove quotes from a string."""
|
||||||
if len(str) > 1:
|
if len(s) > 1:
|
||||||
if str.startswith('"'):
|
if s.startswith('"'):
|
||||||
if str.endswith('"'):
|
if s.endswith('"'):
|
||||||
str = str[1:-1]
|
s = s[1:-1]
|
||||||
else: # remove garbage after trailing quote
|
else: # remove garbage after trailing quote
|
||||||
try: str = str[1:str[1:].index('"')+1]
|
try: s = s[1:s[1:].index('"')+1]
|
||||||
except: return str
|
except: return s
|
||||||
return str.replace('\\\\', '\\').replace('\\"', '"')
|
return s.replace('\\\\', '\\').replace('\\"', '"')
|
||||||
if str.startswith('<') and str.endswith('>'):
|
if s.startswith('<') and s.endswith('>'):
|
||||||
return str[1:-1]
|
return s[1:-1]
|
||||||
return str
|
return s
|
||||||
|
|
||||||
from types import TupleType
|
from types import TupleType
|
||||||
|
|
||||||
@@ -216,21 +222,21 @@ def _unquotevalue(value):
|
|||||||
|
|
||||||
email.Message._unquotevalue = _unquotevalue
|
email.Message._unquotevalue = _unquotevalue
|
||||||
|
|
||||||
def _parseparam(str):
|
def _parseparam(s):
|
||||||
plist = []
|
plist = []
|
||||||
while str[:1] == ';':
|
while s[:1] == ';':
|
||||||
str = str[1:]
|
s = s[1:]
|
||||||
end = str.find(';')
|
end = s.find(';')
|
||||||
while end > 0 and (str.count('"',0,end) & 1):
|
while end > 0 and (s.count('"',0,end) & 1):
|
||||||
end = str.find(';',end + 1)
|
end = s.find(';',end + 1)
|
||||||
if end < 0: end = len(str)
|
if end < 0: end = len(s)
|
||||||
f = str[:end]
|
f = s[:end]
|
||||||
if '=' in f:
|
if '=' in f:
|
||||||
i = f.index('=')
|
i = f.index('=')
|
||||||
f = f[:i].strip().lower() + \
|
f = f[:i].strip().lower() + \
|
||||||
'=' + f[i+1:].strip()
|
'=' + f[i+1:].strip()
|
||||||
plist.append(f.strip())
|
plist.append(f.strip())
|
||||||
str = str[end:]
|
s = s[end:]
|
||||||
return plist
|
return plist
|
||||||
|
|
||||||
# Enhance email.Message
|
# Enhance email.Message
|
||||||
@@ -350,9 +356,9 @@ class MimeMessage(Message):
|
|||||||
return self.get('content-transfer-encoding',None)
|
return self.get('content-transfer-encoding',None)
|
||||||
|
|
||||||
# Decode body to stream according to transfer encoding, return encoding name
|
# Decode body to stream according to transfer encoding, return encoding name
|
||||||
def decode(self,filter):
|
def decode(self,filt):
|
||||||
try:
|
try:
|
||||||
filter.write(self.get_payload(decode=True))
|
filt.write(self.get_payload(decode=True))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return self.getencoding()
|
return self.getencoding()
|
||||||
@@ -363,7 +369,7 @@ class MimeMessage(Message):
|
|||||||
def __setitem__(self, name, value):
|
def __setitem__(self, name, value):
|
||||||
rc = Message.__setitem__(self,name,value)
|
rc = Message.__setitem__(self,name,value)
|
||||||
self.modified = True
|
self.modified = True
|
||||||
if self.headerchange: self.headerchange(self,name,value)
|
if self.headerchange: self.headerchange(self,name,str(value))
|
||||||
return rc
|
return rc
|
||||||
|
|
||||||
def __delitem__(self, name):
|
def __delitem__(self, name):
|
||||||
@@ -423,7 +429,7 @@ See your administrator.
|
|||||||
|
|
||||||
def check_name(msg,savname=None,ckname=check_ext):
|
def check_name(msg,savname=None,ckname=check_ext):
|
||||||
"Replace attachment with a warning if its name is suspicious."
|
"Replace attachment with a warning if its name is suspicious."
|
||||||
for (key,name) in msg.getnames():
|
for key,name in msg.getnames():
|
||||||
badname = ckname(name)
|
badname = ckname(name)
|
||||||
if badname:
|
if badname:
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
@@ -582,14 +588,14 @@ def check_html(msg,savname=None):
|
|||||||
msgtype = 'text/html'
|
msgtype = 'text/html'
|
||||||
if msgtype == 'text/html':
|
if msgtype == 'text/html':
|
||||||
out = StringIO.StringIO()
|
out = StringIO.StringIO()
|
||||||
filter = HTMLScriptFilter(out)
|
htmlfilter = HTMLScriptFilter(out)
|
||||||
try:
|
try:
|
||||||
filter.write(msg.get_payload(decode=True))
|
htmlfilter.write(msg.get_payload(decode=True))
|
||||||
filter.close()
|
htmlfilter.close()
|
||||||
#except sgmllib.SGMLParseError:
|
#except sgmllib.SGMLParseError:
|
||||||
except:
|
except:
|
||||||
#mimetools.copyliteral(msg.get_payload(),open('debug.out','w')
|
#mimetools.copyliteral(msg.get_payload(),open('debug.out','w')
|
||||||
filter.close()
|
htmlfilter.close()
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
msg.set_payload(
|
msg.set_payload(
|
||||||
"An HTML attachment could not be parsed. The original is saved as '%s:%s'"
|
"An HTML attachment could not be parsed. The original is saved as '%s:%s'"
|
||||||
@@ -600,7 +606,7 @@ def check_html(msg,savname=None):
|
|||||||
name = "WARNING.TXT"
|
name = "WARNING.TXT"
|
||||||
msg["Content-Type"] = "text/plain; name="+name
|
msg["Content-Type"] = "text/plain; name="+name
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
if filter.modified:
|
if htmlfilter.modified:
|
||||||
msg.set_payload(out) # remove embedded scripts
|
msg.set_payload(out) # remove embedded scripts
|
||||||
del msg["content-transfer-encoding"]
|
del msg["content-transfer-encoding"]
|
||||||
email.Encoders.encode_quopri(msg)
|
email.Encoders.encode_quopri(msg)
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Analyze milter log to find abusers
|
||||||
|
|
||||||
|
fp = open('/var/log/milter/milter.log','r')
|
||||||
|
subdict = {}
|
||||||
|
ipdict = {}
|
||||||
|
spamcnt = {}
|
||||||
|
for line in fp:
|
||||||
|
a = line.split(None,4)
|
||||||
|
if len(a) < 4: continue
|
||||||
|
dt,tm,id,op = a[:4]
|
||||||
|
if op == 'Subject:':
|
||||||
|
if len(a) > 4: subdict[id] = a[4].rstrip()
|
||||||
|
elif op == 'connect':
|
||||||
|
ipdict[id] = a[4].rstrip()
|
||||||
|
elif op in ('eom','dspam'):
|
||||||
|
if id in subdict: del subdict[id]
|
||||||
|
if id in ipdict: del ipdict[id]
|
||||||
|
elif op in ('REJECT:','DSPAM:','SPAM:','abort'):
|
||||||
|
if id in subdict:
|
||||||
|
if id in ipdict:
|
||||||
|
ip = ipdict[id]
|
||||||
|
del ipdict[id]
|
||||||
|
f,host,raw = ip.split(None,2)
|
||||||
|
if host in spamcnt:
|
||||||
|
spamcnt[host] += 1
|
||||||
|
else:
|
||||||
|
spamcnt[host] = 1
|
||||||
|
else: ip = ''
|
||||||
|
print dt,tm,op,a[4].rstrip(),subdict[id]
|
||||||
|
del subdict[id]
|
||||||
|
else:
|
||||||
|
print line.rstrip()
|
||||||
|
print len(subdict),'leftover entries'
|
||||||
|
|
||||||
|
spamlist = filter(lambda x: x[1] > 1,spamcnt.items())
|
||||||
|
spamlist.sort(lambda x,y: x[1] - y[1])
|
||||||
|
for ip,cnt in spamlist:
|
||||||
|
print cnt,ip
|
||||||
@@ -12,7 +12,7 @@ if sys.version < '2.2.3':
|
|||||||
DistributionMetadata.classifiers = None
|
DistributionMetadata.classifiers = None
|
||||||
DistributionMetadata.download_url = None
|
DistributionMetadata.download_url = None
|
||||||
|
|
||||||
setup(name = "milter", version = "0.6.9",
|
setup(name = "milter", version = "0.7.0",
|
||||||
description="Python interface to sendmail milter API",
|
description="Python interface to sendmail milter API",
|
||||||
long_description="""\
|
long_description="""\
|
||||||
This is a python extension module to enable python scripts to
|
This is a python extension module to enable python scripts to
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ For news, bugfixes, etc. visit the home page for this implementation at
|
|||||||
# Terrence is not responding to email.
|
# Terrence is not responding to email.
|
||||||
#
|
#
|
||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.13 2004/07/23 19:23:12 stuart
|
||||||
|
# Always fail to match on ip6, until we support it properly.
|
||||||
|
#
|
||||||
|
# Revision 1.12 2004/07/23 18:48:15 stuart
|
||||||
|
# Fold CID parsing into spf
|
||||||
|
#
|
||||||
|
# Revision 1.11 2004/07/21 21:32:01 stuart
|
||||||
|
# Handle CID records (Microsoft XML format).
|
||||||
|
#
|
||||||
# Revision 1.10 2004/04/19 22:12:11 stuart
|
# Revision 1.10 2004/04/19 22:12:11 stuart
|
||||||
# Release 0.6.9
|
# Release 0.6.9
|
||||||
#
|
#
|
||||||
@@ -97,6 +106,144 @@ import struct # for pack() and unpack()
|
|||||||
import time # for time()
|
import time # for time()
|
||||||
|
|
||||||
import DNS # http://pydns.sourceforge.net
|
import DNS # http://pydns.sourceforge.net
|
||||||
|
import xml.sax
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Convert a MS Caller-ID entry (XML) to a SPF entry
|
||||||
|
#
|
||||||
|
# (c) 2004 by Ernesto Baschny
|
||||||
|
# (c) 2004 Python version by Stuart Gathman
|
||||||
|
#
|
||||||
|
# Date: 2004-02-25
|
||||||
|
# Version: 1.0
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./cid2spf.pl "<ep xmlns='http://ms.net/1'>...</ep>"
|
||||||
|
#
|
||||||
|
# Note that the 'include' directives will also have to be checked and
|
||||||
|
# "translated". Future versions of this script might be able to get a
|
||||||
|
# domain name as an argument and "crawl" the DNS for the necessary
|
||||||
|
# information.
|
||||||
|
#
|
||||||
|
# A complete reverse translation (SPF -> CID) might be impossible, since
|
||||||
|
# there are no way to handle:
|
||||||
|
# - PTR and EXISTS mechanism
|
||||||
|
# - MX mechanism with an different domain as argument
|
||||||
|
# - macros
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# http://www.microsoft.com/mscorp/twc/privacy/spam_callerid.mspx
|
||||||
|
# http://spf.pobox.com/
|
||||||
|
#
|
||||||
|
# Known bugs:
|
||||||
|
# - Currently it won't handle the exclusions provided in the A and R
|
||||||
|
# tags (prefix '!'). They will show up "as-is" in the SPF record
|
||||||
|
# - I really haven't read the MS-CID specs in-depth, so there are probably
|
||||||
|
# other bugs too :)
|
||||||
|
#
|
||||||
|
# Ernesto Baschny <ernst@baschny.de>
|
||||||
|
#
|
||||||
|
|
||||||
|
class CIDParser(xml.sax.ContentHandler):
|
||||||
|
"Convert a MS Caller-ID entry (XML) to a SPF entry."
|
||||||
|
|
||||||
|
def __init__(self,q=None):
|
||||||
|
self.spf = []
|
||||||
|
self.action = '-all'
|
||||||
|
self.has_servers = None
|
||||||
|
self.spf_entry = None
|
||||||
|
if q:
|
||||||
|
self.spf_query = q
|
||||||
|
else:
|
||||||
|
self.spf_query = query(i='127.0.0.1', s='localhost', h='unknown')
|
||||||
|
|
||||||
|
def startElement(self,tag,attr):
|
||||||
|
if tag == 'm':
|
||||||
|
if self.has_servers != None and not self.has_servers:
|
||||||
|
raise ValueError(
|
||||||
|
"Declared <noMailServers\> and later <m>, this CID entry is not valid."
|
||||||
|
)
|
||||||
|
self.has_servers = True
|
||||||
|
elif tag == 'noMailServers':
|
||||||
|
if self.has_servers:
|
||||||
|
raise ValueError(
|
||||||
|
"Declared <m> and later <noMailServers\>, this CID entry is not valid."
|
||||||
|
)
|
||||||
|
self.has_servers = False
|
||||||
|
elif tag == 'ep':
|
||||||
|
if attr.has_key('testing') and attr.getValue('testing') == 'true':
|
||||||
|
# A CID with 'testing' found:
|
||||||
|
# From the MS-specs:
|
||||||
|
# "Documents in which such attribute is present with a true
|
||||||
|
# value SHOULD be entirely ignored (one should act as if the
|
||||||
|
# document were absent)"
|
||||||
|
# From the SPF-specs:
|
||||||
|
# "Neutral (?): The SPF client MUST proceed as if a domain did
|
||||||
|
# not publish SPF data."
|
||||||
|
# So we set SPF action to "neutral":
|
||||||
|
self.action = '?all'
|
||||||
|
elif tag == 'mx':
|
||||||
|
# The empty MX-tag, same as SPF's MX-mechanism
|
||||||
|
self.spf.append('mx')
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
def characters(self,text):
|
||||||
|
tag = self.tag
|
||||||
|
# Remove starting and trailing spaces from text:
|
||||||
|
text = text.strip()
|
||||||
|
|
||||||
|
if tag == 'a' or tag == 'r':
|
||||||
|
# The A and R tags from MS-CID are both handled by the
|
||||||
|
# ipv4/6-mechanisms from SPF:
|
||||||
|
if text.find(':') < 0:
|
||||||
|
mechanism = 'ip4'
|
||||||
|
else:
|
||||||
|
mechanism = 'ip6'
|
||||||
|
self.spf.append(mechanism + ':' + text)
|
||||||
|
elif tag == 'indirect':
|
||||||
|
# MS-CID's indirect is "sort of" the include from SPF:
|
||||||
|
# Not really true, because the <indirect> tag from MS-CID also
|
||||||
|
# provides a fallback in case the included domain doesn't provide
|
||||||
|
# _ep-records: The inbound MX-servers of the included domains
|
||||||
|
# are added to the list of allowed outgoing mailservers for the
|
||||||
|
# domain that declared the _ep-record with the <indirect> tag.
|
||||||
|
# In SPF you would use the 'mx:domain' to handle this, but this
|
||||||
|
# wouldn't depend on referred domain having or not SPF-records.
|
||||||
|
cid_xml = self.cid_txt(text)
|
||||||
|
if cid_xml:
|
||||||
|
p = CIDParser()
|
||||||
|
xml.sax.parseString(cid_xml,p)
|
||||||
|
if p.has_servers != False:
|
||||||
|
self.spf += p.spf
|
||||||
|
else:
|
||||||
|
self.spf.append('mx:' + text)
|
||||||
|
|
||||||
|
def cid_txt(self,domain):
|
||||||
|
q = self.spf_query
|
||||||
|
domain='_ep.' + domain
|
||||||
|
a = q.dns_txt(domain)
|
||||||
|
if not a: return None
|
||||||
|
if a[0].lower().startswith('<ep ') and a[-1].lower().endswith('</ep>'):
|
||||||
|
return ''.join(a)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def endElement(self,tag):
|
||||||
|
if tag == 'ep':
|
||||||
|
# This is the end... assemble what we've got
|
||||||
|
spf_entry = ['v=spf1']
|
||||||
|
if self.has_servers != False:
|
||||||
|
spf_entry += self.spf
|
||||||
|
spf_entry.append(self.action)
|
||||||
|
self.spf_entry = ' '.join(spf_entry)
|
||||||
|
|
||||||
|
def spf_txt(self,cid_xml):
|
||||||
|
if not cid_xml.startswith('<'):
|
||||||
|
cid_xml = self.cid_txt(cid_xml)
|
||||||
|
if not cid_xml: return None
|
||||||
|
# Parse the beast. Any XML-problem will be reported by xlm.sax
|
||||||
|
self.spf_entry = None
|
||||||
|
xml.sax.parseString(cid_xml,self)
|
||||||
|
return self.spf_entry
|
||||||
|
|
||||||
# 32-bit IPv4 address mask
|
# 32-bit IPv4 address mask
|
||||||
MASK = 0xFFFFFFFFL
|
MASK = 0xFFFFFFFFL
|
||||||
@@ -330,9 +477,14 @@ class query(object):
|
|||||||
cidrlength):
|
cidrlength):
|
||||||
break
|
break
|
||||||
|
|
||||||
elif m in ('ip4', 'ipv4') and arg != self.d:
|
elif m in ('ip4', 'ipv4', 'ip') and arg != self.d:
|
||||||
if cidrmatch(self.i, [arg], cidrlength):
|
if cidrmatch(self.i, [arg], cidrlength):
|
||||||
break
|
break
|
||||||
|
elif m == 'ip6':
|
||||||
|
# Until we support IPV6, we should never
|
||||||
|
# get an IPv6 connection. So this mech
|
||||||
|
# will never match.
|
||||||
|
pass
|
||||||
|
|
||||||
elif m in ('ptr', 'prt'):
|
elif m in ('ptr', 'prt'):
|
||||||
if domainmatch(self.validated_ptrs(self.i),
|
if domainmatch(self.validated_ptrs(self.i),
|
||||||
@@ -465,17 +617,24 @@ class query(object):
|
|||||||
is found.
|
is found.
|
||||||
"""
|
"""
|
||||||
a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')]
|
a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')]
|
||||||
if not a and DELEGATE:
|
if not a:
|
||||||
a = [t
|
if DELEGATE:
|
||||||
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
|
a = [t
|
||||||
if t.startswith('v=spf1')
|
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
|
||||||
]
|
if t.startswith('v=spf1')
|
||||||
|
]
|
||||||
|
if not a:
|
||||||
|
# No SPF record: convert and return CID if present
|
||||||
|
p = CIDParser(q=self)
|
||||||
|
return p.spf_txt(domain)
|
||||||
|
|
||||||
if len(a) == 1:
|
if len(a) == 1:
|
||||||
return a[0]
|
return a[0]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def dns_txt(self, domainname):
|
def dns_txt(self, domainname):
|
||||||
|
"Get a list of TXT records for a domain name."
|
||||||
if domainname:
|
if domainname:
|
||||||
return [t for a in self.dns(domainname, 'TXT') for t in a]
|
return [t for a in self.dns(domainname, 'TXT') for t in a]
|
||||||
return []
|
return []
|
||||||
|
|||||||
Reference in New Issue
Block a user