Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65c73f61c2 |
@@ -5,11 +5,8 @@ wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
|||||||
it with distutils, and generally transformed it from a quick hack to a
|
it with distutils, and generally transformed it from a quick hack to a
|
||||||
real, usable Python extension.
|
real, usable Python extension.
|
||||||
|
|
||||||
Other contributors (in random order):
|
Other contributors:
|
||||||
|
|
||||||
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
|
Terence Way
|
||||||
for providing a Python port of SPF
|
for providing a Python port of SPF
|
||||||
Scott Kitterman
|
Scott Kitterman
|
||||||
|
|||||||
+4
-4
@@ -16,7 +16,7 @@ from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
|
|||||||
try: from milter import QUARANTINE
|
try: from milter import QUARANTINE
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
__version__ = '0.8.4'
|
__version__ = '0.8.3'
|
||||||
|
|
||||||
_seq_lock = thread.allocate_lock()
|
_seq_lock = thread.allocate_lock()
|
||||||
_seq = 0
|
_seq = 0
|
||||||
@@ -44,7 +44,7 @@ class Milter:
|
|||||||
for i in msg: print i,
|
for i in msg: print i,
|
||||||
print
|
print
|
||||||
|
|
||||||
def connect(self,hostname,family,hostaddr):
|
def connect(self,hostname,unused,hostaddr):
|
||||||
"Called for each connection to sendmail."
|
"Called for each connection to sendmail."
|
||||||
self.log("connect from %s at %s" % (hostname,hostaddr))
|
self.log("connect from %s at %s" % (hostname,hostaddr))
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
@@ -106,8 +106,8 @@ class Milter:
|
|||||||
return self.__ctx.setreply(rcode,xcode,msg,*ml)
|
return self.__ctx.setreply(rcode,xcode,msg,*ml)
|
||||||
|
|
||||||
# Milter methods which can only be called from eom callback.
|
# Milter methods which can only be called from eom callback.
|
||||||
def addheader(self,field,value,idx=-1):
|
def addheader(self,field,value):
|
||||||
return self.__ctx.addheader(field,value,idx)
|
return self.__ctx.addheader(field,value)
|
||||||
|
|
||||||
def chgheader(self,field,idx,value):
|
def chgheader(self,field,idx,value):
|
||||||
return self.__ctx.chgheader(field,idx,value)
|
return self.__ctx.chgheader(field,idx,value)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
Here is a history of user visible changes to Python milter.
|
Here is a history of user visible changes to Python milter.
|
||||||
|
|
||||||
0.8.4 Auto-whitelist recipients of outgoing email.
|
|
||||||
0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
|
0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
|
||||||
spf_accept_fail option for braindead SPF senders
|
spf_accept_fail option for braindead SPF senders
|
||||||
(treats fail like softfail)
|
(treats fail like softfail)
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
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.
|
Find rfc2822 policy for MFROM quoting.
|
||||||
|
|
||||||
Support explicit errors for SPF policy in access file:
|
Use /etc/mail/access for domain specific SPF policies.
|
||||||
|
|
||||||
|
SPF-Fail: REJECT
|
||||||
|
SPF-Softfail: OK
|
||||||
|
SPF-Neutral: OK
|
||||||
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
|
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
|
||||||
|
|
||||||
Defer TEMPERROR in SPF evaluation - give precedence to security
|
Defer TEMPERROR in SPF evaluation - give precedence to security
|
||||||
@@ -19,6 +16,9 @@ Option to add Received-SPF header, but never reject on SPF.
|
|||||||
Create null config that does nothing - except maybe add Received-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.
|
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.
|
Can't output messages with malformed rfc822 attachments.
|
||||||
|
|
||||||
Move milter,Milter,mime,spf modules to pymilter
|
Move milter,Milter,mime,spf modules to pymilter
|
||||||
@@ -33,8 +33,7 @@ check spam keywords with character classes, e.g.
|
|||||||
|
|
||||||
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
|
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
|
||||||
forwarder accounts, and a util provides a special local alias for the
|
forwarder accounts, and a util provides a special local alias for the
|
||||||
user to give to the forwarder. (Or user just adds arbitrary alias
|
user to give to the forwarder. Alias only works for mail from that
|
||||||
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
|
forwarder. Milter gets forwarder domain from alias and uses it to
|
||||||
SPF check forwarder.
|
SPF check forwarder.
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# A simple milter that has grown quite a bit.
|
# A simple milter that has grown quite a bit.
|
||||||
# $Log$
|
# $Log$
|
||||||
# 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
|
# Revision 1.29 2005/10/11 22:50:07 customdesigned
|
||||||
# Always check HELO except for SPF pass, temperror.
|
# Always check HELO except for SPF pass, temperror.
|
||||||
#
|
#
|
||||||
@@ -352,7 +337,6 @@ dspam_users = {}
|
|||||||
dspam_userdir = None
|
dspam_userdir = None
|
||||||
dspam_exempt = {}
|
dspam_exempt = {}
|
||||||
dspam_whitelist = {}
|
dspam_whitelist = {}
|
||||||
whitelist_senders = {}
|
|
||||||
dspam_screener = ()
|
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 = ()
|
||||||
@@ -368,8 +352,9 @@ spf_best_guess = False
|
|||||||
spf_reject_noptr = False
|
spf_reject_noptr = False
|
||||||
supply_sender = False
|
supply_sender = False
|
||||||
access_file = None
|
access_file = None
|
||||||
|
time_format = '%Y%b%d %H:%M:%S %Z'
|
||||||
timeout = 600
|
timeout = 600
|
||||||
|
cbv_cache = {}
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
stream=sys.stdout,
|
stream=sys.stdout,
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -378,6 +363,19 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
milter_log = logging.getLogger('milter')
|
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
|
||||||
|
|
||||||
class MilterConfigParser(ConfigParser.ConfigParser):
|
class MilterConfigParser(ConfigParser.ConfigParser):
|
||||||
|
|
||||||
def getlist(self,sect,opt):
|
def getlist(self,sect,opt):
|
||||||
@@ -501,8 +499,6 @@ def read_config(list):
|
|||||||
# dspam section
|
# dspam section
|
||||||
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt, dspam_internal
|
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt, dspam_internal
|
||||||
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
|
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_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')
|
||||||
@@ -618,7 +614,7 @@ class SPFPolicy(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def getFailPolicy(self):
|
def getFailPolicy(self):
|
||||||
policy = self.getPolicy('spf-fail:')
|
policy = self.getPolicy('SPF-Fail:')
|
||||||
if not policy:
|
if not policy:
|
||||||
if self.domain in spf_accept_fail:
|
if self.domain in spf_accept_fail:
|
||||||
policy = 'CBV'
|
policy = 'CBV'
|
||||||
@@ -627,7 +623,7 @@ class SPFPolicy(object):
|
|||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getNonePolicy(self):
|
def getNonePolicy(self):
|
||||||
policy = self.getPolicy('spf-none:')
|
policy = self.getPolicy('SPF-None:')
|
||||||
if not policy:
|
if not policy:
|
||||||
if spf_reject_noptr:
|
if spf_reject_noptr:
|
||||||
policy = 'REJECT'
|
policy = 'REJECT'
|
||||||
@@ -636,7 +632,7 @@ class SPFPolicy(object):
|
|||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getSoftfailPolicy(self):
|
def getSoftfailPolicy(self):
|
||||||
policy = self.getPolicy('spf-softfail:')
|
policy = self.getPolicy('SPF-Softfail:')
|
||||||
if not policy:
|
if not policy:
|
||||||
if self.domain in spf_accept_softfail:
|
if self.domain in spf_accept_softfail:
|
||||||
policy = 'OK'
|
policy = 'OK'
|
||||||
@@ -647,7 +643,7 @@ class SPFPolicy(object):
|
|||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getNeutralPolicy(self):
|
def getNeutralPolicy(self):
|
||||||
policy = self.getPolicy('spf-neutral:')
|
policy = self.getPolicy('SPF-Neutral:')
|
||||||
if not policy:
|
if not policy:
|
||||||
if self.domain in spf_reject_neutral:
|
if self.domain in spf_reject_neutral:
|
||||||
policy = 'REJECT'
|
policy = 'REJECT'
|
||||||
@@ -655,59 +651,17 @@ class SPFPolicy(object):
|
|||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getPermErrorPolicy(self):
|
def getPermErrorPolicy(self):
|
||||||
policy = self.getPolicy('spf-permerror:')
|
policy = self.getPolicy('SPF-PermError:')
|
||||||
if not policy:
|
if not policy:
|
||||||
policy = 'REJECT'
|
policy = 'REJECT'
|
||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getPassPolicy(self):
|
def getPassPolicy(self):
|
||||||
policy = self.getPolicy('spf-pass:')
|
policy = self.getPolicy('SPF-Pass:')
|
||||||
if not policy:
|
if not policy:
|
||||||
policy = 'OK'
|
policy = 'OK'
|
||||||
return policy
|
return policy
|
||||||
|
|
||||||
class AddrCache(object):
|
|
||||||
time_format = '%Y%b%d %H:%M:%S %Z'
|
|
||||||
|
|
||||||
def load(self,fname,age=7):
|
|
||||||
self.fname = fname
|
|
||||||
cache = {}
|
|
||||||
self.cache = cache
|
|
||||||
try:
|
|
||||||
too_old = time.time() - 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()] = None
|
|
||||||
except:
|
|
||||||
cache[ln.strip().lower()] = None
|
|
||||||
except IOError: pass
|
|
||||||
|
|
||||||
def has_key(self,sender):
|
|
||||||
return self.cache.has_key(sender.lower())
|
|
||||||
|
|
||||||
def __getitem__(self,sender):
|
|
||||||
return self.cache[sender.lower()]
|
|
||||||
|
|
||||||
def __setitem__(self,sender,res):
|
|
||||||
lsender = sender.lower()
|
|
||||||
cached = lsender in self.cache
|
|
||||||
self.cache[lsender] = res
|
|
||||||
if not cached and not res:
|
|
||||||
s = time.strftime(AddrCache.time_format,time.localtime())
|
|
||||||
print >>open(self.fname,'a'),sender,s # log who we sent DSNs to
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.cache)
|
|
||||||
|
|
||||||
cbv_cache = AddrCache()
|
|
||||||
cbv_cache.load('send_dsn.log',age=7)
|
|
||||||
auto_whitelist = AddrCache()
|
|
||||||
auto_whitelist.load('auto_whitelist.log',age=30)
|
|
||||||
|
|
||||||
class bmsMilter(Milter.Milter):
|
class bmsMilter(Milter.Milter):
|
||||||
"""Milter to replace attachments poisonous to Windows with a WARNING message,
|
"""Milter to replace attachments poisonous to Windows with a WARNING message,
|
||||||
check SPF, and other anti-forgery features, and implement wiretapping
|
check SPF, and other anti-forgery features, and implement wiretapping
|
||||||
@@ -824,7 +778,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.hidepath = False
|
self.hidepath = False
|
||||||
self.discard = False
|
self.discard = False
|
||||||
self.dspam = True
|
self.dspam = True
|
||||||
self.whitelist = False
|
|
||||||
self.reject_spam = True
|
self.reject_spam = True
|
||||||
self.data_allowed = True
|
self.data_allowed = True
|
||||||
self.trust_received = self.trusted_relay
|
self.trust_received = self.trusted_relay
|
||||||
@@ -834,7 +787,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.new_headers = []
|
self.new_headers = []
|
||||||
self.recipients = []
|
self.recipients = []
|
||||||
self.cbv_needed = None
|
self.cbv_needed = None
|
||||||
self.whitelist_sender = False
|
|
||||||
t = parse_addr(f)
|
t = parse_addr(f)
|
||||||
if len(t) == 2: t[1] = t[1].lower()
|
if len(t) == 2: t[1] = t[1].lower()
|
||||||
self.canon_from = '@'.join(t)
|
self.canon_from = '@'.join(t)
|
||||||
@@ -879,22 +831,17 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log("REJECT: spam from self",pat)
|
self.log("REJECT: spam from self",pat)
|
||||||
self.setreply('550','5.7.1','I hate talking to myself.')
|
self.setreply('550','5.7.1','I hate talking to myself.')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
else:
|
elif internal_domains:
|
||||||
if internal_domains:
|
|
||||||
for pat in internal_domains:
|
for pat in internal_domains:
|
||||||
if fnmatchcase(domain,pat): break
|
if fnmatchcase(domain,pat): break
|
||||||
else:
|
else:
|
||||||
self.log("REJECT: zombie PC at ",self.connectip,
|
self.log("REJECT: zombie PC at ",self.connectip," sending MAIL FROM ",
|
||||||
" sending MAIL FROM ",self.canon_from)
|
self.canon_from)
|
||||||
self.setreply('550','5.7.1',
|
self.setreply('550','5.7.1',
|
||||||
'Your PC is using an unauthorized MAIL FROM.',
|
'Your PC is using an unauthorized MAIL FROM.',
|
||||||
'It is either badly misconfigured or controlled by organized crime.'
|
'It is either badly misconfigured or controlled by organized crime.'
|
||||||
)
|
)
|
||||||
return Milter.REJECT
|
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
|
self.rejectvirus = domain in reject_virus_from
|
||||||
if user in wiretap_users.get(domain,()):
|
if user in wiretap_users.get(domain,()):
|
||||||
self.add_recipient(wiretap_dest)
|
self.add_recipient(wiretap_dest)
|
||||||
@@ -1040,9 +987,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
return Milter.TEMPFAIL
|
return Milter.TEMPFAIL
|
||||||
self.add_header('Received-SPF',q.get_header(res,receiver))
|
self.add_header('Received-SPF',q.get_header(res,receiver))
|
||||||
self.spf = q
|
self.spf = q
|
||||||
if res == 'pass' and auto_whitelist.has_key(self.canon_from):
|
|
||||||
self.whitelist = True
|
|
||||||
self.log("WHITELIST",self.canon_from)
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
# hide_path causes a copy of the message to be saved - until we
|
# hide_path causes a copy of the message to be saved - until we
|
||||||
@@ -1054,9 +998,8 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log('DISCARD: RCPT TO:',to,str)
|
self.log('DISCARD: RCPT TO:',to,str)
|
||||||
return Milter.DISCARD
|
return Milter.DISCARD
|
||||||
self.log("rcpt to",to,str)
|
self.log("rcpt to",to,str)
|
||||||
t = parse_addr(to)
|
t = parse_addr(to.lower())
|
||||||
if len(t) == 2:
|
if len(t) == 2:
|
||||||
t[1] = t[1].lower()
|
|
||||||
user,domain = t
|
user,domain = t
|
||||||
if self.is_bounce and srs and domain in srs_domain:
|
if self.is_bounce and srs and domain in srs_domain:
|
||||||
oldaddr = '@'.join(parse_addr(to))
|
oldaddr = '@'.join(parse_addr(to))
|
||||||
@@ -1084,8 +1027,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
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
|
# non DSN mail to SRS address will bounce due to invalid local part
|
||||||
canon_to = '@'.join(t)
|
self.recipients.append('@'.join(t))
|
||||||
self.recipients.append(canon_to)
|
|
||||||
users = check_user.get(domain)
|
users = check_user.get(domain)
|
||||||
if self.discard:
|
if self.discard:
|
||||||
self.del_recipient(to)
|
self.del_recipient(to)
|
||||||
@@ -1101,14 +1043,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.hidepath = True
|
self.hidepath = True
|
||||||
if not domain in dspam_reject:
|
if not domain in dspam_reject:
|
||||||
self.reject_spam = False
|
self.reject_spam = False
|
||||||
if self.internal_connection and self.whitelist_sender:
|
|
||||||
if internal_domains:
|
|
||||||
for pat in internal_domains:
|
|
||||||
if fnmatchcase(domain,pat): break
|
|
||||||
else:
|
|
||||||
auto_whitelist[canon_to] = None
|
|
||||||
else:
|
|
||||||
auto_whitelist[canon_to] = None
|
|
||||||
self.smart_alias(to)
|
self.smart_alias(to)
|
||||||
#rcpt = self.getsymval("{rcpt_addr}")
|
#rcpt = self.getsymval("{rcpt_addr}")
|
||||||
#self.log("rcpt-addr",rcpt);
|
#self.log("rcpt-addr",rcpt);
|
||||||
@@ -1227,7 +1161,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
|
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
|
||||||
self.fp.write("\n") # terminate headers
|
self.fp.write("\n") # terminate headers
|
||||||
# log when neither sender nor from domains matches mail from domain
|
# log when neither sender nor from domains matches mail from domain
|
||||||
if supply_sender and self.mailfrom != '<>' and not self.internal_connection:
|
if supply_sender and self.mailfrom != '<>':
|
||||||
mf_domain = self.canon_from.split('@')[-1]
|
mf_domain = self.canon_from.split('@')[-1]
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
msg = rfc822.Message(self.fp)
|
msg = rfc822.Message(self.fp)
|
||||||
@@ -1260,7 +1194,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
dspam.DSF_CHAINED|dspam.DSF_CLASSIFY)
|
dspam.DSF_CHAINED|dspam.DSF_CLASSIFY)
|
||||||
try:
|
try:
|
||||||
ds.process(headers)
|
ds.process(headers)
|
||||||
if ds.probability > 0.93 and self.dspam and not self.whitelist:
|
if ds.probability > 0.93 and self.dspam:
|
||||||
self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability)
|
self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability)
|
||||||
self.setreply('550','5.7.1','Your Message looks spammy')
|
self.setreply('550','5.7.1','Your Message looks spammy')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
@@ -1367,9 +1301,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.fp = None
|
self.fp = None
|
||||||
if len(self.recipients) > 1:
|
if len(self.recipients) > 1:
|
||||||
self.log("HONEYPOT:",rcpt,'SCREENED')
|
self.log("HONEYPOT:",rcpt,'SCREENED')
|
||||||
if self.whitelist:
|
|
||||||
# don't train when recipients includes honeypot
|
|
||||||
return False
|
|
||||||
if self.spf:
|
if self.spf:
|
||||||
# check that sender accepts quarantine DSN
|
# check that sender accepts quarantine DSN
|
||||||
msg = mime.message_from_file(StringIO.StringIO(txt))
|
msg = mime.message_from_file(StringIO.StringIO(txt))
|
||||||
@@ -1384,12 +1315,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
force_result=dspam.DSR_ISSPAM)
|
force_result=dspam.DSR_ISSPAM)
|
||||||
self.log("HONEYPOT:",rcpt)
|
self.log("HONEYPOT:",rcpt)
|
||||||
return Milter.DISCARD
|
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)
|
txt = ds.check_spam(user,txt,self.recipients)
|
||||||
if not txt:
|
if not txt:
|
||||||
# DISCARD if quarrantined for any recipient. It
|
# DISCARD if quarrantined for any recipient. It
|
||||||
@@ -1414,13 +1339,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
screener = dspam_screener[self.id % len(dspam_screener)]
|
screener = dspam_screener[self.id % len(dspam_screener)]
|
||||||
if not ds.check_spam(screener,txt,self.recipients,
|
if not ds.check_spam(screener,txt,self.recipients,
|
||||||
classify=True,quarantine=False):
|
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:
|
if self.reject_spam:
|
||||||
self.log("DSPAM:",screener,
|
self.log("DSPAM:",screener,
|
||||||
'REJECT: X-DSpam-Score: %f' % ds.probability)
|
'REJECT: X-DSpam-Score: %f' % ds.probability)
|
||||||
@@ -1589,6 +1507,9 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.setreply('550','5.7.1',*desc.splitlines())
|
self.setreply('550','5.7.1',*desc.splitlines())
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
cbv_cache[sender] = res
|
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
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.1 Final//EN">
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Python Milter Log Documentation</title>
|
|
||||||
<style>
|
|
||||||
DT { font-weight: bolder; padding-top: 1em }
|
|
||||||
</style>
|
|
||||||
</head><body>
|
|
||||||
|
|
||||||
<h1> Milter Log Documentation </h1>
|
|
||||||
|
|
||||||
The milter log 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.
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
+3
-9
@@ -8,7 +8,7 @@ tempdir = /var/log/milter/save
|
|||||||
log_headers = 0
|
log_headers = 0
|
||||||
# connection ips and hostnames are matched against this glob style list
|
# connection ips and hostnames are matched against this glob style list
|
||||||
# to recognize internal senders.
|
# to recognize internal senders.
|
||||||
;internal_connect = 192.168.*.*,127.*
|
;internal_connect = 192.168.*.*
|
||||||
|
|
||||||
# mail that is not an internal_connect and claims to be from an
|
# mail that is not an internal_connect and claims to be from an
|
||||||
# internal domain is rejected. Furthermore, internal mail that
|
# internal domain is rejected. Furthermore, internal mail that
|
||||||
@@ -17,7 +17,7 @@ log_headers = 0
|
|||||||
# flexible. However, SPF is not currently checked for outgoing
|
# flexible. However, SPF is not currently checked for outgoing
|
||||||
# (internal_connect) mail because it doesn't yet handle authorizing
|
# (internal_connect) mail because it doesn't yet handle authorizing
|
||||||
# internal IPs locally.
|
# internal IPs locally.
|
||||||
;internal_domains = mycorp.com,localhost.localdomain
|
;internal_domains = mycorp.com
|
||||||
|
|
||||||
# connections from a trusted relay can trust the first Received header
|
# connections from a trusted relay can trust the first Received header
|
||||||
# SPF checks are bypassed for internal connections and trusted relays.
|
# SPF checks are bypassed for internal connections and trusted relays.
|
||||||
@@ -146,13 +146,7 @@ blind = 1
|
|||||||
# only EXTERNAL messages are dspam filtered
|
# only EXTERNAL messages are dspam filtered
|
||||||
;dspam_dict=/var/lib/dspam/moderator.dict
|
;dspam_dict=/var/lib/dspam/moderator.dict
|
||||||
|
|
||||||
# Recipients of mail sent from these senders are added to the auto_whitelist.
|
# Opt-opt recipients from dspam screening and header triage
|
||||||
# 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
|
;dspam_exempt=getitall@mycorp.com
|
||||||
# Do not scan mail (ostensibly) from these senders
|
# Do not scan mail (ostensibly) from these senders
|
||||||
;dspam_whitelist=getitall@sender.com
|
;dspam_whitelist=getitall@sender.com
|
||||||
|
|||||||
+2
-19
@@ -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 Oct 12, 2005</h4>
|
Last updated Aug 28, 2005</h4>
|
||||||
|
|
||||||
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
|
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="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
|
||||||
@@ -47,25 +47,10 @@ efficient and secure. I recommend upgrading.
|
|||||||
|
|
||||||
<h2> Recent Changes </h2>
|
<h2> Recent Changes </h2>
|
||||||
|
|
||||||
Python milter has been moved to
|
Python milter is being moved to
|
||||||
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
|
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
|
||||||
project</a> for development and release downloads.
|
project</a> for development and release downloads.
|
||||||
<p>
|
<p>
|
||||||
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.
|
|
||||||
<p>
|
|
||||||
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
|
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
|
in line with the newly official RFC. It adds
|
||||||
<a href="http://ses.codeshare.ca/">SES</a>
|
<a href="http://ses.codeshare.ca/">SES</a>
|
||||||
@@ -260,8 +245,6 @@ content filtering. SPF checking
|
|||||||
requires <a href="http://pydns.sourceforge.net/">
|
requires <a href="http://pydns.sourceforge.net/">
|
||||||
pydns</a>. Configuration documentation is currently included as comments
|
pydns</a>. Configuration documentation is currently included as comments
|
||||||
in the <a href="milter.cfg">sample config file</a> for the bms.py milter.
|
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>
|
<p>
|
||||||
Python milter is under GPL. The authors can probably be convinced to
|
Python milter is under GPL. The authors can probably be convinced to
|
||||||
change this to LGPL if needed.
|
change this to LGPL if needed.
|
||||||
|
|||||||
+2
-7
@@ -1,5 +1,5 @@
|
|||||||
%define name milter
|
%define name milter
|
||||||
%define version 0.8.4
|
%define version 0.8.3
|
||||||
%define release 1.RH7
|
%define release 1.RH7
|
||||||
# what version of RH are we building for?
|
# what version of RH are we building for?
|
||||||
%define redhat9 0
|
%define redhat9 0
|
||||||
@@ -169,12 +169,7 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
/usr/share/sendmail-cf/hack/rhsbl.m4
|
/usr/share/sendmail-cf/hack/rhsbl.m4
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Thu Oct 20 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-1
|
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-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.
|
- Keep screened honeypot mail, but optionally discard honeypot only mail.
|
||||||
- spf_accept_fail option for braindead SPF senders (treats fail like softfail)
|
- spf_accept_fail option for braindead SPF senders (treats fail like softfail)
|
||||||
- Consider SMTP AUTH connections internal.
|
- Consider SMTP AUTH connections internal.
|
||||||
|
|||||||
+6
-15
@@ -34,12 +34,6 @@ $ python setup.py help
|
|||||||
libraries=["milter","smutil","resolv"]
|
libraries=["milter","smutil","resolv"]
|
||||||
|
|
||||||
* $Log$
|
* $Log$
|
||||||
* 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
|
* Revision 1.5 2005/06/24 04:20:07 customdesigned
|
||||||
* Report context allocation error.
|
* Report context allocation error.
|
||||||
*
|
*
|
||||||
@@ -973,31 +967,28 @@ milter_setreply(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static char milter_addheader__doc__[] =
|
static char milter_addheader__doc__[] =
|
||||||
"addheader(field, value, idx=-1) -> None\n\
|
"addheader(field, value) -> None\n\
|
||||||
Add a header to the message. This header is not passed to other\n\
|
Add a header to the message. This header is not passed to other\n\
|
||||||
filters. It is not checked for standards compliance;\n\
|
filters. It is not checked for standards compliance;\n\
|
||||||
the mail filter must ensure that no protocols are violated\n\
|
the mail filter must ensure that no protocols are violated\n\
|
||||||
as a result of adding this header.\n\
|
as a result of adding this header.\n\
|
||||||
field - header field name\n\
|
field - header field name\n\
|
||||||
value - header field value\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.";
|
Both are strings. This function can only be called from the EOM callback.";
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
milter_addheader(PyObject *self, PyObject *args) {
|
milter_addheader(PyObject *self, PyObject *args) {
|
||||||
char *headerf;
|
char *headerf;
|
||||||
char *headerv;
|
char *headerv;
|
||||||
int idx = -1;
|
|
||||||
SMFICTX *ctx;
|
SMFICTX *ctx;
|
||||||
PyThreadState *t;
|
PyThreadState *t;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "ss|i:addheader", &headerf, &headerv, &idx))
|
if (!PyArg_ParseTuple(args, "ss:addheader", &headerf, &headerv)) return NULL;
|
||||||
return NULL;
|
|
||||||
ctx = _find_context(self);
|
ctx = _find_context(self);
|
||||||
if (ctx == NULL) return NULL;
|
if (ctx == NULL) return NULL;
|
||||||
t = PyEval_SaveThread();
|
t = PyEval_SaveThread();
|
||||||
return _thread_return(t, (idx < 0) ? smfi_addheader(ctx, headerf, headerv) :
|
return _thread_return(t,smfi_addheader(ctx, headerf, headerv),
|
||||||
smfi_insheader(ctx, idx, headerf, headerv), "cannot add header");
|
"cannot add header");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_chgheader__doc__[] =
|
static char milter_chgheader__doc__[] =
|
||||||
@@ -1152,7 +1143,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SMFIR_PROGRESS
|
#if _FFR_SMFI_PROGRESS
|
||||||
static char milter_progress__doc__[] =
|
static char milter_progress__doc__[] =
|
||||||
"progress() -> None\n\
|
"progress() -> None\n\
|
||||||
Notify the MTA that we are working on a message so it will reset timeouts.";
|
Notify the MTA that we are working on a message so it will reset timeouts.";
|
||||||
@@ -1183,7 +1174,7 @@ static PyMethodDef context_methods[] = {
|
|||||||
#ifdef SMFIF_QUARANTINE
|
#ifdef SMFIF_QUARANTINE
|
||||||
{ "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__},
|
{ "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__},
|
||||||
#endif
|
#endif
|
||||||
#ifdef SMFIR_PROGRESS
|
#if _FFR_SMFI_PROGRESS
|
||||||
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
|
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
|
||||||
#endif
|
#endif
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
|
|||||||
+1
-1
@@ -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
|
is no positive confirmation that the message is really from you, we have
|
||||||
to give it extra scrutiny - including verifying that the sender really
|
to give it extra scrutiny - including verifying that the sender really
|
||||||
exists by sending you this DSN. We will remember this sender and not
|
exists by sending you this DSN. We will remember this sender and not
|
||||||
bother you again for a while. You can avoid this message entirely for
|
bother you again for while. You can avoid this message entirely for
|
||||||
legitimate mail by using an authorized SMTP server. Contact your mail
|
legitimate mail by using an authorized SMTP server. Contact your mail
|
||||||
administrator and ask how to configure your email client to use an
|
administrator and ask how to configure your email client to use an
|
||||||
authorized server.
|
authorized server.
|
||||||
|
|||||||
Reference in New Issue
Block a user