diff --git a/Doxyfile b/Doxyfile
index ad35549f978f7a7d05472f7beaa6e5aabb414dea..fecd1229decb983c079aeb4e97ca0f91f89d4b90 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -31,7 +31,7 @@ PROJECT_NAME           = pymilter
 # This could be handy for archiving the generated documentation or 
 # if some version control system is used.
 
-PROJECT_NUMBER         = 0.9.2
+PROJECT_NUMBER         = 0.9.3
 
 # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) 
 # base path where the generated documentation will be put. 
diff --git a/Milter/__init__.py b/Milter/__init__.py
index 960cad10c5375fd7a081548b74de230c40ad042e..f712fe0b3ef8ff0c278bccbd54864c22c8f80f1f 100755
--- a/Milter/__init__.py
+++ b/Milter/__init__.py
@@ -146,6 +146,14 @@ class Base(object):
   def close(self): return CONTINUE
 
   ## Return mask of SMFIP_N.. protocol option bits to clear for this class
+  # The @@nocallback and @@noreply decorators set the
+  # <code>milter_protocol</code> function attribute to the protocol mask bit to
+  # pass to libmilter, causing that callback or its reply to be skipped.
+  # Overriding a method creates a new function object, so that 
+  # <code>milter_protocol</code> defaults to 0.
+  # Libmilter passes the protocol bits that the current MTA knows
+  # how to skip.  We clear the ones we don't want to skip.
+  # The negation is somewhat mind bending, but it is simple.
   @classmethod
   def protocol_mask(klass):
     try:
diff --git a/Milter/dns.py b/Milter/dns.py
index ff962c26032aa08ce128d155d45f342f2c41d8be..fe9b54a2aeb88b99b5e5c9b667a7516e2858c47a 100644
--- a/Milter/dns.py
+++ b/Milter/dns.py
@@ -1,12 +1,22 @@
-# provide a higher level interface to pydns
+## @package Milter.dns
+# Provide a higher level interface to pydns.
 
 import DNS
 from DNS import DNSError
 
 MAX_CNAME = 10
 
+## Lookup DNS records by label and RR type.
+# The response can include records of other types that the DNS
+# server thinks we might need.
+# @param name the DNS label to lookup
+# @param qtype the name of the DNS RR type to lookup
+# @return a list of ((name,type),data) tuples
 def DNSLookup(name, qtype):
     try:
+	# To be thread safe, we create a fresh DnsRequest with
+	# each call.  It would be more efficient to reuse
+	# a req object stored in a Session.
         req = DNS.DnsRequest(name, qtype=qtype)
         resp = req.req()
         #resp.show()
@@ -24,25 +34,28 @@ class Session(object):
   def __init__(self):
     self.cache = {}
 
+  ## Additional DNS RRs we can safely cache.
   # We have to be careful which additional DNS RRs we cache.  For
   # instance, PTR records are controlled by the connecting IP, and they
   # could poison our local cache with bogus A and MX records.  
+  # Each entry is a tuple of (query_type,rr_type).  So for instance,
+  # the entry ('MX','A') says it is safe (for milter purposes) to cache
+  # any 'A' RRs found in an 'MX' query.
+  SAFE2CACHE = frozenset((
+    ('MX','MX'), ('MX','A'),
+    ('CNAME','CNAME'), ('CNAME','A'),
+    ('A','A'),
+    ('AAAA','AAAA'),
+    ('PTR','PTR'),
+    ('NS','NS'), ('NS','A'),
+    ('TXT','TXT'),
+    ('SPF','SPF')
+  ))
 
-  SAFE2CACHE = {
-    ('MX','A'): None,
-    ('MX','MX'): None,
-    ('CNAME','A'): None,
-    ('CNAME','CNAME'): None,
-    ('A','A'): None,
-    ('AAAA','AAAA'): None,
-    ('PTR','PTR'): None,
-    ('NS','NS'): None,
-    ('NS','A'): None,
-    ('TXT','TXT'): None,
-    ('SPF','SPF'): None
-  }
-
-
+  ## Cached DNS lookup.
+  # @param name the DNS label to query
+  # @param qtype the query type, e.g. 'A'
+  # @param cnames tracks CNAMES already followed in recursive calls
   def dns(self, name, qtype, cnames=None):
     """DNS query.
 
diff --git a/Milter/dsn.py b/Milter/dsn.py
index 75be7ff53a359b5c1a4440e943913adbea8a4e3e..290fdd0b8ff0e7179c057950798e7ef6a649f90a 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.17  2009/05/20 20:08:44  customdesigned
+# Support non-DSN CBV (non-empty MAIL FROM)
+#
 # Revision 1.16  2007/09/25 01:24:59  customdesigned
 # Allow arbitrary object, not just spf.query like, to provide data for create_msg
 #
@@ -26,7 +29,31 @@
 # Revision 1.10  2006/05/24 20:56:35  customdesigned
 # Remove default templates.  Scrub test.
 #
-
+## @package Milter.dsn
+# Support DSNs and CallBackValidations (CBV).
+#
+# A Delivery Status Notification (bounce) is sent to the envelope
+# sender (original MAIL FROM) with a null MAIL FROM (<>) to notify the
+# original sender # of delays or problems with delivery.  A Callback Validation
+# starts the DSN process, but stops before issuing the DATA command.  The
+# purpose is to check whether the envelope recipient is accepted (and is
+# therefore a valid email).  The null MAIL FROM tells the remote
+# MTA to never reply according to RFC2821 (but some braindead MTAs
+# reply anyway, of course).
+#
+# Milters should cache CBV results and should avoid sending DSNs
+# unless the sender is authenticated somehow (e.g. SPF Pass).  However,
+# when email is quarantined, and is not known to be a forgery, sending a DSN 
+# is better than silently disappearing, and a DSN is better than sending
+# a normal message as notification - because MAIL FROM signing schemes
+# can reject bounces of forged emails.   Whatever you do, don't copy those
+# assinine commercial filters that send a normal message to notify you
+# that some virus is forging your email.
+#
+# <b>DSNs should *only* be sent to MAIL FROM addresses.</b>  Never send
+# a DSN or use a null MAIL FROM with an email address obtained from
+# anywhere else.
+#
 import smtplib
 import socket
 from email.Message import Message
@@ -34,6 +61,19 @@ import Milter
 import time
 import dns
 
+## Send DSN.
+# Try the published MX names in order, rejecting obviously bogus entries
+# (like <code>localhost</code>).
+# @param mailfrom the original sender we are notifying or validating
+# @param receiver the HELO name of the MTA we are sending the DSN on behalf of.
+#	Be sure to send from an IP that matches the HELO.
+# @param msg the DSN message in RFC2822 format, or None for CBV.
+# @param timeout total seconds to wait for a response from an MX
+# @param session Milter.dns.Session object from current incoming mail
+#	session to reuse its cache, or None to create a fresh one.
+# @param ourfrom set to a valid email to send a normal notification from, or
+#	to validate emails not obtained from MAIL FROM.
+# @return None on success or (status_code,msg) on failure.
 def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
   """Send DSN.  If msg is None, do callback verification.
      Mailfrom is original sender we are sending DSN or CBV to.