diff --git a/MANIFEST.in b/MANIFEST.in
index b7fed2c1681db8327a4e157143b5fef1a05db604..a7faa283c48c67e13a55a64eed6462baeb8ae3b9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -20,3 +20,4 @@ include start.sh
 include milter.rc
 include milter.rc7
 include milter.cfg
+include rhsbl.m4
diff --git a/NEWS b/NEWS
index 6f82cafc7e0be4a918db7d97d22672c6cc64a089..3b44bf761a90ba8dba107095643fb055a228f779 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,14 @@
 Here is a history of user visible changes to Python milter.
 
+0.7.2	Return unknown for invalid ip address in mechanism
+	Recognize dynamic PTR names, and don't count them as authentication.
+	Three strikes and yer out rule.
+	Block softfail by default when no PTR or HELO
+	Return unknown for null mechanism
+	Return unknown for invalid ip address in mechanism
+	Try best guess on HELO also
+	Expand setreply for common errors
+	make rhsbl.m4 hack available for sendmail.mc
 0.7.1	Handle modifying mislabeled multipart messages without an exception
 	Support setbacklog, setmlreply
 	Allow multi-recipient CBV
diff --git a/TODO b/TODO
index cc99affbb2327dad4e5055793e7efa6bc9071bad..2fae047d27fc45eabb695ea7f874d7f001c2e1c8 100644
--- a/TODO
+++ b/TODO
@@ -1,10 +1,30 @@
+Checking in mime.py;
+/bms/cvs/milter/mime.py,v  <--  mime.py
+new revision: 1.56; previous revision: 1.55
+done
+Checking in spf.py;
+/bms/cvs/milter/spf.py,v  <--  spf.py
+new revision: 1.18; previous revision: 1.17
+done
+Checking in testmime.py;
+/bms/cvs/milter/testmime.py,v  <--  testmime.py
+new revision: 1.19; previous revision: 1.18
+
+Auto whitelist based on outgoing email - perhaps with magic subject
+or recipient prefix.
+
+Can't output messages with malformed rfc822 attachments.
+
+Use python exceptions in SPF to cleanly handle unknown and error results.
+
+Example malformed SPF:
+onvunvuvvx.usafisnews.org text "v=spf1 mx ptr ip4:207.44.199.970 -all"
+
 Move milter,Milter,mime,spf modules to pymilter
 milter package will have bms.py application
 
-spf.py has no recursion bound on CNAME lookup
 Support SMTP AUTH and disable SPF checks when connection is authorized.
 Web admin interface
-RHSBL
 Check valid domains allowed by internal senders to detect PCs infected
 with spam trojans.
 Do CBV (callback verification) for mail with no published SPF record.
@@ -52,3 +72,4 @@ Wrap smfi_setbacklog(int) - but it is only available in sendmail >= 8.12.3,
 Need a test module to feed sample messages to a milter though a live 
 sendmail and SMTP.  The mockup currently used is probably not very accurate,
 and doesn't test the threading code.
+
diff --git a/bms.py b/bms.py
index fffecf32d99236ce6ddc01f975cc34e9d504bcce..0c044740866bbcb5970f31b61d8aba56ecc2677e 100644
--- a/bms.py
+++ b/bms.py
@@ -1,6 +1,30 @@
 #!/usr/bin/env python
 # A simple milter.
 # $Log$
+# Revision 1.126  2004/11/24 14:39:38  stuart
+# Also accept softfail if valid PTR or HELO.
+#
+# Revision 1.125  2004/11/19 16:40:14  stuart
+# Block softfail except for listed domains.
+#
+# Revision 1.124  2004/11/19 06:18:04  stuart
+# block softfail for configured domains only
+#
+# Revision 1.123  2004/11/18 20:36:49  stuart
+# Recognize more dynamic hosts.  Ignore dynamic PTR for best_guess.
+#
+# Revision 1.122  2004/11/18 17:16:10  stuart
+# Recognize more dynamic ips.
+#
+# Revision 1.121  2004/11/09 22:37:48  stuart
+# Don't accept helo names which are dynamic IP addresses.
+#
+# Revision 1.120  2004/11/09 20:33:50  stuart
+# Recognize more dynamic PTR variations.
+#
+# Revision 1.118  2004/08/30 21:19:50  stuart
+# Try best guess for HELO, expand setreply for common errors
+#
 # Revision 1.117  2004/08/23 02:27:53  stuart
 # Allow multi rcpt CBV.  Add some multiline replies.
 #
@@ -292,6 +316,7 @@ srs = None
 srs_reject_spoofed = False
 srs_fwdomain = None
 spf_reject_neutral = ()
+spf_accept_softfail = ()
 spf_best_guess = False
 spf_reject_noptr = False
 timeout = 600
@@ -401,6 +426,7 @@ def read_config(list):
   global dspam_dict, dspam_users, dspam_userdir, dspam_exempt
   global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
   global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr
+  global spf_accept_softfail
   dspam_dict = cp.getdefault('dspam','dspam_dict')
   dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
   dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
@@ -414,6 +440,7 @@ def read_config(list):
   if spf:
     spf.DELEGATE = cp.getdefault('spf','delegate')
     spf_reject_neutral = cp.getlist('spf','reject_neutral')
+    spf_accept_softfail = cp.getlist('spf','accept_softfail')
     spf_best_guess = cp.getboolean('spf','best_guess')
     spf_reject_noptr = cp.getboolean('spf','reject_noptr')
   srs_config = cp.getdefault('srs','config')
@@ -461,6 +488,43 @@ def parse_header(val):
   except LookupError: pass
   return val
 
+ip3 = re.compile('([0-9]{1,3})[.-]([0-9]{1,3})[.-]([0-9]{1,3})')
+rehmac = re.compile('h[0-9a-f]{12}[.]|pcp[0-9]{6,10}pcs[.]|no-reverse')
+
+def dynip(host,addr):
+  """Return True if hostname is for a dynamic ip.
+  Examples:
+
+  >>> is_dynip('post3.fabulousdealz.com','69.60.99.112')
+  False
+  >>> is_dynip('adsl-69-208-201-177.dsl.emhril.ameritech.net','69.208.201.177')
+  True
+  """
+  if host.startswith('[') and host.endswith(']'):
+    return True
+  if addr:
+    if host.find(addr) >= 0: return True
+    a = addr.split('.')
+    m = ip3.search(host)
+    if m:
+      g = list(m.groups())
+      if g == a[1:] or g == a[:3]: return True
+      g.reverse()
+      if g == a[1:] or g == a[:3]: return True
+    if rehmac.search(host): return True
+    if host.find("-%s." % '-'.join(a[2:])) >= 0: return True
+    if host.find("w%s." % '-'.join(a[:2])) >= 0: return True
+    if host.find(''.join(a[:3])) >= 0: return True
+    if host.find(''.join(a[1:])) >= 0: return True
+    x = "%02x%02x%02x%02x" % tuple(map(int,a))
+    if host.lower().find(x) >= 0: return True
+    z = [n.zfill(3) for n in a]
+    if host.find('-'.join(z)) >= 0: return True
+    if host.find("-%s." % '-'.join(z[2:])) >= 0: return True
+    if host.find("%s." % ''.join(z[2:])) >= 0: return True
+    if host.find(''.join(z)) >= 0: return True
+  return False
+
 class bmsMilter(Milter.Milter):
   """Milter to replace attachments poisonous to Windows with a WARNING message,
      check SPF, and other anti-forgery features, and implement wiretapping
@@ -500,7 +564,6 @@ class bmsMilter(Milter.Milter):
     self.log('%s: %s' % (name,val))
 
   def connect(self,hostname,unused,hostaddr):
-    self.missing_ptr = hostname.startswith('[') and hostname.endswith(']')
     self.internal_connection = False
     self.trusted_relay = False
     self.receiver = self.getsymval('j')
@@ -517,6 +580,7 @@ class bmsMilter(Milter.Milter):
       self.connectip = ipaddr
     else:
       self.connectip = None
+    self.missing_ptr = dynip(hostname,self.connectip)
     for pat in internal_connect:
       if fnmatchcase(hostname,pat):
 	self.internal_connection = True
@@ -602,47 +666,61 @@ class bmsMilter(Milter.Milter):
     q.set_default_explanation('SPF fail: see http://spf.pobox.com/why.html')
     res,code,txt = q.check()
     receiver = self.receiver
-    if res == 'none':
+    if res in ('none', 'softfail'):
       if self.mailfrom != '<>':
 	# check hello name via spf
-	hres,hcode,htxt = spf.check(self.connectip,'',self.hello_name)
+	h = spf.query(self.connectip,'',self.hello_name)
+	hres,hcode,htxt = h.check()
 	if hres in ('deny','fail','neutral','softfail'):
 	  self.log('REJECT: hello SPF: %s 550 %s' % (hres,htxt))
 	  self.setreply('550','5.7.1',htxt,
 	    "The hostname given in your MTA's HELO response is not listed",
-	    "as a legitimate MTA in the SPF records for your domain.",
-	    "If you get this bounce, the message was not in fact a forgery,",
-	    "and you should notify your email administrator of the problem."
+	    "as a legitimate MTA in the SPF records for your domain.  If you",
+	    "get this bounce, the message was not in fact a forgery, and you",
+	    "should IMMEDIATELY notify your email administrator of the problem."
 	  )
 	  return Milter.REJECT
-      if spf_best_guess:
+	if hres == 'none' and spf_best_guess \
+	  and not dynip(self.hello_name,self.connectip):
+	  hres,hcode,htxt = h.best_guess()
+      else: hres = res
+      if spf_best_guess and res == 'none':
 	#self.log('SPF: no record published, guessing')
 	q.set_default_explanation(
 		'SPF guess: see http://spf.pobox.com/why.html')
 	# best_guess should not result in fail
-	res,code,txt = q.best_guess()
+	if self.missing_ptr:
+	  # ignore dynamic PTR for best guess
+	  res,code,txt = q.best_guess('v=spf1 a/24 mx/24')
+	else:
+	  res,code,txt = q.best_guess()
 	receiver += ': guessing'
-      if self.missing_ptr and res in ('neutral', 'none') and spf_reject_noptr:
-        self.log('REJECT: no PTR or SPF')
+      if self.missing_ptr and res in ('neutral', 'none')	\
+      	and spf_reject_noptr and hres != 'pass':
+        self.log('REJECT: no PTR, HELO or SPF')
 	self.setreply('550','5.7.1',
-  'You must have a reverse lookup or publish SPF: http://spf.pobox.com'
+  'You must have a reverse lookup or publish SPF: http://spf.pobox.com',
+  'Contact your mail administrator IMMEDIATELY!  Your mail server is',
+  'severely misconfigured.  It has no PTR record (dynamic PTR records',
+  "that contain your IP don't count), an invalid HELO, and no SPF record."
 	)
 	return Milter.REJECT
     if res in ('deny', 'fail'):
       self.log('REJECT: SPF %s %i %s' % (res,code,txt))
       self.setreply(str(code),'5.7.1',txt)
       return Milter.REJECT
-    if res == 'softfail':
-      self.log('TEMPFAIL: SPF %s 450 %s' % (res,txt))
-      self.setreply('450','4.3.0',
-	'SPF softfail: will keep trying until your SPF record is fixed.',
-	'If you get this Delivery Status Notice, your email was probably',
-	'legitimate.  Your administrator has published SPF records in a',
-	'testing mode.  The SPF record reported your email as a forgery,',
-	'which is a mistake if you are reading this.  Please notify your',
-	'administrator of the problem.'
-      )
-      return Milter.TEMPFAIL
+    if res == 'softfail' and not q.o in spf_accept_softfail:
+      if self.missing_ptr and spf_reject_noptr and hres != 'pass':
+	self.log('TEMPFAIL: SPF %s 450 %s' % (res,txt))
+	self.setreply('450','4.3.0',
+	  'SPF softfail: will keep trying until your SPF record is fixed.',
+	  'If you get this Delivery Status Notice, your email was probably',
+	  'legitimate.  Your administrator has published SPF records in a',
+	  'testing mode.  The SPF record reported your email as a forgery,',
+	  'which is a mistake if you are reading this.  Please notify your',
+	  'administrator of the problem immediately.'
+	)
+	return Milter.TEMPFAIL
     if res == 'neutral' and q.o in spf_reject_neutral:
       self.log('REJECT: SPF neutral for',q.s)
       self.setreply('550','5.7.1',
@@ -655,6 +733,11 @@ class bmsMilter(Milter.Milter):
       )
       return Milter.REJECT
     if res == 'error':
+      if code >= 500:
+        self.log('REJECT: SPF %s %i %s' % (res,code,txt))
+	self.setreply(str(code),'5.7.1',txt)
+	return Milter.REJECT
+      self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
       self.setreply(str(code),'4.3.0',txt)
       return Milter.TEMPFAIL
     self.add_header('Received-SPF',q.get_header(res,receiver))
@@ -985,6 +1068,9 @@ class bmsMilter(Milter.Milter):
       self.tempname = None
       if exc_type == email.Errors.BoundaryError:
 	self.log("MALFORMED: %s" % fname)	# log filename
+        if self.internal_connection:
+	  # accept anyway for now
+	  return Milter.ACCEPT
 	self.setreply('554','5.7.7',
 		'Boundary error in your message, are you a spammer?')
         return Milter.REJECT
diff --git a/milter.cfg b/milter.cfg
index e5f8d5287a83f7d408144d72ccb406cb128f1918..adbd0ed054c8d768a982539e1a311fbbcf40472e 100644
--- a/milter.cfg
+++ b/milter.cfg
@@ -20,10 +20,11 @@ log_headers = 0
 ;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com
 # reject mail with these case insensitive strings in the subject
 porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
-	vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck,
+	vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck, xanaax,
 	p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam,
 	v1@gra, xan@x, cialis, ci@lis, fr�e, x�nax, val�um, v�lium, via-gra,
-	x@n3x, vicod3n, pen�s, c0d1n, phentermine, en1arge, dip1oma, v1codin
+	x@n3x, vicod3n, pen�s, c0d1n, phentermine, en1arge, dip1oma, v1codin,
+	valium, rolex
 # reject mail with these case sensitive strings in the subject
 spam_words = $$$, !!!, XXX, FREE, HGH
 
@@ -68,6 +69,8 @@ reject_spoofed = 0
 ;best_guess = 0
 # reject senders that have neither PTR nor SPF records
 ;reject_noptr = 0
+# always accept softfail from these domains
+;accept_softfail = bounces.amazon.com
 
 # features intended to clean up outgoing mail
 [scrub]
diff --git a/milter.html b/milter.html
index 34c03e0546eafdfc33ff93c02badcc0beea2e88a..3b9a8d54b2460b4072851c94f7a0ae26e1ff5106 100644
--- a/milter.html
+++ b/milter.html
@@ -24,7 +24,7 @@ ALT="Viewable With Any Browser" BORDER="0"></A>
   Stuart D. Gathman</a><br>
 This web page is written by Stuart D. Gathman<br>and<br>sponsored by
 <a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
-Last updated Aug 06, 2004</h4>
+Last updated Nov 24, 2004</h4>
 
 See the <a href="faq.html">FAQ</a> | <a href="#download">Download now</a> |
 <a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
@@ -43,14 +43,52 @@ separation features to enhance security.
 I recommend upgrading.
 
 <h2> Recent Changes </h2>
+
+Release 0.7.2 tightens the authentication screws with a "3 strikes and
+your out" policy.  A sender must have a valid PTR, HELO, or SPF record
+to send email.  Specific senders can be whitelisted using the
+"delegate" option in the spf configuration section by adding a
+default SPF record for them.  The PTR and HELO are required
+by RFC anyway, so this is not an unreasonable requirement.
+There is now a coherent policy for an SPF softfail result.  A softfail
+is accepted if there is a valid PTR or HELO, or if the domain
+is listed in the "accept_softfail" option of the spf configuration section.
+A neutral result is accepted by default if there is a valid PTR or
+HELO, (and the SPF record was not guessed), unless the domain is listed in the
+"reject_neutral" option.  Common forms of PTR records for dynamic IPs are
+recognized, and do not count as a valid PTR.  This does not prevent anyone
+from sending mail from a dynamic IP - they just need to configure a
+valid HELO name or publish an SPF record.
+<p>
 The RPM for release 0.7.0 moves the config file and socket locations to
 /etc/mail and /var/run/milter respectively.  We now parse Microsoft CID records
-- but only hotmail.com uses them.  They seem to have a patent on the brilliant
-idea of examining the mail headers to see who the message is from.
-We aren't doing that here, so not to worry - but I am not a lawyer, so if you
-are worried, change spf.py around line 626 to return None instead of
+- but only hotmail.com uses them.  They seem to have applied for a patent on
+the brilliant idea of examining the mail headers to see who the message is
+from.  We aren't doing that here, so not to worry - but I am not a lawyer, so
+if you are worried, change spf.py around line 626 to return None instead of
 calling CIDParser().  There is a new option to reject mail with no PTR
 and no SPF.
+<p>
+Microsoft is pushing an anti-opensource license for their pending patent
+along with their sender-ID proposal before the IETF.
+It is royalty free - but requires anyone distributing a binary they've
+compiled from source to sign a license agreement.  The Apache Software
+Foundation <a
+href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
+the problem with sender-ID</a>, and Debian <a
+href="http://www.debian.org/News/2004/20040904">concurs</a>.  Since 
+the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
+<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
+software in general</a> and the <a
+href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
+particular</a>, Python milter will not be able to implement sender-ID in its
+current form.  This was, no doubt, Microsoft's intent all along.
+<p>
+Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers.
+Unlike SPF, it has never been tried, and is encumbered by a stupid patent.  I
+recommend ignoring it and continuing to implement and improve SPF until a
+working and unencumbered proposal for RFC2822 headers surfaces.
+
 <p>
 <a href="http://spf.pobox.com">
 <img src="SPF.gif" align=left alt="SPF logo"></a>
@@ -168,13 +206,40 @@ in the <a href="milter.cfg">sample config file</a> for the bms.py milter.
 
 <h3><a name=download>Downloading</a></h3>
 
-The latest stable release is <a href="#stable">0.7.0</a>. A stable
+The latest stable release is <a href="#stable">0.7.2</a>. A stable
 release is one which has been installed (and working correctly) on
 production systems long enough to convince me that it is stable.  As
 the package gains more features and complexity, stable will mean no
 bug reports from outside users either.
 <p>
-The latest version is 0.7.0-1.  See the <a href=NEWS>Change Log</a>.
+The latest version is 0.7.2-2.  See the <a href=NEWS>Change Log</a>.
+PLEASE NOTE - if you are using the modules, but not the bms milter application,
+then ignore the RPMs and milter.spec.  Use 'python setup.py bdist_rpm' to 
+build source and binary rpms that do not include the milter application.
+<p>
+I want to split the bms milter application to a new project once I figure
+out the renaming.  The current plan is to rename 'milter' to 'pymilter', which
+will have the Python modules.  The bms milter application will still be named
+'milter' and depend on pymilter (so that my installs won't notice anything).
+<p>
+<a name="stable"><b>Stable</b></a>
+<a href="http://bmsi.com/python/milter-0.7.2.tar.gz">
+milter-0.7.2.tar.gz</a> Three strikes and your out policy.  Some SPF fixes.
+Recognizes PTR records for dynamic IPs.
+<p>
+<a href="http://bmsi.com/python/milter-0.7.1.tar.gz">
+milter-0.7.1.tar.gz</a> Support setmlreply, handle some more exceptions
+for malformed spam.  Compiling pymilter with sendmail-8.12.10, requires
+sendmail-devel with _FFR_MULTILINE set.  The binary will work with older
+sendmails.  The _FFR_MULTILINE option only affects libmilter.a.
+<br>
+<a href="http://bmsi.com/linux/rh72/milter-0.7.1-1.i386.rpm">
+milter-0.7.1-1.i386.rpm</a> Binary RPM for Redhat 7.x, now requires 
+	sendmail-8.12 and <a href="http://www.python.org/2.3.3/rpms.html">
+	python2.3</a>.
+<br>
+<a href="http://bmsi.com/linux/rh9/milter-0.7.1-1.src.rpm">
+milter-0.7.1-1.src.rpm</a> Source RPM for Redhat 9,7.x. 
 <p>
 <a name="stable"><b>Stable</b></a>
 <a href="http://bmsi.com/python/milter-0.7.0.tar.gz">
@@ -191,6 +256,9 @@ milter-0.7.0-1rh9.i386.rpm</a> Binary RPM for Redhat 9, requires
 	sendmail-8.12 and <a href="http://www.python.org/2.3.3/rpms.html">
 	python2.3</a>.
 <br>
+<a href="http://bmsi.com/aix/milter-0.7.0-1.ppc.rpm">
+milter-0.7.0-1.ppc.rpm</a> Binary RPM for AIX, requires sendmail-8.13.1.
+<br>
 <a href="http://bmsi.com/linux/rh9/milter-0.7.0-1.src.rpm">
 milter-0.7.0-1.src.rpm</a> Source RPM for Redhat 9,7.x.  
 <p>
diff --git a/milter.spec b/milter.spec
index d9babc262277e6f9f3b7c372bb7a2b42bcdd6ef9..0bb7fe3732cfd0033d6a0b8862e249059e2fe29e 100644
--- a/milter.spec
+++ b/milter.spec
@@ -1,6 +1,6 @@
 %define name milter
-%define version 0.7.1
-%define release 1
+%define version 0.7.2
+%define release 2
 # Redhat 7.x and earlier (multiple ps lines per thread)
 %define sysvinit milter.rc7
 # RH9, other systems (single ps line per process)
@@ -25,6 +25,9 @@ Vendor: Stuart D. Gathman <stuart@bmsi.com>
 Packager: Stuart D. Gathman <stuart@bmsi.com>
 Url: http://www.bmsi.com/python/milter.html
 Requires: %{python} >= 2.2.2, sendmail >= 8.12.10
+%ifnos aix4.1
+Requires: chkconfig
+%endif
 BuildRequires: %{python}-devel >= 2.2.2, sendmail-devel >= 8.12.10
 
 %description
@@ -59,10 +62,15 @@ EOF
 
 # purge saved defanged message copies
 mkdir -p $RPM_BUILD_ROOT/etc/cron.daily
+%ifos aix4.1
+R=
+%else
+R='-r'
+%endif
 cat >$RPM_BUILD_ROOT/etc/cron.daily/milter <<'EOF'
 #!/bin/sh
 
-find /var/log/milter/save -mtime +7 | xargs -r rm
+find /var/log/milter/save -mtime +7 | xargs $R rm
 EOF
 chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter
 
@@ -96,6 +104,8 @@ EOF
 chmod a+x $RPM_BUILD_ROOT/var/log/milter/start.sh
 
 mkdir -p $RPM_BUILD_ROOT/var/run/milter
+mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
+cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
 
 %ifos aix4.1
 %post
@@ -107,7 +117,13 @@ if [ $1 = 0 ]; then
 fi
 %else
 %post
-echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
+#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
+/sbin/chkconfig --add milter
+
+%preun
+if [ $1 = 0 ]; then
+  /sbin/chkconfig --del milter
+fi
 %endif
 
 %clean
@@ -130,8 +146,19 @@ rm -rf $RPM_BUILD_ROOT
 %config /var/log/milter/start.sh
 %config /var/log/milter/bms.py
 %config(noreplace) /etc/mail/pymilter.cfg
+/usr/share/sendmail-cf/hack/rhsbl.m4
 
 %changelog
+* Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1
+- Fix various SPF bugs
+- Recognize dynamic PTR names, and don't count them as authentication.
+- Three strikes and yer out rule.
+- Block softfail by default unless valid PTR or HELO
+- Return unknown for null mechanism
+- Return unknown for invalid ip address in mechanism
+- Try best guess on HELO also
+- Expand setreply for common errors
+- make rhsbl.m4 hack available for sendmail.mc
 * Sun Aug 22 2004 Stuart Gathman <stuart@bmsi.com> 0.7.1-1
 - Handle modifying mislabeled multipart messages without an exception
 - Support setbacklog, setmlreply
diff --git a/rhsbl.m4 b/rhsbl.m4
new file mode 100644
index 0000000000000000000000000000000000000000..8f886c794c946f40d53d83d7fa464202b8a83734
--- /dev/null
+++ b/rhsbl.m4
@@ -0,0 +1,44 @@
+divert(-1)
+#
+# Copyright (c) 2002 Derek J. Balling
+#	All rights reserved.
+#
+# Permission to use granted for all purposes. If modifications are made
+# they are requested to be sent to <dredd@megacity.org> for inclusion in future
+# versions 
+#
+# Allows (hopefully) for checking of access.db whitelisting now. This ONLY
+# works on sendmail-8.12.x ... use on any other version may require tinkering
+# by you the downloader.
+#
+# Incorporates many changes by Sergey S. Mokryshev <mokr@mokr.net>
+#
+#
+
+divert(0)
+ifdef(`_RHSBL_R_',`dnl',`dnl
+VERSIONID(`$Id$')
+define(`_RHSBL_R_',`')
+ifdef(`_DNSBL_R_',`dnl',`dnl
+LOCAL_CONFIG
+# map for DNS based blacklist lookups based on the sender RHS
+Kdnsbl host -T<TMP>')')
+divert(-1)
+define(`_RHSBL_SRV_', `_ARG_')dnl
+define(`_RHSBL_MSG_', `ifelse(len(X`'_ARG2_),`1',`"550 Mail from " $`'&{RHS} " refused by blackhole site '_RHSBL_SRV_`"',`_ARG2_')')dnl
+define(`_RHSBL_MSG_TMP_', `ifelse(_ARG3_,`t',`"451 Temporary lookup failure of " $`'&{RHS} " at '_RHSBL_SRV_`"',`_ARG3_')')dnl
+
+MAILER_DEFINITIONS
+
+SLocal_check_mail
+# DNS based RHS spam list blackholes.bmsi.com
+R$*			$: <?> $>CanonAddr $1
+R<?> $*<@$+.>		$: <?> $1<@$2.> $| $>SearchList <+ rhs> $| <F:$1@$2> <D:$2> <>
+R<?> $* $| <$={Accept}>	$: OKSOFAR
+R<?> $*<@$+.> $| $*	$: <?> $(dnsbl $2._RHSBL_SRV_. $: OK $) $(macro {RHS} $@ $2 $)
+R<?> OK			$: OKSOFAR
+R<?> $*<@$*>		$: OKSOFAR
+ifelse(len(X`'_ARG3_),`1',
+`R<?>$+<TMP>		$: TMPOK',
+`R<?>$+<TMP>		$#error $@ 4.7.1 $: _RHSBL_MSG_TMP_')
+R<?>$+			$#error $@ 5.7.1 $: _RHSBL_MSG_
diff --git a/setup.py b/setup.py
index 42a2b61712d541ee012dbf476f8cb63632f70ecf..a1c980cf2b47001c609603767811544283bccd6d 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ if sys.version < '2.2.3':
   DistributionMetadata.classifiers = None
   DistributionMetadata.download_url = None
 
-setup(name = "milter", version = "0.7.1",
+setup(name = "milter", version = "0.7.2",
 	description="Python interface to sendmail milter API",
 	long_description="""\
 This is a python extension module to enable python scripts to
diff --git a/spf.py b/spf.py
index 51450fad38438d241f9e3ebfc7750777baaa96cf..8ce7844eb8e5272e5f7f0dcace0c6e83bb0577c5 100755
--- a/spf.py
+++ b/spf.py
@@ -45,6 +45,25 @@ For news, bugfixes, etc. visit the home page for this implementation at
 # Terrence is not responding to email.
 #
 # $Log$
+# Revision 1.21  2004/11/20 16:37:03  stuart
+# Handle multi-segment TXT records.
+#
+# Revision 1.20  2004/11/19 06:10:30  stuart
+# Use PermError exception instead of reporting unknown.
+#
+# Revision 1.19  2004/11/09 23:00:18  stuart
+# Limit recursion and DNS lookups separately.
+#
+#
+# Revision 1.17  2004/09/10 18:08:26  stuart
+# Return unknown for null mechanism
+#
+# Revision 1.16  2004/09/04 23:27:06  stuart
+# More mechanism aliases.
+#
+# Revision 1.15  2004/08/30 21:19:05  stuart
+# Return unknown for invalid ip syntax in mechanism
+#
 # Revision 1.14  2004/08/23 02:28:24  stuart
 # Remove Perl usage message.
 #
@@ -120,7 +139,7 @@ import xml.sax
 # Date: 2004-02-25
 #
 # A complete reverse translation (SPF -> CID) might be impossible, since
-# there are no way to handle:
+# there are no ways to handle:
 # - PTR and EXISTS mechanism 
 # - MX mechanism with an different domain as argument
 # - macros
@@ -284,6 +303,16 @@ except NameError:
 # standard default SPF record
 DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
 
+# maximum DNS lookups allowed
+MAX_LOOKUP = 50
+MAX_RECURSION = 20
+
+class TempError(Exception):
+	"Temporary SPF error"
+
+class PermError(Exception):
+	"Permanent SPF error"
+
 def check(i, s, h,local=None):
 	"""Test an incoming MAIL FROM:<s>, from a client with ip address i.
 	h is the HELO/EHLO domain name.
@@ -329,6 +358,7 @@ class query(object):
 		self.cache = {}
 		self.exps = dict(EXPLANATIONS)
 		self.local = local	# local policy
+    		self.lookups = 0
 
 	def set_default_explanation(self,exp):
 		exps = self.exps
@@ -357,27 +387,32 @@ class query(object):
 			return ('pass', 250, 'local connections always pass')
 
 		try:
+			self.lookups = 0
 			if not spf:
 			    spf = self.dns_spf(self.d)
 			if self.local and spf:
 			    spf += ' ' + self.local
 			return self.check1(spf, self.d, 0)
-		except DNS.DNSError:
-			return ('error', 450, 'SPF DNS Error')
+		except DNS.DNSError,x:
+			return ('error', 450, 'SPF DNS Error: ' + str(x))
+		except TempError,x:
+			return ('error', 450, 'SPF Temporary Error: ' + str(x))
+		except PermError,x:
+			return ('error', 550, 'SPF Permanent Error: ' + str(x))
 
 	def check1(self, spf, domain, recursion):
 		# spf rfc: 3.7 Processing Limits
 		#
-		if recursion > 20:
-			self.prob =  'Mechanisms used too many DNS lookups'
+		if recursion > MAX_RECURSION:
+			self.prob =  'Too many levels of recursion'
 			return ('unknown', 250, 'SPF recursion limit exceeded')
 		try:
 			tmp, self.d = self.d, domain
-			return self.check0(spf, recursion)
+			return self.check0(spf,recursion)
 		finally:
 			self.d = tmp
 
-	def check0(self, spf, recursion):
+	def check0(self, spf,recursion):
 		"""Test this query information against SPF text.
 
 		Returns (result, mta-status-code, explanation) where
@@ -425,35 +460,30 @@ class query(object):
 			m, arg, cidrlength = parse_mechanism(mech, self.d)
 
 			# map '?' '+' or '-' to 'unknown' 'pass' or 'fail'
-			result = RESULTS.get(m[0])
-			if result:
+			if m:
+			  result = RESULTS.get(m[0])
+			  if result:
 				# eat '?' '+' or '-'
 				m = m[1:]
-			else:
+			  else:
 				# default pass
 				result = 'pass'
 
-			if m in ['a', 'mx', 'ptr', 'exists', 'include']:
+			if m in ['a', 'mx', 'ptr', 'prt', 'exists', 'include']:
 				arg = self.expand(arg)
 
 			if m == 'include':
-			    if arg != self.d:
-				res,code,txt = self.check1(self.dns_spf(arg),
-						  arg, recursion + 1)
-				if res == 'pass':
-					break
-				if res in ('fail','neutral','softfail'):
-					continue
-				if res == 'none':
-				  	self.prob = \
-					  'Could not find a valid SPF record'
-				  	res = 'unknown'
-				return res,code,txt
-			    else:
-			    	self.prob = 'Required option is missing'
-				self.mech.append(mech)
-				return ('unknown', 250, 'missing SPF option')
-
+			  if arg != self.d:
+			    res,code,txt = self.check1(self.dns_spf(arg),
+					      arg, recursion + 1)
+			    if res == 'pass':
+			      break
+			    if res == 'none':
+			      raise PermError(
+			      	'No valid SPF record for included domain')
+			    continue
+			  else:
+			    raise PermError('include mechanism missing domain')
 			elif m == 'all':
 				break
 
@@ -472,9 +502,15 @@ class query(object):
 					break
 
 			elif m in ('ip4', 'ipv4', 'ip') and arg != self.d:
+			    try:
 				if cidrmatch(self.i, [arg], cidrlength):
-					break
-			elif m == 'ip6':
+				    break
+			    except socket.error:
+				self.mech.append(mech)
+				self.prob = 'Bad mechanism syntax found'
+				return ('unknown',250,'SPF mechanism syntax error')
+			        
+			elif m in ('ip6', 'ipv6'):
 			# Until we support IPV6, we should never
 			# get an IPv6 connection.  So this mech
 			# will never match.
@@ -486,17 +522,14 @@ class query(object):
 					break
 
 			else:
-				# unknown mechanisms cause immediate unknown
-				# abort results
-				self.mech.append(mech)
-				self.prob = 'Unknown mechanism found'
-				return ('unknown',250,'unknown SPF mechanism')
-
+			  # unknown mechanisms cause immediate unknown
+			  # abort results
+			  raise PermError('Unknown mechanism found: ' + mech)
 		else:
 			# no matches
 			if redirect:
 				return self.check1(self.dns_spf(redirect),
-				                   redirect, recursion+1)
+				                   redirect, recursion + 1)
 			else:
 				result = default
 
@@ -630,7 +663,7 @@ class query(object):
 	def dns_txt(self, domainname):
 		"Get a list of TXT records for a domain name."
 		if domainname:
-		  return [t for a in self.dns(domainname, 'TXT') for t in a]
+		  return [''.join(a) for a in self.dns(domainname, 'TXT')]
 		return []
 
 	def dns_mx(self, domainname):
@@ -672,6 +705,9 @@ class query(object):
 		pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
 		post: isinstance(__return__, types.ListType)
 		"""
+		self.lookups += 1
+		if self.lookups > MAX_LOOKUP:
+			raise PermError('Too many DNS lookups')
 		result = self.cache.get( (name, qtype) )
 		cname = None
 		if not result:
diff --git a/test/missingboundary b/test/missingboundary
new file mode 100644
index 0000000000000000000000000000000000000000..b59c3e1be274e76110699525610d858e6f4bf9c8
--- /dev/null
+++ b/test/missingboundary
@@ -0,0 +1,128 @@
+From leec@windowsshop.com Fri Sep 10 11:48:25 2004
+Message-ID: <4141CDD4.7040305@windowsshop.com>
+Date: Fri, 10 Sep 2004 11:52:52 -0400
+From: Lee Connor <leec@windowsshop.com>
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.4) Gecko/20030624 Netscape/7.1 (ax)
+X-Accept-Language: en-us, en
+MIME-Version: 1.0
+To: Cleo Matthews-Conley <cleom@windowsshop.com>, 
+ Tony Collini <tonyc@windowsshop.com>,
+ John Higinbothom <johnh@windowsshop.com>
+CC: Rich Higgins <richh@windowsshop.com>
+Subject: [Fwd: [Fwd: Customer Concerns]]
+Content-Type: multipart/mixed;
+ boundary="------------020209070802060007090105"
+
+This is a multi-part message in MIME format.
+--------------020209070802060007090105
+Content-Type: text/plain; charset=us-ascii; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Cleo - please review attached feedback from Sales team.......I recall at 
+an early meeting after we moved in you and Tony (and maybe 1 or 2 
+others) were going to develop a voice mail procedure or instruction 
+sheet for all staff. It looks like we really need this to get what we 
+are looking for from the system. Please let me know when you can produce 
+this and give a draft to the managers here for review.
+Thanks,
+Lee
+
+
+--------------020209070802060007090105
+Content-Type: message/rfc822;
+ name="[Fwd: Customer Concerns]"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline;
+ filename="[Fwd: Customer Concerns]"
+
+Return-Path: <richh@windowsshop.com>
+Received: from windowsshop.com (pc147.windowsshop.com [192.168.100.147] (may be forged))
+	by lord.windowsshop.com (8.12.10/8.12.10) with ESMTP id i89KCClX003425
+	for <leec@windowsshop.com>; Thu, 9 Sep 2004 16:12:12 -0400
+Message-ID: <4140B851.3020501@windowsshop.com>
+Date: Thu, 09 Sep 2004 16:08:49 -0400
+From: Rich <richh@windowsshop.com>
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.0.2) Gecko/20021120 Netscape/7.01
+X-Accept-Language: en-us, en
+MIME-Version: 1.0
+To: Lee Connor <leec@windowsshop.com>
+Subject: [Fwd: Customer Concerns]
+Content-Type: multipart/mixed;
+ boundary="------------030301030706020401010801"
+X-DSpam-Score: 0.000000
+
+This is a multi-part message in MIME format.
+--------------030301030706020401010801
+Content-Type: text/plain; charset=us-ascii; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Lee - do you want me to do anything else with this?
+
+Rich
+
+<!DSPAM:FEE4D3278234264874834386>
+
+
+--------------030301030706020401010801
+Content-Type: message/rfc822; name="Customer Concerns";
+	boundary="===============0045392615=="
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline;
+ filename="Customer Concerns"
+
+
+Return-Path: <joes@windowsshop.com>
+Received: from joes (pc148.windowsshop.com [192.168.100.148] (may be forged))
+	by lord.windowsshop.com (8.12.10/8.12.10) with SMTP id i89K9BlX003262
+	for <richh@windowsshop.com>; Thu, 9 Sep 2004 16:09:11 -0400
+From: "Joe Schmuck" <joes@windowsshop.com>
+To: <richh@windowsshop.com>
+Subject: Customer Concerns
+Date: Thu, 9 Sep 2004 16:08:26 -0400
+Message-ID: <OFEPKHCCLPIECLFBLDHBAEAECAAA.joes@windowsshop.com>
+MIME-Version: 1.0
+Content-Type: text/plain;
+	charset="iso-8859-1"
+Content-Transfer-Encoding: 7bit
+X-Priority: 3 (Normal)
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)
+Importance: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2800.1106
+X-DSpam-Score: 0.000000
+
+Rich:
+
+Following is a summary of concerns from customers regarding internal
+communications within WS:
+
+	- Not all employees have activated their voice mail - when this is the
+case, the system will automatically cut you off
+	- When employees are out of the office, phones are not forwarded to a back
+up, ie manager
+	- Reception has no record of employee attendance, and therefore will
+forward call to individual requested - see point 2
+ 	- Reception directs calls to incorrect individuals
+ 	- When entering voice mail, if you press '0', system does not default to
+operator, but puts you back into individual 	voice mail
+	- Reception phone demeanor has no 'pep'
+
+Thanks
+Joe
+
+
+
+
+---
+Outgoing mail is certified Virus Free.
+Checked by AVG anti-virus system (http://www.grisoft.com).
+Version: 6.0.752 / Virus Database: 503 - Release Date: 9/3/2004
+
+
+<!DSPAM:FEE4D05F1332634871908793>
+
+--===============0045392615==--
+--------------030301030706020401010801--
+
+--------------020209070802060007090105--
+