diff --git a/Milter/cache.py b/Milter/cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..94df3b1ec7b7e6d9da51eb99b0de17abe43f0a59
--- /dev/null
+++ b/Milter/cache.py
@@ -0,0 +1,108 @@
+# Email address list with expiration
+#
+# This class acts like a map.  Entries with a value of None are persistent,
+# but disappear after a time limit.  This is useful for automatic whitelists
+# and blacklists with expiration.  The persistent store is a simple ascii
+# file with sender and timestamp on each line.  Entries can be appended
+# to the store, and will be picked up the next time it is loaded.
+#
+# Entries with other values are not persistent.  This is used to hold failed
+# CBV results.
+#
+# $Log$
+
+# Author: Stuart D. Gathman <stuart@bmsi.com>
+# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
+# This code is under the GNU General Public License.  See COPYING for details.
+
+import time
+
+class AddrCache(object):
+  time_format = '%Y%b%d %H:%M:%S %Z'
+
+  def __init__(self,renew=7,fname=None):
+    self.age = renew
+    self.cache = {}
+    self.fname = fname
+
+  def load(self,fname,age=0):
+    "Load address cache from persistent store."
+    if not age:
+      age = self.age
+    self.fname = fname
+    cache = {}
+    self.cache = cache
+    now = time.time()
+    try:
+      too_old = now - 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.lower()] = (t,None)
+	except:
+	  cache[ln.strip().lower()] = (now,None)
+    except IOError: pass
+
+  def has_key(self,sender):
+    "True if sender is cached and has not expired."
+    try:
+      lsender = sender.lower()
+      ts,res = self.cache[lsender]
+      too_old = time.time() - self.age*24*60*60	# max age in days
+      if not ts or ts > too_old:
+        return True
+      del self.cache[lsender]
+      try:
+	user,host = sender.split('@',1)
+	return self.has_key(host)
+      except ValueError:
+        pass
+    except KeyError:
+      try:
+	user,host = sender.split('@',1)
+	return self.has_key(host)
+      except ValueError:
+        pass
+    return False
+
+  def __getitem__(self,sender):
+    try:
+      lsender = sender.lower()
+      ts,res = self.cache[lsender]
+      too_old = time.time() - self.age*24*60*60	# max age in days
+      if not ts or ts > too_old:
+	return res
+      del self.cache[lsender]
+      raise KeyError, sender
+    except KeyError,x:
+      try:
+	user,host = sender.split('@',1)
+	return self.__getitem__(host)
+      except ValueError:
+        raise x
+
+  def addperm(self,sender,res=None):
+    "Add a permanent sender."
+    lsender = sender.lower()
+    if self.has_key(lsender):
+      ts,res = self.cache[lsender]
+      if not ts: return		# already permanent
+    self.cache[lsender] = (None,res)
+    if not res:
+      print >>open(self.fname,'a'),sender
+    
+  def __setitem__(self,sender,res):
+    lsender = sender.lower()
+    now = time.time()
+    cached = self.has_key(sender)
+    if not cached:
+      self.cache[lsender] = (now,res)
+      if not res and self.fname:
+	s = time.strftime(AddrCache.time_format,time.localtime(now))
+	print >>open(self.fname,'a'),sender,s # log refreshed senders
+
+  def __len__(self):
+    return len(self.cache)
diff --git a/TODO b/TODO
index 5b1db9b172f92e55a59b4c53a47bc833c34b1054..e3e282197ea6d30053495c0e4b7da6bce1fb94e0 100644
--- a/TODO
+++ b/TODO
@@ -1,7 +1,6 @@
-When bms.py can't find templates, it passes None to dsn.create_msg(),
-which uses local variable as backup, which no longer exist.
-
-Purge old GOSSiP records nightly.
+DONE When bms.py can't find templates, it passes None to dsn.create_msg(),
+which uses local variable as backup, which no longer exist.  Do plain
+CBV in that case instead.
 
 Find and use X-GOSSiP: header for SPAM: and FP: submissions.  Would need to 
 keep tags longer.
diff --git a/bms.py b/bms.py
index 248f7aac6e7e42deb6fa909af2f2ba0257b0fa4c..164f17370f51eaafca8dba60f46e34cc623c47c7 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.78  2007/01/04 18:01:10  customdesigned
+# Do plain CBV when template missing.
+#
 # Revision 1.77  2006/12/31 03:07:20  customdesigned
 # Use HELO identity if good when MAILFROM is bad.
 #
@@ -510,79 +513,7 @@ def iniplist(ipaddr,iplist):
       return True
   return False
 
-class AddrCache(object):
-  time_format = '%Y%b%d %H:%M:%S %Z'
-
-  def __init__(self,renew=7):
-    self.age = renew
-
-  def load(self,fname,age=0):
-    if not age:
-      age = self.age
-    self.fname = fname
-    cache = {}
-    self.cache = cache
-    now = time.time()
-    try:
-      too_old = now - 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.lower()] = (t,None)
-	except:
-	  cache[ln.strip().lower()] = (now,None)
-    except IOError: pass
-
-  def has_key(self,sender):
-    try:
-      ts,res = self.cache[sender.lower()]
-      too_old = time.time() - self.age*24*60*60	# max age in days
-      if ts > too_old:
-        return True
-      del self.cache[sender.lower()]
-      try:
-	user,host = sender.split('@',1)
-	return self.has_key(host)
-      except ValueError:
-        pass
-    except KeyError:
-      try:
-	user,host = sender.split('@',1)
-	return self.has_key(host)
-      except ValueError:
-        pass
-    return False
-
-  def __getitem__(self,sender):
-    try:
-      ts,res = self.cache[sender.lower()]
-      too_old = time.time() - self.age*24*60*60	# max age in days
-      if ts > too_old:
-	return res
-      del self.cache[sender.lower()]
-      raise KeyError, sender
-    except KeyError,x:
-      try:
-	user,host = sender.split('@',1)
-	return self.__getitem__(host)
-      except ValueError:
-        raise x
-
-  def __setitem__(self,sender,res):
-    lsender = sender.lower()
-    now = time.time()
-    cached = self.has_key(sender)
-    if not cached:
-      self.cache[lsender] = (now,res)
-      if not res:
-	s = time.strftime(AddrCache.time_format,time.localtime(now))
-	print >>open(self.fname,'a'),sender,s # log refreshed senders
-
-  def __len__(self):
-    return len(self.cache)
+from Milter.cache import AddrCache
 
 cbv_cache = AddrCache(renew=7)
 cbv_cache.load('send_dsn.log',age=7)
diff --git a/milter.spec b/milter.spec
index c674a29cc3e6c134cef74395e9fecccb85b50f12..4da9f8bed71fe670e31078d3a24eb0252b658df5 100644
--- a/milter.spec
+++ b/milter.spec
@@ -11,7 +11,8 @@
 # some systems dont have initrddir defined
 %{?_initrddir:%define _initrddir /etc/rc.d/init.d}
 
-%if %{redhat7} # Redhat 7.x and earlier (multiple ps lines per thread)
+%if %{redhat7} 
+# Redhat 7.x and earlier (multiple ps lines per thread)
 %define sysvinit milter.rc7
 %else	
 %define sysvinit milter.rc
@@ -167,6 +168,7 @@ rm -rf $RPM_BUILD_ROOT
 %config /var/log/milter/bms.py
 %config(noreplace) /var/log/milter/strike3.txt
 %config(noreplace) /var/log/milter/softfail.txt
+%config(noreplace) /var/log/milter/fail.txt
 %config(noreplace) /var/log/milter/neutral.txt
 %config(noreplace) /var/log/milter/quarantine.txt
 %config(noreplace) /var/log/milter/permerror.txt
diff --git a/test.py b/test.py
index 0e12658b3f4bd559a60ef3a76608a11890862cdf..90c3d427d17e0a4cdd0b57a7a063847f94fdc6e8 100644
--- a/test.py
+++ b/test.py
@@ -2,6 +2,7 @@ import unittest
 import testbms
 import testmime
 import testsample
+import testcache
 import os
 
 def suite(): 
@@ -9,6 +10,7 @@ def suite():
   s.addTest(testbms.suite())
   s.addTest(testmime.suite())
   s.addTest(testsample.suite())
+  s.addTest(testcache.suite())
   return s
 
 if __name__ == '__main__':