diff --git a/Milter/dns.py b/Milter/dns.py
new file mode 100644
index 0000000000000000000000000000000000000000..21941c2465b5b98361225f703c74a73ae5cda3ac
--- /dev/null
+++ b/Milter/dns.py
@@ -0,0 +1,62 @@
+# provide a higher level interface to pydns
+
+import DNS
+from DNS import DNSError
+
+MAX_CNAME = 10
+
+def DNSLookup(name, qtype):
+    try:
+        req = DNS.DnsRequest(name, qtype=qtype)
+        resp = req.req()
+        #resp.show()
+        # key k: ('wayforward.net', 'A'), value v
+        # FIXME: pydns returns AAAA RR as 16 byte binary string, but
+        # A RR as dotted quad.  For consistency, this driver should
+        # return both as binary string.
+        return [((a['name'], a['typename']), a['data']) for a in resp.answers]
+    except IOError, x:
+        raise DNSError, str(x)
+
+class Session(object):
+  """A Session object has a simple cache with no TTL that is valid
+   for a single "session", for example an SMTP conversation."""
+  def __init__(self):
+    self.cache = {}
+
+  def dns(self, name, qtype, cnames=None):
+    """DNS query.
+
+    If the result is in cache, return that.  Otherwise pull the
+    result from DNS, and cache ALL answers, so additional info
+    is available for further queries later.
+
+    CNAMEs are followed.
+
+    If there is no data, [] is returned.
+
+    pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
+    post: isinstance(__return__, types.ListType)
+    """
+    result = self.cache.get( (name, qtype) )
+    cname = None
+
+    if not result:
+        safe2cache = query.SAFE2CACHE
+        for k, v in DNSLookup(name, qtype, self.strict):
+            if k == (name, 'CNAME'):
+                cname = v
+            if (qtype,k[1]) in safe2cache:
+                self.cache.setdefault(k, []).append(v)
+        result = self.cache.get( (name, qtype), [])
+    if not result and cname:
+        if not cnames:
+            cnames = {}
+        elif len(cnames) >= MAX_CNAME:
+            #return result    # if too many == NX_DOMAIN
+            raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
+        cnames[name] = cname
+        if cname in cnames:
+            raise DNSError, 'CNAME loop'
+        result = self.dns(cname, qtype, cnames=cnames)
+    return result
diff --git a/Milter/dsn.py b/Milter/dsn.py
index efdeef935786ee8c783a4b53e8069f454d967b9b..259fb135c997773cb7c76c669e941c4e6b654821 100644
--- a/Milter/dsn.py
+++ b/Milter/dsn.py
@@ -5,6 +5,9 @@
 # Send DSNs, do call back verification,
 # and generate DSN messages from a template
 # $Log$
+# Revision 1.14  2007/03/03 18:19:40  customdesigned
+# Handle DNS error sending DSN.
+#
 # Revision 1.13  2007/01/04 18:01:11  customdesigned
 # Do plain CBV when template missing.
 #
@@ -19,22 +22,22 @@
 #
 
 import smtplib
-import spf
 import socket
 from email.Message import Message
 import Milter
 import time
+import dns
 
-def send_dsn(mailfrom,receiver,msg=None,timeout=600):
+def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None):
   """Send DSN.  If msg is None, do callback verification.
      Mailfrom is original sender we are sending DSN or CBV to.
      Receiver is the MTA sending the DSN.
      Return None for success or (code,msg) for failure."""
   user,domain = mailfrom.split('@')
+  if not session: session = dns.Session()
   try:
-    q = spf.query(None,None,None)
-    mxlist = q.dns(domain,'MX')
-  except spf.TempError:
+    mxlist = session.dns(domain,'MX')
+  except dns.DNSError:
     return (450,'DNS Timeout: %s MX'%domain)	# temp error
   if not mxlist:
     mxlist = (0,domain),	# fallback to A record when no MX
@@ -124,6 +127,7 @@ def create_msg(q,rcptlist,origmsg=None,template=None):
   return msg
 
 if __name__ == '__main__':
+  import spf
   q = spf.query('192.168.9.50',
   'SRS0=pmeHL=RH==stuart@example.com',
   'red.example.com',receiver='mail.example.com')