Send DSN before adding message to quarantine.
This commit is contained in:
+5
-2
@@ -112,8 +112,11 @@ def send_dsn(mailfrom,receiver,msg=None):
|
|||||||
smtp.connect(host)
|
smtp.connect(host)
|
||||||
code,resp = smtp.helo(receiver)
|
code,resp = smtp.helo(receiver)
|
||||||
# some wiley spammers have MX records that resolve to 127.0.0.1
|
# some wiley spammers have MX records that resolve to 127.0.0.1
|
||||||
if resp.split()[0] == receiver:
|
a = resp.split()
|
||||||
return (553,'Fraudulent MX for %s' % domain)
|
if not a:
|
||||||
|
return (553,'MX for %s has no hostname in banner: %s' % (domain,host))
|
||||||
|
if a[0] == receiver:
|
||||||
|
return (553,'Fraudulent MX for %s: %s' % (domain,host))
|
||||||
if not (200 <= code <= 299):
|
if not (200 <= code <= 299):
|
||||||
raise smtplib.SMTPHeloError(code, resp)
|
raise smtplib.SMTPHeloError(code, resp)
|
||||||
if msg:
|
if msg:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#!/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.22 2005/08/11 22:17:58 customdesigned
|
||||||
|
# Consider SMTP AUTH connections internal.
|
||||||
|
#
|
||||||
# Revision 1.21 2005/08/04 21:21:31 customdesigned
|
# Revision 1.21 2005/08/04 21:21:31 customdesigned
|
||||||
# Treat fail like softfail for selected (braindead) domains.
|
# Treat fail like softfail for selected (braindead) domains.
|
||||||
# Treat mail according to extended processing results, but
|
# Treat mail according to extended processing results, but
|
||||||
@@ -726,8 +729,8 @@ class bmsMilter(Milter.Milter):
|
|||||||
q.set_default_explanation(
|
q.set_default_explanation(
|
||||||
'SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s' % (q.s,q.i))
|
'SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s' % (q.s,q.i))
|
||||||
res,code,txt = q.check()
|
res,code,txt = q.check()
|
||||||
if res == 'unknown' and q.perm_error:
|
|
||||||
q.result = res
|
q.result = res
|
||||||
|
if res == 'unknown' and q.perm_error:
|
||||||
self.cbv_needed = q # report SPF syntax error to sender
|
self.cbv_needed = q # report SPF syntax error to sender
|
||||||
res,code,txt = q.perm_error.ext # extended (lax processing) result
|
res,code,txt = q.perm_error.ext # extended (lax processing) result
|
||||||
txt = 'EXT: ' + txt
|
txt = 'EXT: ' + txt
|
||||||
@@ -774,11 +777,9 @@ class bmsMilter(Milter.Milter):
|
|||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if self.mailfrom != '<>':
|
if self.mailfrom != '<>':
|
||||||
q.result = res
|
|
||||||
self.cbv_needed = q
|
self.cbv_needed = q
|
||||||
if res in ('deny', 'fail'):
|
if res in ('deny', 'fail'):
|
||||||
if hres == 'pass' and q.o in spf_accept_fail:
|
if hres == 'pass' and q.o in spf_accept_fail:
|
||||||
q.result = res
|
|
||||||
self.cbv_needed = q
|
self.cbv_needed = q
|
||||||
else:
|
else:
|
||||||
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
|
||||||
@@ -800,7 +801,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if self.mailfrom != '<>':
|
if self.mailfrom != '<>':
|
||||||
q.result = res
|
|
||||||
self.cbv_needed = q
|
self.cbv_needed = q
|
||||||
if res == 'neutral' and q.o in spf_reject_neutral:
|
if res == 'neutral' and q.o in spf_reject_neutral:
|
||||||
self.log('REJECT: SPF neutral for',q.s)
|
self.log('REJECT: SPF neutral for',q.s)
|
||||||
@@ -823,6 +823,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.setreply(str(code),'4.3.0',txt)
|
self.setreply(str(code),'4.3.0',txt)
|
||||||
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
|
||||||
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
|
||||||
@@ -1075,6 +1076,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
# this will give a fast start to stats
|
# this will give a fast start to stats
|
||||||
|
|
||||||
def check_spam(self):
|
def check_spam(self):
|
||||||
|
"return True/False if self.fp, else return Milter.REJECT/TEMPFAIL/etc"
|
||||||
if not dspam_userdir: return False
|
if not dspam_userdir: return False
|
||||||
ds = Dspam.DSpamDirectory(dspam_userdir)
|
ds = Dspam.DSpamDirectory(dspam_userdir)
|
||||||
ds.log = self.log
|
ds.log = self.log
|
||||||
@@ -1094,7 +1096,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
ds.add_spam(sender,txt)
|
ds.add_spam(sender,txt)
|
||||||
txt = None
|
txt = None
|
||||||
self.fp = None
|
self.fp = None
|
||||||
return False
|
return Milter.DISCARD
|
||||||
elif user == 'falsepositive' and self.internal_connection:
|
elif user == 'falsepositive' and self.internal_connection:
|
||||||
sender = dspam_users.get(self.canon_from)
|
sender = dspam_users.get(self.canon_from)
|
||||||
if sender:
|
if sender:
|
||||||
@@ -1111,16 +1113,23 @@ class bmsMilter(Milter.Milter):
|
|||||||
return False
|
return False
|
||||||
if user == 'honeypot' and Dspam.VERSION >= '1.1.9':
|
if user == 'honeypot' and Dspam.VERSION >= '1.1.9':
|
||||||
keep = False # keep honeypot mail
|
keep = False # keep honeypot mail
|
||||||
|
self.fp = None
|
||||||
if len(self.recipients) > 1:
|
if len(self.recipients) > 1:
|
||||||
|
self.log("HONEYPOT:",rcpt,'SCREENED')
|
||||||
|
if self.spf:
|
||||||
|
# check that sender accepts quarantine DSN
|
||||||
|
msg = mime.message_from_file(StringIO.StringIO(txt))
|
||||||
|
rc = self.send_dsn(self.spf,msg,'quarantine.txt')
|
||||||
|
del msg
|
||||||
|
if rc != Milter.CONTINUE:
|
||||||
|
return rc
|
||||||
ds.check_spam(user,txt,self.recipients,quarantine=True,
|
ds.check_spam(user,txt,self.recipients,quarantine=True,
|
||||||
force_result=dspam.DSR_ISSPAM)
|
force_result=dspam.DSR_ISSPAM)
|
||||||
self.log("HONEYPOT:",rcpt,'SCREENED')
|
|
||||||
else:
|
else:
|
||||||
ds.check_spam(user,txt,self.recipients,quarantine=keep,
|
ds.check_spam(user,txt,self.recipients,quarantine=keep,
|
||||||
force_result=dspam.DSR_ISSPAM)
|
force_result=dspam.DSR_ISSPAM)
|
||||||
self.log("HONEYPOT:",rcpt)
|
self.log("HONEYPOT:",rcpt)
|
||||||
self.fp = None
|
return Milter.DISCARD
|
||||||
return False
|
|
||||||
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
|
||||||
@@ -1128,7 +1137,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
# as a false positive.
|
# as a false positive.
|
||||||
self.log("DSPAM:",user,rcpt)
|
self.log("DSPAM:",user,rcpt)
|
||||||
self.fp = None
|
self.fp = None
|
||||||
return False
|
return Milter.DISCARD
|
||||||
self.fp = StringIO.StringIO(txt)
|
self.fp = StringIO.StringIO(txt)
|
||||||
modified = True
|
modified = True
|
||||||
except Exception,x:
|
except Exception,x:
|
||||||
@@ -1143,18 +1152,25 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log("Large message:",len(txt))
|
self.log("Large message:",len(txt))
|
||||||
return False
|
return False
|
||||||
screener = dspam_screener[self.id % len(dspam_screener)]
|
screener = dspam_screener[self.id % len(dspam_screener)]
|
||||||
# FIXME: if screener is 'honeypot', classify with no quarantine.
|
|
||||||
# If spam, send DSN and reject if not accepted. Otherwise, use
|
|
||||||
# force_result to quarantine.
|
|
||||||
if not ds.check_spam(screener,txt,self.recipients,
|
if not ds.check_spam(screener,txt,self.recipients,
|
||||||
classify=True,quarantine=not self.reject_spam):
|
classify=True,quarantine=False):
|
||||||
self.fp = None
|
self.fp = None
|
||||||
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)
|
||||||
self.setreply('550','5.7.1','Your Message looks spammy')
|
self.setreply('550','5.7.1','Your Message looks spammy')
|
||||||
return True
|
return Milter.REJECT
|
||||||
self.log("DSPAM:",screener,"SCREENED")
|
self.log("DSPAM:",screener,"SCREENED")
|
||||||
|
if self.spf:
|
||||||
|
# check that sender accepts quarantine DSN
|
||||||
|
msg = mime.message_from_file(StringIO.StringIO(txt))
|
||||||
|
rc = self.send_dsn(self.spf,msg,'quarantine.txt')
|
||||||
|
del msg
|
||||||
|
if rc != Milter.CONTINUE:
|
||||||
|
return rc
|
||||||
|
ds.check_spam(screener,txt,self.recipients,quarantine=True,
|
||||||
|
force_result=dspam.DSR_ISSPAM)
|
||||||
|
return Milter.DISCARD
|
||||||
return modified
|
return modified
|
||||||
|
|
||||||
def eom(self):
|
def eom(self):
|
||||||
@@ -1165,8 +1181,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
# analyze external mail for spam
|
# analyze external mail for spam
|
||||||
spam_checked = self.check_spam() # tag or quarantine for spam
|
spam_checked = self.check_spam() # tag or quarantine for spam
|
||||||
if not self.fp:
|
if not self.fp:
|
||||||
if spam_checked: return Milter.REJECT
|
return spam_checked
|
||||||
return Milter.DISCARD # message quarantined for all recipients
|
|
||||||
|
|
||||||
# analyze all mail for dangerous attachments and scripts
|
# analyze all mail for dangerous attachments and scripts
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
@@ -1233,41 +1248,15 @@ class bmsMilter(Milter.Milter):
|
|||||||
|
|
||||||
if self.cbv_needed:
|
if self.cbv_needed:
|
||||||
q = self.cbv_needed
|
q = self.cbv_needed
|
||||||
sender = q.s
|
|
||||||
cached = cbv_cache.has_key(sender)
|
|
||||||
if cached:
|
|
||||||
self.log('CBV:',sender,'(cached)')
|
|
||||||
res = cbv_cache[sender]
|
|
||||||
else:
|
|
||||||
self.log('CBV:',sender)
|
|
||||||
try:
|
|
||||||
if q.result in ('softfail','fail','deny'):
|
if q.result in ('softfail','fail','deny'):
|
||||||
template = file('softfail.txt').read()
|
template_name = 'softfail.txt'
|
||||||
elif q.result == 'unknown':
|
elif q.result == 'unknown':
|
||||||
template = file('permerror.txt').read()
|
template_name = 'permerror.txt'
|
||||||
else:
|
else:
|
||||||
template = file('strike3.txt').read()
|
template_name = 'strike3.txt'
|
||||||
except IOError: template = None
|
rc = self.send_dsn(q,msg,template_name)
|
||||||
m = dsn.create_msg(q,self.recipients,msg,template)
|
|
||||||
m = m.as_string()
|
|
||||||
print >>open('last_dsn','w'),m
|
|
||||||
res = dsn.send_dsn(sender,self.receiver,m)
|
|
||||||
if res:
|
|
||||||
desc = "CBV: %d %s" % res[:2]
|
|
||||||
if 400 <= res[0] < 500:
|
|
||||||
self.log('TEMPFAIL:',desc)
|
|
||||||
self.setreply('450','4.2.0',*desc.splitlines())
|
|
||||||
return Milter.TEMPFAIL
|
|
||||||
if len(res) < 3: res += time.time(),
|
|
||||||
cbv_cache[sender] = res
|
|
||||||
self.log('REJECT:',desc)
|
|
||||||
self.setreply('550','5.7.1',*desc.splitlines())
|
|
||||||
return Milter.REJECT
|
|
||||||
cbv_cache[sender] = res
|
|
||||||
if not cached:
|
|
||||||
s = time.strftime(time_format,time.localtime())
|
|
||||||
print >>open('send_dsn.log','a'),sender,s # log who we sent DSNs to
|
|
||||||
self.cbv_needed = None
|
self.cbv_needed = None
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
|
||||||
if not defanged and not spam_checked:
|
if not defanged and not spam_checked:
|
||||||
os.remove(self.tempname)
|
os.remove(self.tempname)
|
||||||
@@ -1301,6 +1290,38 @@ class bmsMilter(Milter.Milter):
|
|||||||
out.close()
|
out.close()
|
||||||
return Milter.TEMPFAIL
|
return Milter.TEMPFAIL
|
||||||
|
|
||||||
|
def send_dsn(self,q,msg,template_name):
|
||||||
|
sender = q.s
|
||||||
|
cached = cbv_cache.has_key(sender)
|
||||||
|
if cached:
|
||||||
|
self.log('CBV:',sender,'(cached)')
|
||||||
|
res = cbv_cache[sender]
|
||||||
|
else:
|
||||||
|
self.log('CBV:',sender)
|
||||||
|
try:
|
||||||
|
template = file(template_name).read()
|
||||||
|
except IOError: template = None
|
||||||
|
m = dsn.create_msg(q,self.recipients,msg,template)
|
||||||
|
m = m.as_string()
|
||||||
|
print >>open('last_dsn','w'),m
|
||||||
|
res = dsn.send_dsn(sender,self.receiver,m)
|
||||||
|
if res:
|
||||||
|
desc = "CBV: %d %s" % res[:2]
|
||||||
|
if 400 <= res[0] < 500:
|
||||||
|
self.log('TEMPFAIL:',desc)
|
||||||
|
self.setreply('450','4.2.0',*desc.splitlines())
|
||||||
|
return Milter.TEMPFAIL
|
||||||
|
if len(res) < 3: res += time.time(),
|
||||||
|
cbv_cache[sender] = res
|
||||||
|
self.log('REJECT:',desc)
|
||||||
|
self.setreply('550','5.7.1',*desc.splitlines())
|
||||||
|
return Milter.REJECT
|
||||||
|
cbv_cache[sender] = res
|
||||||
|
if not cached:
|
||||||
|
s = time.strftime(time_format,time.localtime())
|
||||||
|
print >>open('send_dsn.log','a'),sender,s # log who we sent DSNs to
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
sys.stdout.flush() # make log messages visible
|
sys.stdout.flush() # make log messages visible
|
||||||
if self.tempname:
|
if self.tempname:
|
||||||
|
|||||||
+6
-3
@@ -160,9 +160,10 @@ 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/strike3.txt
|
%config(noreplace) /var/log/milter/strike3.txt
|
||||||
%config /var/log/milter/softfail.txt
|
%config(noreplace) /var/log/milter/softfail.txt
|
||||||
%config /var/log/milter/quarantine.txt
|
%config(noreplace) /var/log/milter/quarantine.txt
|
||||||
|
%config(noreplace) /var/log/milter/permerror.txt
|
||||||
%config(noreplace) /etc/mail/pymilter.cfg
|
%config(noreplace) /etc/mail/pymilter.cfg
|
||||||
/usr/share/sendmail-cf/hack/rhsbl.m4
|
/usr/share/sendmail-cf/hack/rhsbl.m4
|
||||||
|
|
||||||
@@ -171,6 +172,8 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
- 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.
|
||||||
|
- Send DSN for SPF errors corrected by extended processing.
|
||||||
|
- Send DSN before SCREENED mail is quarantined
|
||||||
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-4
|
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-4
|
||||||
- Limit each CNAME chain independently like PTR and MX
|
- Limit each CNAME chain independently like PTR and MX
|
||||||
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-3
|
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-3
|
||||||
|
|||||||
Reference in New Issue
Block a user