diff --git a/bms.py b/bms.py index 15bc2763b9bdad9d093c92cae700dac7f6d2dd80..f401a86aefdacae28c6271a34d41b7c6fe13ec36 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.30 2005/10/12 16:36:30 customdesigned +# Release 0.8.3 +# # Revision 1.29 2005/10/11 22:50:07 customdesigned # Always check HELO except for SPF pass, temperror. # @@ -352,9 +355,8 @@ spf_best_guess = False spf_reject_noptr = False supply_sender = False access_file = None -time_format = '%Y%b%d %H:%M:%S %Z' timeout = 600 -cbv_cache = {} + logging.basicConfig( stream=sys.stdout, level=logging.INFO, @@ -363,19 +365,6 @@ logging.basicConfig( ) 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): def getlist(self,sect,opt): @@ -662,6 +651,47 @@ class SPFPolicy(object): policy = 'OK' 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] = None + except: + cache[ln.strip()] = None + except IOError: pass + + def has_key(self,sender): + return self.cache.has_key(sender) + + def __getitem__(self,sender): + return self.cache[sender] + + def __setitem__(self,sender,res): + cached = sender in self.cache + self.cache[sender] = 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): """Milter to replace attachments poisonous to Windows with a WARNING message, check SPF, and other anti-forgery features, and implement wiretapping @@ -987,6 +1017,9 @@ class bmsMilter(Milter.Milter): return Milter.TEMPFAIL self.add_header('Received-SPF',q.get_header(res,receiver)) self.spf = q + if self.dspam and not self.internal_connection and res == 'pass': + if auto_whitelist.has_key(self.canon_from): + self.dspam = False return Milter.CONTINUE # hide_path causes a copy of the message to be saved - until we @@ -998,8 +1031,9 @@ class bmsMilter(Milter.Milter): self.log('DISCARD: RCPT TO:',to,str) return Milter.DISCARD self.log("rcpt to",to,str) - t = parse_addr(to.lower()) + t = parse_addr(to) if len(t) == 2: + t[1] = t[1].lower() user,domain = t if self.is_bounce and srs and domain in srs_domain: oldaddr = '@'.join(parse_addr(to)) @@ -1027,7 +1061,8 @@ class bmsMilter(Milter.Milter): self.data_allowed = not srs_reject_spoofed # non DSN mail to SRS address will bounce due to invalid local part - self.recipients.append('@'.join(t)) + canon_to = '@'.join(t) + self.recipients.append(canon_to) users = check_user.get(domain) if self.discard: self.del_recipient(to) @@ -1043,6 +1078,14 @@ class bmsMilter(Milter.Milter): self.hidepath = True if not domain in dspam_reject: self.reject_spam = False + if self.internal_connection: + 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) #rcpt = self.getsymval("{rcpt_addr}") #self.log("rcpt-addr",rcpt); @@ -1161,7 +1204,7 @@ class bmsMilter(Milter.Milter): self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer self.fp.write("\n") # terminate headers # log when neither sender nor from domains matches mail from domain - if supply_sender and self.mailfrom != '<>': + if supply_sender and self.mailfrom != '<>' and not self.internal_connection: mf_domain = self.canon_from.split('@')[-1] self.fp.seek(0) msg = rfc822.Message(self.fp) @@ -1315,6 +1358,8 @@ class bmsMilter(Milter.Milter): force_result=dspam.DSR_ISSPAM) self.log("HONEYPOT:",rcpt) return Milter.DISCARD + #if not self.dspam: + # FIXME: tag, but force as ham txt = ds.check_spam(user,txt,self.recipients) if not txt: # DISCARD if quarrantined for any recipient. It @@ -1507,9 +1552,6 @@ class bmsMilter(Milter.Milter): 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):