Skip to content
Snippets Groups Projects
Commit 2a4ab4e8 authored by Stuart Gathman's avatar Stuart Gathman
Browse files

Send DSN before adding message to quarantine.

parent 241717b0
No related branches found
No related tags found
No related merge requests found
...@@ -112,8 +112,11 @@ def send_dsn(mailfrom,receiver,msg=None): ...@@ -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:
......
#!/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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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:
......
...@@ -160,9 +160,10 @@ rm -rf $RPM_BUILD_ROOT ...@@ -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 ...@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment