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