1# Email address list with expiration 2# 3# This class acts like a map. Entries with a value of None are persistent, 4# but disappear after a time limit. This is useful for automatic whitelists 5# and blacklists with expiration. The persistent store is a simple ascii 6# file with sender and timestamp on each line. Entries can be appended 7# to the store, and will be picked up the next time it is loaded. 8# 9# Entries with other values are not persistent. This is used to hold failed 10# CBV results. 11# 12# $Log$ 13# Revision 1.9 2008/05/08 21:35:57 customdesigned 14# Allow explicitly whitelisted email from banned_users. 15# 16# Revision 1.8 2007/09/03 16:18:45 customdesigned 17# Delete unparseable timestamps when loading address cache. These have 18# arisen because of failure to parse MAIL FROM properly. Will have to 19# tighten up MAIL FROM parsing to match RFC. 20# 21# Revision 1.7 2007/01/25 22:47:26 customdesigned 22# Persist blacklisting from delayed DSNs. 23# 24# Revision 1.6 2007/01/19 23:31:38 customdesigned 25# Move parse_header to Milter.utils. 26# Test case for delayed DSN parsing. 27# Fix plock when source missing or cannot set owner/group. 28# 29# Revision 1.5 2007/01/11 19:59:40 customdesigned 30# Purge old entries in auto_whitelist and send_dsn logs. 31# 32# Revision 1.4 2007/01/11 04:31:26 customdesigned 33# Negative feedback for bad headers. Purge cache logs on startup. 34# 35# Revision 1.3 2007/01/08 23:20:54 customdesigned 36# Get user feedback. 37# 38# Revision 1.2 2007/01/05 23:33:55 customdesigned 39# Make blacklist an AddrCache 40# 41# Revision 1.1 2007/01/05 21:25:40 customdesigned 42# Move AddrCache to Milter package. 43# 44 45# Author: Stuart D. Gathman <stuart@bmsi.com> 46# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. 47# This code is under the GNU General Public License. See COPYING for details. 48 49from __future__ import print_function 50import time 51from Milter.plock import PLock 52 53class AddrCache(object): 54 time_format = '%Y%b%d %H:%M:%S %Z' 55 56 def __init__(self,renew=7,fname=None): 57 self.age = renew 58 self.cache = {} 59 self.fname = fname 60 61 def load(self,fname,age=0): 62 "Load address cache from persistent store." 63 if not age: 64 age = self.age 65 self.fname = fname 66 cache = {} 67 self.cache = cache 68 now = time.time() 69 lock = PLock(self.fname) 70 wfp = lock.lock() 71 changed = False 72 try: 73 too_old = now - age*24*60*60 # max age in days 74 try: 75 fp = open(self.fname) 76 except OSError: 77 fp = () 78 for ln in fp: 79 try: 80 rcpt,ts = ln.strip().split(None,1) 81 try: 82 l = time.strptime(ts,AddrCache.time_format) 83 t = time.mktime(l) 84 if t < too_old: 85 changed = True 86 continue 87 cache[rcpt.lower()] = (t,None) 88 except: # unparsable timestamp - likely garbage 89 changed = True 90 continue 91 except: # manual entry (no timestamp) 92 cache[ln.strip().lower()] = (now,None) 93 wfp.write(ln) 94 if changed: 95 lock.commit(self.fname+'.old') 96 else: 97 lock.unlock() 98 except IOError: 99 lock.unlock() 100 101 def has_precise_key(self,sender): 102 """True if precise sender is cached and has not expired. Don't 103 try looking up wildcard entries. 104 """ 105 try: 106 lsender = sender and sender.lower() 107 ts,res = self.cache[lsender] 108 too_old = time.time() - self.age*24*60*60 # max age in days 109 if not ts or ts > too_old: 110 return True 111 del self.cache[lsender] 112 except KeyError: pass 113 return False 114 115 def has_key(self,sender): 116 "True if sender is cached and has not expired." 117 if self.has_precise_key(sender): 118 return True 119 try: 120 user,host = sender.split('@',1) 121 return self.has_precise_key(host) 122 except: pass 123 return False 124 125 __contains__ = has_key 126 127 def __getitem__(self,sender): 128 try: 129 lsender = sender.lower() 130 ts,res = self.cache[lsender] 131 too_old = time.time() - self.age*24*60*60 # max age in days 132 if not ts or ts > too_old: 133 return res 134 del self.cache[lsender] 135 raise KeyError(sender) 136 except KeyError as x: 137 try: 138 user,host = sender.split('@',1) 139 return self.__getitem__(host) 140 except ValueError: 141 raise x 142 143 def addperm(self,sender,res=None): 144 "Add a permanent sender." 145 lsender = sender.lower() 146 if self.has_key(lsender): 147 ts,res = self.cache[lsender] 148 if not ts: return # already permanent 149 self.cache[lsender] = (None,res) 150 if not res: 151 with open(self.fname,'a') as fp: 152 print(sender,file=fp) 153 154 def __setitem__(self,sender,res): 155 lsender = sender.lower() 156 now = time.time() 157 self.cache[lsender] = (now,res) 158 if not res and self.fname: 159 s = time.strftime(AddrCache.time_format,time.localtime(now)) 160 with open(self.fname,'a') as fp: 161 print(sender,s,file=fp) # log refreshed senders 162 163 def __len__(self): 164 return len(self.cache) 165