diff --git a/Milter.py b/Milter.py
index d42f3fd1d228bab7e5b4895b2fd7de36ed27f980..ac8d3f448b7f8ba60bbdfb902586202b5b6bb033 100755
--- a/Milter.py
+++ b/Milter.py
@@ -8,15 +8,12 @@ import milter
 import thread
 
 from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL,	\
-  set_flags, setdbg, \
+  set_flags, setdbg, setbacklog, settimeout, \
   ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS,	\
   V1_ACTS, V2_ACTS, CURR_ACTS
 
-try:
-  from milter import QUARANTINE
-except:
-  #print 'No QUARANTINE support'
-  pass
+try: from milter import QUARANTINE
+except: pass
 
 _seq_lock = thread.allocate_lock()
 _seq = 0
@@ -100,8 +97,10 @@ class Milter:
   def getsymval(self,sym):
     return self.__ctx.getsymval(sym)
 
-  def setreply(self,rcode,xcode,msg):
-    return self.__ctx.setreply(rcode,xcode,msg)
+  # If sendmail does not support setmlreply, then only the
+  # first msg line is used.
+  def setreply(self,rcode,xcode=None,msg=None,*ml):
+    return self.__ctx.setreply(rcode,xcode,msg,*ml)
 
   # Milter methods which can only be called from eom callback.
   def addheader(self,field,value):
@@ -119,6 +118,8 @@ class Milter:
   def replacebody(self,body):
     return self.__ctx.replacebody(body)
 
+  # When quarantined, a message goes into the mailq as if to be delivered,
+  # but delivery is deferred until the message is unquarantined.
   def quarantine(self,reason):
     return self.__ctx.quarantine(reason)
 
diff --git a/NEWS b/NEWS
index 218ac3850982d72ec98c8ca4bc3630b745d7782d..6f82cafc7e0be4a918db7d97d22672c6cc64a089 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,9 @@
 Here is a history of user visible changes to Python milter.
 
+0.7.1	Handle modifying mislabeled multipart messages without an exception
+	Support setbacklog, setmlreply
+	Allow multi-recipient CBV
+	Return TEMPFAIL for SPF softfail
 0.7.0	SPF check hello name
 	Move pythonsock to /var/run/milter
 	Move milter.cfg to /etc/mail/pymilter.cfg
diff --git a/TODO b/TODO
index 372aa7019cafc0bcf8741849e99d59b65337edf5..cc99affbb2327dad4e5055793e7efa6bc9071bad 100644
--- a/TODO
+++ b/TODO
@@ -1,17 +1,6 @@
-Message not saved for following traceback:
-Traceback (most recent call last):
-  File "/usr/lib/python2.3/site-packages/Milter.py", line 188, in <lambda>
-    milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
-  File "bms.py", line 935, in eom
-    msg.dump(out)
-  File "/usr/lib/python2.3/site-packages/mime.py", line 347, in dump
-    g.flatten(self,unixfrom=unixfrom)
-  File "/var/tmp/python2.3-2.3.3-root/usr/lib/python2.3/email/Generator.py", line 102, in flatten
-  File "/var/tmp/python2.3-2.3.3-root/usr/lib/python2.3/email/Generator.py", line 130, in _write
-  File "/var/tmp/python2.3-2.3.3-root/usr/lib/python2.3/email/Generator.py", line 156, in _dispatch
-  File "/var/tmp/python2.3-2.3.3-root/usr/lib/python2.3/email/Generator.py", line 199, in _handle_text
-TypeError: string payload expected: <type 'list'>
-------------
+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
diff --git a/bms.py b/bms.py
index e592e4d4c833391bffe1688cb948c2bc609dec84..fffecf32d99236ce6ddc01f975cc34e9d504bcce 100644
--- a/bms.py
+++ b/bms.py
@@ -1,6 +1,17 @@
 #!/usr/bin/env python
 # A simple milter.
 # $Log$
+# Revision 1.117  2004/08/23 02:27:53  stuart
+# Allow multi rcpt CBV.  Add some multiline replies.
+#
+# Revision 1.116  2004/08/20 22:27:52  stuart
+# Generate TEMPFAIL for SPF softfail.
+#
+# Revision 1.115  2004/08/19 20:55:49  stuart
+# Always show reversed SRS path.
+# Check if encodings are an ASCII superset.  Some messages were encoded as
+# BIG5 and getting rejected even though chars were all in ascii subset.
+#
 # Revision 1.114  2004/07/27 00:40:12  stuart
 # Make reject on no PTR optional.
 #
@@ -435,7 +446,10 @@ def parse_header(val):
     u = []
     for s,enc in h:
       if enc:
-	u.append(unicode(s,enc))
+        try:
+	  u.append(unicode(s,enc))
+	except LookupError:
+	  u.append(unicode(s))
       else:
 	u.append(unicode(s))
     u = ''.join(u)
@@ -443,13 +457,14 @@ def parse_header(val):
       try:
 	return u.encode(enc)
       except UnicodeError: continue
-  except UnicodeDecodeError:
-    return val
-  except LookupError:
-    return val
+  except UnicodeDecodeError: pass
+  except LookupError: pass
+  return val
 
 class bmsMilter(Milter.Milter):
-  "Milter to replace attachments poisonous to Windows with a WARNING message."
+  """Milter to replace attachments poisonous to Windows with a WARNING message,
+     check SPF, and other anti-forgery features, and implement wiretapping
+     and smart alias redirection."""
 
   def log(self,*msg):
     print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id),
@@ -592,8 +607,13 @@ class bmsMilter(Milter.Milter):
 	# check hello name via spf
 	hres,hcode,htxt = spf.check(self.connectip,'',self.hello_name)
 	if hres in ('deny','fail','neutral','softfail'):
-	  self.log('REJECT: hello SPF: %s %i %s' % (hres,hcode,htxt))
-	  self.setreply('550','5.7.1',htxt)
+	  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."
+	  )
 	  return Milter.REJECT
       if spf_best_guess:
 	#self.log('SPF: no record published, guessing')
@@ -612,10 +632,26 @@ class bmsMilter(Milter.Milter):
       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 == 'neutral' and q.o in spf_reject_neutral:
       self.log('REJECT: SPF neutral for',q.s)
       self.setreply('550','5.7.1',
-	'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o
+	'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o,
+	'The %s domain is one that spammers love to forge.  Due to' % q.o,
+	'the volume of forged mail, we can only accept mail that',
+	'the SPF record for %s explicitly designates as legitimate.' % q.o,
+	'Sending your email through the recommended outgoing SMTP',
+	'servers for %s should accomplish this.' % q.o
       )
       return Milter.REJECT
     if res == 'error':
@@ -639,22 +675,20 @@ class bmsMilter(Milter.Milter):
       if self.mailfrom == '<>' or self.canon_from.startswith('postmaster@') \
       	or self.canon_from.startswith('mailer-daemon@'):
         if self.recipients:
-	  self.log('REJECT: Multiple bounce recipients')
-	  self.setreply('550','5.7.1','Multiple bounce recipients')
-	  return Milter.REJECT
-        if srs and not (self.internal_connection or self.trusted_relay) \
-		and domain == srs_fwdomain:
+	  self.data_allowed = False
+        if srs and domain == srs_fwdomain:
 	  oldaddr = '@'.join(parse_addr(to))
 	  try:
 	    newaddr = srs.reverse(oldaddr)
 	    # Currently, a sendmail map reverses SRS.  We just log it here.
 	    self.log("srs rcpt:",newaddr)
 	  except:
-	    if srsre.match(oldaddr):
-	      self.log("REJECT: srs spoofed:",oldaddr)
-	      self.setreply('550','5.7.1','Invalid SRS signature')
-	      return Milter.REJECT
-	    self.data_allowed = not srs_reject_spoofed
+            if not (self.internal_connection or self.trusted_relay):
+	      if srsre.match(oldaddr):
+		self.log("REJECT: srs spoofed:",oldaddr)
+		self.setreply('550','5.7.1','Invalid SRS signature')
+		return Milter.REJECT
+	      self.data_allowed = not srs_reject_spoofed
       # non DSN mail to SRS address will bounce due to invalid local part
       self.recipients.append('@'.join(t))
       users = check_user.get(domain)
@@ -748,8 +782,12 @@ class bmsMilter(Milter.Milter):
 
   def header(self,name,hval):
     if not self.data_allowed:
-      self.log('REJECT: bounce with no SRS encoding')
-      self.setreply('550','5.7.1',"I did not send you this message.")
+      if len(self.recipients) > 1:
+	self.log('REJECT: Multiple bounce recipients')
+	self.setreply('550','5.7.1','Multiple bounce recipients')
+      else:
+	self.log('REJECT: bounce with no SRS encoding')
+	self.setreply('550','5.7.1',"I did not send you that message.")
       return Milter.REJECT
     lname = name.lower()
     # decode near ascii text to unobfuscate
@@ -757,8 +795,8 @@ class bmsMilter(Milter.Milter):
     if not self.internal_connection:
       # even if we wanted the Taiwanese spam, we can't read Chinese
       if block_chinese and lname == 'subject':
-	if hval.startswith('=?big5') or hval.startswith('=?ISO-2022-JP'):
-	  self.log('REJECT: %s: %s' % (name,hval))
+	if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'):
+	  self.log('REJECT: %s: %s' % (name,val))
 	  self.setreply('550','5.7.1',"We don't understand chinese")
 	  return Milter.REJECT
       rc = self.check_header(name,val)
@@ -770,7 +808,7 @@ class bmsMilter(Milter.Milter):
       try:
         val = val.encode('us-ascii')
       except:
-        val = hval
+	val = hval
       self.fp.write("%s: %s\n" % (name,val))	# add header to buffer
     return Milter.CONTINUE
 
diff --git a/milter.cfg b/milter.cfg
index f722c445d8728df42a421278795c060c1a4abae9..e5f8d5287a83f7d408144d72ccb406cb128f1918 100644
--- a/milter.cfg
+++ b/milter.cfg
@@ -1,52 +1,66 @@
 # features intended to filter or block incoming mail
 [milter]
+# the socket used to communicate with sendmail.  Must match sendmail.cf
 ;socket=/var/run/milter/pythonsock
+# where to save original copies of defanged and failed messages
 tempdir = /var/log/milter/save
+# how long to wait for a response from sendmail before giving up 
 ;timeout=600
 
+# do virus scanning on attached messages also
 scan_rfc822 = 1
-# can be CPU intensive
+# Comment out scripts in HTML attachments.  Can be CPU intensive.
 scan_html = 0
-# reject asian fonts because we can't read them
+# reject messages with asian fonts because we can't read them
 block_chinese = 1
-# users who hate forwarded mail
+# list users who hate forwarded mail
 ;block_forward = egghead@mycorp.com, busybee@mycorp.com
 log_headers = 0
 # Reject mail for domains mentioned unless user is mentioned here also
 ;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com
-# porn words are case insensitive
+# 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,
 	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
-# spam words are case sensitive
+# reject mail with these case sensitive strings in the subject
 spam_words = $$$, !!!, XXX, FREE, HGH
 
 # connection ips and hostnames are matched against this glob style list
 # to recognize internal senders
 ;internal_connect = 192.168.*.*
+
 # mail that is not an internal_connect and claims to be from an
-# internal domain is rejected.
+# internal domain is rejected.  You should enable SPF instead if you can.
+# SPF is much more comprehensive and flexible.
 ;internal_domains = mycorp.com
+
 # connections from a trusted relay can trust the first Received header
+# SPF checks are bypassed for internal connections and trusted relays.
 ;trusted_relay = 1.2.3.4, 66.12.34.56
+
 # reject external senders with hello names no legit external sender would use
+# SPF will do this also, but listing your own domain and mailserver here
+# will save some DNS lookups when rejecting certain viruses.
 ;hello_blacklist = mycorp.com, 66.12.34.56
 
+# See http://bmsi.com/python/pysrs.html for details
 [srs]
 config=/etc/mail/pysrs.cfg
+# SRS options can be set here also, but must match the sendmail plugin
 ;secret="shhhh!"
 ;maxage=21
 ;hashlength=4
 ;database=/var/log/milter/srsdata
 ;fwdomain = mydomain.com
-# turn this on after a grace period
+# turn this on after a grace period to reject spoofed DSNs
 reject_spoofed = 0
 
+# See http://spf.pobox.com for more info on SPF.
 [spf]
 # namespace where SPF records can be supplied for domains without one
-# records are search for under _spf.domain.com
+# records are searched for under _spf.domain.com
 ;delegate = domain.com
 # domains where a neutral SPF result should cause mail to be rejected
 ;reject_neutral = aol.com
@@ -57,9 +71,9 @@ reject_spoofed = 0
 
 # features intended to clean up outgoing mail
 [scrub]
-# domains that stupidly block visible private nodes
+# domains that block visible private nodes
 ;hide_path = jcpenney.com	
-# block, don't just replace with warning, viruses from these domains
+# reject, don't just replace with warning, viruses from these domains
 ;reject_virus_from = mycorp.com
 
 # features intended for spying on users and coworkers
@@ -88,16 +102,18 @@ blind = 1
 ;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com,
 ;	walter@bigcorp.com
 
+# See http://bmsi.com/python/dspam.html
 [dspam]
-# Select a well moderated dspam dictionary to reject spammy headers
-# dspam-python must be installed to use: http://bmsi.com/python/dspam.html
+# Select a well moderated dspam dictionary to reject spammy headers.
+# To filter on the entire message, use the full setup below.
 # only EXTERNAL messages are dspam filtered
 ;dspam_dict=/var/lib/dspam/moderator.dict
+
 # Opt-opt recipients from dspam screening and header triage
 ;dspam_exempt=getitall@mycorp.com
 # Do not scan mail (ostensibly) from these senders
 ;dspam_whitelist=getitall@sender.com
-# Reject spam to these domains, perhaps because we are a backup MX server
+# Reject spam to these domains instead of quarantining it.
 ;dspam_reject=othercorp.com
 
 # directory for dspam user quarantine, signature db, and dictionaries
@@ -115,8 +131,9 @@ blind = 1
 ;spam=spam@foocorp.com
 # address to forward false positives to.  milter will process and not deliver
 ;falsepositive=ham@foocorp.com
-# the dspam_screener is used to screen mail for all recipients who are
-# not dspam_users.  Spam goes to the screeners quarantine, and the original
-# recipients saved so that false positives can be properly delivered.
-
+# the dspam_screener is a list of dspam users who screen mail for all
+# recipients who are not dspam_users.  Spam goes to the screeners quarantine,
+# and the original recipients are saved so that false positives can be properly
+# delivered.
+;dspam_screener=david,goliath
 # The dspam CGI can also be used: logins must match dspam users
diff --git a/milter.html b/milter.html
index 5a5dc50e58d15aace635b7a008bebd50ad527940..34c03e0546eafdfc33ff93c02badcc0beea2e88a 100644
--- a/milter.html
+++ b/milter.html
@@ -13,8 +13,8 @@ ALT="Viewable With Any Browser" BORDER="0"></A>
 	usemap="#banner_4" alt="Your vote?">
 <map name="banner_4">
   <area shape="rect" coords="330,25,426,59"
-  	href="http://www.sepschool.org/survey/" alt="I Disagree">
-  <area shape="rect" coords="234,28,304,57" href="http://sepschool.org/" alt="I Agree">
+  	href="http://education-survey.org/" alt="I Disagree">
+  <area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree">
 </map>
 
 </P>
@@ -24,12 +24,14 @@ 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 Jun 08, 2004</h4>
+Last updated Aug 06, 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>
+<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
+<a href="#overview">Overview</a>
 <p>
-<img src="python55.gif" align=left alt="A Python">
+<a href="//www.python.org">
+<img src="python55.gif" align=left alt="A Python"></a>
 <a href="//www.sendmail.org/">Sendmail</a> introduced a
 <a href="http://www.milter.org/milter_api/api.html"> new API</a> beginning with version 8.10 -
 libmilter.  The milter module for <a href="//www.python.org">Python</a>
@@ -41,7 +43,17 @@ separation features to enhance security.
 I recommend upgrading.
 
 <h2> Recent Changes </h2>
-
+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
+calling CIDParser().  There is a new option to reject mail with no PTR
+and no SPF.
+<p>
+<a href="http://spf.pobox.com">
+<img src="SPF.gif" align=left alt="SPF logo"></a>
 Release 0.6.6 adds support for <a href="http://spf.pobox.com/">SPF</a>,
 a protocol to prevent forging of the envelope from address.  
 SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
@@ -106,7 +118,7 @@ entries.  Be sure to handle both Bcc and file copies, and designating what
 mail should be copied.  How should "outgoing" be defined?  Implementing it is
 easy once the configuration is designed.
 
-<h3>Overview</h3>
+<h3><a name=overview>Overview</a></h3>
 
 This package provides a robust toolkit for Python <a
 href="#milter">milters</a>, and the beginnings of a general purpose mail
@@ -138,21 +150,50 @@ methods that
 do nothing, and also provides wrappers for the libmilter methods to mutate
 the message.
 <p>
+The 'spf' module provides an implementation of <a href="http://spf.pobox.com">
+SPF</a> useful for detecting email forgery.
+<p>
+The 'mime' module provides a wrapper for the Python email package that
+fixes some bugs, and simplifies modifying selected parts of a MIME message.
+<p>
 Finally, the bms.py application is both a sample of how to use the
-Milter module, and the beginnings of a general purpose SPAM filtering,
-wiretapping, and Win32 virus protection milter.
+Milter and spf modules, and the beginnings of a general purpose SPAM filtering,
+wiretapping, SPF checking, and Win32 virus protecting milter.  It can
+make use of the <a href="pysrs.html">pysrs</a> package when available for
+SRS/SES checking and the <a href="dspam.html">pydspam</a> package for Bayesian
+content filtering.  SPF checking
+requires <a href="http://pydns.sourceforge.net/">
+pydns</a>.  Configuration documentation is currently included as comments
+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.6.9</a>. A stable
+The latest stable release is <a href="#stable">0.7.0</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.6.9-1.  See the <a href=NEWS>Change Log</a>.
+The latest version is 0.7.0-1.  See the <a href=NEWS>Change Log</a>.
 <p>
 <a name="stable"><b>Stable</b></a>
+<a href="http://bmsi.com/python/milter-0.7.0.tar.gz">
+milter-0.7.0.tar.gz</a> Move config file and default socket location.
+Parse M$ CID records.
+<br>
+<a href="http://bmsi.com/linux/rh72/milter-0.7.0-1.i386.rpm">
+milter-0.7.0-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.0-1rh9.i386.rpm">
+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/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>
 <a href="http://bmsi.com/python/milter-0.6.9.tar.gz">
 milter-0.6.9.tar.gz</a> Add SPF test suite driver, and validate
 spf.py against test suite.  Add best_guess and get_header to spf.py.
diff --git a/milter.spec b/milter.spec
index f1148ce5ea789ca4184aa3d358b6a2c79203ebb7..d9babc262277e6f9f3b7c372bb7a2b42bcdd6ef9 100644
--- a/milter.spec
+++ b/milter.spec
@@ -1,5 +1,5 @@
 %define name milter
-%define version 0.7.0
+%define version 0.7.1
 %define release 1
 # Redhat 7.x and earlier (multiple ps lines per thread)
 %define sysvinit milter.rc7
@@ -24,8 +24,8 @@ Prefix: %{_prefix}
 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
-BuildRequires: %{python}-devel >= 2.2.2, sendmail-devel >= 8.12
+Requires: %{python} >= 2.2.2, sendmail >= 8.12.10
+BuildRequires: %{python}-devel >= 2.2.2, sendmail-devel >= 8.12.10
 
 %description
 This is a python extension module to enable python scripts to
@@ -132,6 +132,11 @@ rm -rf $RPM_BUILD_ROOT
 %config(noreplace) /etc/mail/pymilter.cfg
 
 %changelog
+* Sun Aug 22 2004 Stuart Gathman <stuart@bmsi.com> 0.7.1-1
+- Handle modifying mislabeled multipart messages without an exception
+- Support setbacklog, setmlreply
+- allow multi-recipient CBV
+- return TEMPFAIL for SPF softfail
 * Fri Jul 23 2004 Stuart Gathman <stuart@bmsi.com> 0.7.0-1
 - SPF check hello name
 - Move pythonsock to /var/run/milter
diff --git a/miltermodule.c b/miltermodule.c
index 54c08d223aa89c36d242b565efaab1186e7cb5ef..29d23583c539e0a8c1b16d2990656d8d88ac1af5 100644
--- a/miltermodule.c
+++ b/miltermodule.c
@@ -33,6 +33,18 @@ $ python setup.py help
      libraries=["milter","smutil","resolv"]
 
  * $Log$
+ * Revision 2.31  2004/08/23 02:24:36  stuart
+ * Support setbacklog
+ *
+ * Revision 2.30  2004/08/21 20:29:53  stuart
+ * Support option of 11 lines max for mlreply.
+ *
+ * Revision 2.29  2004/08/21 04:14:29  stuart
+ * mlreply support
+ *
+ * Revision 2.28  2004/08/21 02:45:21  stuart
+ * Don't leak int constants if module unloaded.
+ *
  * Revision 2.27  2004/04/06 03:19:59  stuart
  * Release 0.6.8
  *
@@ -127,11 +139,20 @@ $ python setup.py help
  *
  */
 
+#ifndef MAX_ML_REPLY
+#define MAX_ML_REPLY 32
+#endif
+#if MAX_ML_REPLY != 1 && MAX_ML_REPLY != 32 && MAX_ML_REPLY != 11
+#error MAX_ML_REPLY must be 1 or 11 or 32
+#endif
+#define _FFR_MULTILINE (MAX_ML_REPLY > 1)
+
 #include <pthread.h>
 #include <netinet/in.h>
 #include <Python.h>
 #include <libmilter/mfapi.h>
 
+
 /* See if we have IPv4 and/or IPv6 support in this OS and in
  * libmilter.  We need to make several macro tests because some OS's
  * may define some if IPv6 is only partially supported, and we may
@@ -746,6 +767,18 @@ milter_setdbg(PyObject *self, PyObject *args) {
   return _generic_return(smfi_setdbg(val), "cannot set debug value");
 }
 
+static char milter_setbacklog__doc__[] =
+"setbacklog(int) -> None\n\
+Set the TCP connection queue size for the milter socket.";
+
+static PyObject *
+milter_setbacklog(PyObject *self, PyObject *args) {
+   int val;
+
+   if (!PyArg_ParseTuple(args, "i:setbacklog", &val)) return NULL;
+   return _generic_return(smfi_setbacklog(val), "cannot set backlog");
+}
+
 static char milter_settimeout__doc__[] =
 "settimeout(int) -> None\n\
 Set the time (in seconds) that sendmail will wait before\n\
@@ -820,13 +853,54 @@ static PyObject *
 milter_setreply(PyObject *self, PyObject *args) {
   char *rcode;
   char *xcode;
-  char *message;
+  char *message[MAX_ML_REPLY];
+  char fmt[MAX_ML_REPLY + 16];
   SMFICTX *ctx;
-  if (!PyArg_ParseTuple(args, "szz:setreply", &rcode, &xcode, &message))
+  int i;
+  strcpy(fmt,"sz|");
+  for (i = 0; i < MAX_ML_REPLY; ++i) {
+    message[i] = 0;
+    fmt[i+3] = 's';
+  }
+  strcpy(fmt+i+3,":setreply");
+  if (!PyArg_ParseTuple(args, fmt,
+	&rcode, &xcode, message
+#if MAX_ML_REPLY > 1
+	,message+1,message+2,message+3,message+4,message+5,message+6,
+	message+7,message+8,message+9,message+10
+#if MAX_ML_REPLY > 11
+	,message+11,message+12,message+13,message+14,message+15,
+	message+16,message+17,message+18,message+19,message+20,
+	message+21,message+22,message+23,message+24,message+25,
+	message+26,message+27,message+28,message+29,message+30,
+	message+31
+#endif
+#endif
+  ))
     return NULL;
   ctx = _find_context(self);
   if (ctx == NULL) return NULL;
-  return _generic_return(smfi_setreply(ctx, rcode, xcode, message),
+#if MAX_ML_REPLY > 1
+  /*
+   * C varargs might be convenient for some things, but they sure are a pain
+   * when the number of args is not known at compile time.
+   */
+  if (message[0] && message[1])
+    return _generic_return(smfi_setmlreply(ctx, rcode, xcode,
+	  message[0],
+	  message[1],message[2],message[3],message[4],message[5],
+	  message[6],message[7],message[8],message[9],message[10],
+#if MAX_ML_REPLY > 11
+	  message[11],message[12],message[13],message[14],message[15],
+	  message[16],message[17],message[18],message[19],message[20],
+	  message[21],message[22],message[23],message[24],message[25],
+	  message[26],message[27],message[28],message[29],message[30],
+	  message[31],
+#endif
+	  (char *)0
+    ), "cannot set reply");
+#endif
+  return _generic_return(smfi_setreply(ctx, rcode, xcode, message[0]),
 			 "cannot set reply");
 }
 
@@ -986,7 +1060,7 @@ milter_getpriv(PyObject *self, PyObject *args) {
   return o;
 }
 
-#if _FFR_QUARANTINE
+#ifdef SMFIF_QUARANTINE
 static char milter_quarantine__doc__[] =
 "quarantine(string) -> None\n\
 Place the message in quarantine.  A string with a description of the reason\n\
@@ -1035,7 +1109,7 @@ static PyMethodDef context_methods[] = {
   { "replacebody", milter_replacebody, METH_VARARGS, milter_replacebody__doc__},
   { "setpriv",     milter_setpriv,     METH_VARARGS, milter_setpriv__doc__},
   { "getpriv",     milter_getpriv,     METH_VARARGS, milter_getpriv__doc__},
-#if _FFR_QUARANTINE
+#ifdef SMFIF_QUARANTINE
   { "quarantine",  milter_quarantine,  METH_VARARGS, milter_quarantine__doc__},
 #endif
 #if _FFR_SMFI_PROGRESS
@@ -1081,6 +1155,7 @@ static PyMethodDef milter_methods[] = {
    { "main",                 milter_main,                 METH_VARARGS, milter_main__doc__},
    { "setdbg",               milter_setdbg,               METH_VARARGS, milter_setdbg__doc__},
    { "settimeout",           milter_settimeout,           METH_VARARGS, milter_settimeout__doc__},
+   { "setbacklog",           milter_setbacklog,           METH_VARARGS, milter_setbacklog__doc__},
    { "setconn",              milter_setconn,              METH_VARARGS, milter_setconn__doc__},
    { "stop",                 milter_stop,                 METH_VARARGS, milter_stop__doc__},
    { NULL, NULL }
@@ -1116,6 +1191,12 @@ allowing one to write email filters directly in Python.\n\
 Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
 See <sendmailsource>/libmilter/README for details on setting it up.\n";
 
+static void setitem(PyObject *d,const char *name,long val) {
+  PyObject *v = PyInt_FromLong(val);
+  PyDict_SetItemString(d,name,v);
+  Py_DECREF(v);
+}
+
 void
 initmilter(void) {
    PyObject *m, *d;
@@ -1125,24 +1206,24 @@ initmilter(void) {
    d = PyModule_GetDict(m);
    MilterError = PyErr_NewException("milter.error", NULL, NULL);
    PyDict_SetItemString(d,"error", MilterError);
-   PyDict_SetItemString(d,"SUCCESS", PyInt_FromLong((long) MI_SUCCESS));
-   PyDict_SetItemString(d,"FAILURE", PyInt_FromLong((long) MI_FAILURE));
-   PyDict_SetItemString(d,"VERSION", PyInt_FromLong((long) SMFI_VERSION));
-   PyDict_SetItemString(d,"ADDHDRS", PyInt_FromLong((long) SMFIF_ADDHDRS));
-   PyDict_SetItemString(d,"CHGBODY", PyInt_FromLong((long) SMFIF_CHGBODY));
-   PyDict_SetItemString(d,"MODBODY", PyInt_FromLong((long) SMFIF_MODBODY));
-   PyDict_SetItemString(d,"ADDRCPT", PyInt_FromLong((long) SMFIF_ADDRCPT));
-   PyDict_SetItemString(d,"DELRCPT", PyInt_FromLong((long) SMFIF_DELRCPT));
-   PyDict_SetItemString(d,"CHGHDRS", PyInt_FromLong((long) SMFIF_CHGHDRS));
-   PyDict_SetItemString(d,"V1_ACTS", PyInt_FromLong((long) SMFI_V1_ACTS));
-   PyDict_SetItemString(d,"V2_ACTS", PyInt_FromLong((long) SMFI_V2_ACTS));
-   PyDict_SetItemString(d,"CURR_ACTS", PyInt_FromLong((long) SMFI_CURR_ACTS));
+   setitem(d,"SUCCESS",  MI_SUCCESS);
+   setitem(d,"FAILURE",  MI_FAILURE);
+   setitem(d,"VERSION",  SMFI_VERSION);
+   setitem(d,"ADDHDRS",  SMFIF_ADDHDRS);
+   setitem(d,"CHGBODY",  SMFIF_CHGBODY);
+   setitem(d,"MODBODY",  SMFIF_MODBODY);
+   setitem(d,"ADDRCPT",  SMFIF_ADDRCPT);
+   setitem(d,"DELRCPT",  SMFIF_DELRCPT);
+   setitem(d,"CHGHDRS",  SMFIF_CHGHDRS);
+   setitem(d,"V1_ACTS",  SMFI_V1_ACTS);
+   setitem(d,"V2_ACTS",  SMFI_V2_ACTS);
+   setitem(d,"CURR_ACTS",  SMFI_CURR_ACTS);
 #ifdef SMFIF_QUARANTINE
-   PyDict_SetItemString(d,"QUARANTINE",PyInt_FromLong((long)SMFIF_QUARANTINE));
+   setitem(d,"QUARANTINE",SMFIF_QUARANTINE);
 #endif
-   PyDict_SetItemString(d,"CONTINUE", PyInt_FromLong((long) SMFIS_CONTINUE));
-   PyDict_SetItemString(d,"REJECT", PyInt_FromLong((long) SMFIS_REJECT));
-   PyDict_SetItemString(d,"DISCARD", PyInt_FromLong((long) SMFIS_DISCARD));
-   PyDict_SetItemString(d,"ACCEPT", PyInt_FromLong((long) SMFIS_ACCEPT));
-   PyDict_SetItemString(d,"TEMPFAIL", PyInt_FromLong((long) SMFIS_TEMPFAIL));
+   setitem(d,"CONTINUE",  SMFIS_CONTINUE);
+   setitem(d,"REJECT",  SMFIS_REJECT);
+   setitem(d,"DISCARD",  SMFIS_DISCARD);
+   setitem(d,"ACCEPT",  SMFIS_ACCEPT);
+   setitem(d,"TEMPFAIL",  SMFIS_TEMPFAIL);
 }
diff --git a/mime.py b/mime.py
index 295375288e1d96f51d37adf75e137db499fb94fe..8c1a14613526282055965ef49a8e4a59a858816f 100644
--- a/mime.py
+++ b/mime.py
@@ -1,4 +1,7 @@
 # $Log$
+# Revision 1.54  2004/08/18 01:59:46  stuart
+# Handle mislabeled multipart messages
+#
 # Revision 1.53  2004/04/24 22:53:20  stuart
 # Rename some local variables to avoid shadowing builtins
 #
@@ -58,6 +61,18 @@ except: from email.Parser import nlcre as NLCRE
 
 from email import Errors
 
+class MimeGenerator(Generator):
+    def _dispatch(self, msg):
+        # Get the Content-Type: for the message, then try to dispatch to
+        # self._handle_<maintype>_<subtype>().  If there's no handler for the
+        # full MIME type, then dispatch to self._handle_<maintype>().  If
+        # that's missing too, then dispatch to self._writeBody().
+        main = msg.get_content_maintype()
+	if msg.is_multipart() and main.lower() != 'multipart':
+	  self._handle_multipart(msg)
+	else:
+	  Generator._dispatch(self,msg)
+
 class MimeParser(Parser):
 
     # This is a copy of _parsebody from email.Parser, with a fix
@@ -349,9 +364,15 @@ class MimeMessage(Message):
 
   def dump(self,file,unixfrom=False):
     "Write this message (and all subparts) to a file"
-    g = Generator(file)
+    g = MimeGenerator(file)
     g.flatten(self,unixfrom=unixfrom)
 
+  def as_string(self, unixfrom=False):
+      "Return the entire formatted message as a string."
+      fp = StringIO.StringIO()
+      self.dump(fp,unixfrom=unixfrom)
+      return fp.getvalue()
+
   def getencoding(self):
     return self.get('content-transfer-encoding',None)
 
@@ -577,7 +598,6 @@ class HTMLScriptFilter(SGMLFilter):
   def handle_comment(self,comment):
     if not self.ignoring: SGMLFilter.handle_comment(self,comment)
 
-
 def check_html(msg,savname=None):
   "Remove scripts from HTML attachments."
   msgtype = msg.get_content_type().lower()
diff --git a/setup.py b/setup.py
index 48c6e3cb2c47e5a4335f2cb44afe63eb47ffa2b0..42a2b61712d541ee012dbf476f8cb63632f70ecf 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.0",
+setup(name = "milter", version = "0.7.1",
 	description="Python interface to sendmail milter API",
 	long_description="""\
 This is a python extension module to enable python scripts to
@@ -28,7 +28,10 @@ querying SPF records.
 	url="http://www.bmsi.com/python/milter.html",
 	py_modules=["Milter","mime","spf"],
 	ext_modules=[
-	  Extension("milter", ["miltermodule.c"],libraries=libs),
+	  Extension("milter", ["miltermodule.c"],
+	    libraries=libs,
+	    define_macros = [ ('MAX_ML_REPLY',32) ]
+	  ),
 	],
 	keywords = ['sendmail','milter'],
 	classifiers = [
diff --git a/spf.py b/spf.py
index 1472bfc7533671312c8a6c0b57fbc734655164c0..51450fad38438d241f9e3ebfc7750777baaa96cf 100755
--- a/spf.py
+++ b/spf.py
@@ -45,6 +45,9 @@ For news, bugfixes, etc. visit the home page for this implementation at
 # Terrence is not responding to email.
 #
 # $Log$
+# Revision 1.14  2004/08/23 02:28:24  stuart
+# Remove Perl usage message.
+#
 # Revision 1.13  2004/07/23 19:23:12  stuart
 # Always fail to match on ip6, until we support it properly.
 #
@@ -115,15 +118,6 @@ import xml.sax
 # (c) 2004 Python version by Stuart Gathman
 #
 # Date: 2004-02-25
-# Version: 1.0
-#
-# Usage:
-#  ./cid2spf.pl "<ep xmlns='http://ms.net/1'>...</ep>"
-#
-# Note that the 'include' directives will also have to be checked and
-# "translated". Future versions of this script might be able to get a
-# domain name as an argument and "crawl" the DNS for the necessary
-# information.
 #
 # A complete reverse translation (SPF -> CID) might be impossible, since
 # there are no way to handle: