Send DSN before adding message to quarantine.

This commit is contained in:
Stuart Gathman
2005-08-17 19:35:28 +00:00
parent 241717b0e2
commit 2a4ab4e87c
3 changed files with 81 additions and 54 deletions
+5 -2
View File
@@ -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:
+68 -47
View File
@@ -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
View File
@@ -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