diff --git a/Milter/dsn.py b/Milter/dsn.py index 469715c..4cda6c4 100644 --- a/Milter/dsn.py +++ b/Milter/dsn.py @@ -112,8 +112,11 @@ def send_dsn(mailfrom,receiver,msg=None): smtp.connect(host) code,resp = smtp.helo(receiver) # some wiley spammers have MX records that resolve to 127.0.0.1 - if resp.split()[0] == receiver: - return (553,'Fraudulent MX for %s' % domain) + a = resp.split() + 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): raise smtplib.SMTPHeloError(code, resp) if msg: diff --git a/bms.py b/bms.py index 48ffcf1..f3c9b07 100644 --- a/bms.py +++ b/bms.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # A simple milter that has grown quite a bit. # $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 # Treat fail like softfail for selected (braindead) domains. # Treat mail according to extended processing results, but @@ -726,8 +729,8 @@ class bmsMilter(Milter.Milter): q.set_default_explanation( 'SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s' % (q.s,q.i)) res,code,txt = q.check() + q.result = res if res == 'unknown' and q.perm_error: - q.result = res self.cbv_needed = q # report SPF syntax error to sender res,code,txt = q.perm_error.ext # extended (lax processing) result txt = 'EXT: ' + txt @@ -774,11 +777,9 @@ class bmsMilter(Milter.Milter): ) return Milter.REJECT if self.mailfrom != '<>': - q.result = res self.cbv_needed = q if res in ('deny', 'fail'): if hres == 'pass' and q.o in spf_accept_fail: - q.result = res self.cbv_needed = q else: self.log('REJECT: SPF %s %i %s' % (res,code,txt)) @@ -800,7 +801,6 @@ class bmsMilter(Milter.Milter): ) return Milter.REJECT if self.mailfrom != '<>': - q.result = res self.cbv_needed = q if res == 'neutral' and q.o in spf_reject_neutral: self.log('REJECT: SPF neutral for',q.s) @@ -823,6 +823,7 @@ class bmsMilter(Milter.Milter): self.setreply(str(code),'4.3.0',txt) return Milter.TEMPFAIL self.add_header('Received-SPF',q.get_header(res,receiver)) + self.spf = q return Milter.CONTINUE # 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 def check_spam(self): + "return True/False if self.fp, else return Milter.REJECT/TEMPFAIL/etc" if not dspam_userdir: return False ds = Dspam.DSpamDirectory(dspam_userdir) ds.log = self.log @@ -1094,7 +1096,7 @@ class bmsMilter(Milter.Milter): ds.add_spam(sender,txt) txt = None self.fp = None - return False + return Milter.DISCARD elif user == 'falsepositive' and self.internal_connection: sender = dspam_users.get(self.canon_from) if sender: @@ -1111,16 +1113,23 @@ class bmsMilter(Milter.Milter): return False if user == 'honeypot' and Dspam.VERSION >= '1.1.9': keep = False # keep honeypot mail + self.fp = None 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, force_result=dspam.DSR_ISSPAM) - self.log("HONEYPOT:",rcpt,'SCREENED') else: ds.check_spam(user,txt,self.recipients,quarantine=keep, force_result=dspam.DSR_ISSPAM) self.log("HONEYPOT:",rcpt) - self.fp = None - return False + return Milter.DISCARD txt = ds.check_spam(user,txt,self.recipients) if not txt: # DISCARD if quarrantined for any recipient. It @@ -1128,7 +1137,7 @@ class bmsMilter(Milter.Milter): # as a false positive. self.log("DSPAM:",user,rcpt) self.fp = None - return False + return Milter.DISCARD self.fp = StringIO.StringIO(txt) modified = True except Exception,x: @@ -1143,18 +1152,25 @@ class bmsMilter(Milter.Milter): self.log("Large message:",len(txt)) return False 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, - classify=True,quarantine=not self.reject_spam): + classify=True,quarantine=False): self.fp = None if self.reject_spam: self.log("DSPAM:",screener, 'REJECT: X-DSpam-Score: %f' % ds.probability) self.setreply('550','5.7.1','Your Message looks spammy') - return True + return Milter.REJECT 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 def eom(self): @@ -1165,8 +1181,7 @@ class bmsMilter(Milter.Milter): # analyze external mail for spam spam_checked = self.check_spam() # tag or quarantine for spam if not self.fp: - if spam_checked: return Milter.REJECT - return Milter.DISCARD # message quarantined for all recipients + return spam_checked # analyze all mail for dangerous attachments and scripts self.fp.seek(0) @@ -1233,41 +1248,15 @@ class bmsMilter(Milter.Milter): if 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] + if q.result in ('softfail','fail','deny'): + template_name = 'softfail.txt' + elif q.result == 'unknown': + template_name = 'permerror.txt' else: - self.log('CBV:',sender) - try: - if q.result in ('softfail','fail','deny'): - template = file('softfail.txt').read() - elif q.result == 'unknown': - template = file('permerror.txt').read() - else: - template = file('strike3.txt').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 + template_name = 'strike3.txt' + rc = self.send_dsn(q,msg,template_name) self.cbv_needed = None + if rc != Milter.CONTINUE: return rc if not defanged and not spam_checked: os.remove(self.tempname) @@ -1301,6 +1290,38 @@ class bmsMilter(Milter.Milter): out.close() 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): sys.stdout.flush() # make log messages visible if self.tempname: diff --git a/milter.spec b/milter.spec index 424eecb..c2a8bfb 100644 --- a/milter.spec +++ b/milter.spec @@ -160,9 +160,10 @@ rm -rf $RPM_BUILD_ROOT %dir /var/log/milter/save %config /var/log/milter/start.sh %config /var/log/milter/bms.py -%config /var/log/milter/strike3.txt -%config /var/log/milter/softfail.txt -%config /var/log/milter/quarantine.txt +%config(noreplace) /var/log/milter/strike3.txt +%config(noreplace) /var/log/milter/softfail.txt +%config(noreplace) /var/log/milter/quarantine.txt +%config(noreplace) /var/log/milter/permerror.txt %config(noreplace) /etc/mail/pymilter.cfg /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. - spf_accept_fail option for braindead SPF senders (treats fail like softfail) - 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 0.8.2-4 - Limit each CNAME chain independently like PTR and MX * Fri Jul 15 2005 Stuart Gathman 0.8.2-3