From 59bf86e7479b26e9dce38212cdf7fbff75853f66 Mon Sep 17 00:00:00 2001
From: Stuart Gathman <stuart@gathman.org>
Date: Wed, 22 Nov 2006 18:32:37 +0000
Subject: [PATCH] Release 0.8.7

---
 ChangeLog      |  214 +++++++
 NEWS           |    3 +
 cid2spf.py     |  153 -----
 milter.spec    |   34 +-
 quarantine.txt |   13 +
 setup.py       |    6 +-
 softfail.txt   |    2 +-
 spf.py         | 1514 ------------------------------------------------
 spfquery.py    |   99 ----
 9 files changed, 252 insertions(+), 1786 deletions(-)
 create mode 100644 ChangeLog
 delete mode 100644 cid2spf.py
 delete mode 100755 spf.py
 delete mode 100755 spfquery.py

diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..8e7bf3b
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,214 @@
+# Revision 1.69  2006/11/04 22:09:39  customdesigned
+# Another lame DSN heuristic.  Block PTR cache poisoning attack.
+#
+# Revision 1.68  2006/10/04 03:46:01  customdesigned
+# Fix defaults.
+#
+# Revision 1.67  2006/10/01 01:44:06  customdesigned
+# case_sensitive_localpart option, more delayed bounce heuristics,
+# optional smart_alias section.
+#
+# Revision 1.66  2006/07/26 16:42:26  customdesigned
+# Support CBV timeout
+#
+# Revision 1.65  2006/06/21 22:22:00  customdesigned
+# Handle multi-line headers in delayed dsns.
+#
+# Revision 1.64  2006/06/21 21:12:04  customdesigned
+# More delayed reject token headers.
+# Don't require HELO pass for CBV.
+#
+# Revision 1.63  2006/05/21 03:41:44  customdesigned
+# Fail dsn
+#
+# Revision 1.61  2006/05/17 21:28:07  customdesigned
+# Create GOSSiP record only when connection will procede to DATA.
+#
+# Revision 1.60  2006/05/12 16:14:48  customdesigned
+# Don't require SPF pass for white/black listing mail from trusted relay.
+# Support localpart wildcard for white and black lists.
+#
+# Revision 1.59  2006/04/06 18:14:17  customdesigned
+# Check whitelist/blacklist even when not checking SPF (e.g. trusted relay).
+#
+# Revision 1.58  2006/03/10 20:52:49  customdesigned
+# Use re to recognize failure DSNs.
+#
+# Revision 1.57  2006/03/07 20:50:54  customdesigned
+# Use signed Message-ID in delayed reject to blacklist senders
+#
+# Revision 1.56  2006/02/24 02:12:54  customdesigned
+# Properly report hard PermError (lax mode fails also) by always setting
+# perm_error attribute with PermError exception.  Improve reporting of
+# invalid domain PermError.
+#
+# Revision 1.55  2006/02/17 05:04:29  customdesigned
+# Use SRS sign domain list.
+# Accept but do not use for training whitelisted senders without SPF pass.
+# Immediate rejection of unsigned bounces.
+#
+# Revision 1.54  2006/02/16 02:16:36  customdesigned
+# User specific SPF receiver policy.
+#
+# Revision 1.53  2006/02/12 04:15:01  customdesigned
+# Remove spf dependency for iniplist
+#
+# Revision 1.52  2006/02/12 02:12:08  customdesigned
+# Use CIDR notation for internal connect list.
+#
+# Revision 1.51  2006/02/12 01:13:58  customdesigned
+# Don't check rcpt user list when signed MFROM.
+#
+# Revision 1.50  2006/02/09 20:39:43  customdesigned
+# Use CIDR notation for trusted_relay iplist
+#
+# Revision 1.49  2006/01/30 23:14:48  customdesigned
+# put back eom condition
+#
+# Revision 1.48  2006/01/12 20:31:24  customdesigned
+# Accelerate training via whitelist and blacklist.
+#
+# Revision 1.47  2005/12/29 04:49:10  customdesigned
+# Do not auto-whitelist autoreplys
+#
+# Revision 1.46  2005/12/28 20:17:29  customdesigned
+# Expire and renew AddrCache entries
+#
+# Revision 1.45  2005/12/23 22:34:46  customdesigned
+# Put guessed result in separate header.
+#
+# Revision 1.44  2005/12/23 21:47:07  customdesigned
+# Move Received-SPF header to top.
+#
+# Revision 1.43  2005/12/09 16:54:01  customdesigned
+# Select neutral DSN template for best_guess
+#
+# Revision 1.42  2005/12/01 22:42:32  customdesigned
+# improve gossip support.
+# Initialize srs_domain from srs.srs config property.  Should probably
+# always block unsigned DSN when signing all.
+#
+# Revision 1.41  2005/12/01 18:59:25  customdesigned
+# Fix neutral policy.  pobox.com -> openspf.org
+#
+# Revision 1.40  2005/11/07 21:22:35  customdesigned
+# GOSSiP support, local database only.
+#
+# Revision 1.39  2005/10/31 00:04:58  customdesigned
+# Simple implementation of trusted_forwarder list.  Inefficient for
+# more than 1 or 2 entries.
+#
+# Revision 1.38  2005/10/28 19:36:54  customdesigned
+# Don't check internal_domains for trusted_relay.
+#
+# Revision 1.37  2005/10/28 09:30:49  customdesigned
+# Do not send quarantine DSN when sender is DSN.
+#
+# Revision 1.36  2005/10/23 16:01:29  customdesigned
+# Consider MAIL FROM a match for supply_sender when a subdomain of From or Sender
+#
+# Revision 1.35  2005/10/20 18:47:27  customdesigned
+# Configure auto_whitelist senders.
+#
+# Revision 1.34  2005/10/19 21:07:49  customdesigned
+# access.db stores keys in lower case
+#
+# Revision 1.33  2005/10/19 19:37:50  customdesigned
+# Train screener on whitelisted messages.
+#
+# Revision 1.32  2005/10/14 16:17:31  customdesigned
+# Auto whitelist refinements.
+#
+# Revision 1.31  2005/10/14 01:14:08  customdesigned
+# Auto whitelist feature.
+#
+# Revision 1.30  2005/10/12 16:36:30  customdesigned
+# Release 0.8.3
+#
+# Revision 1.29  2005/10/11 22:50:07  customdesigned
+# Always check HELO except for SPF pass, temperror.
+#
+# Revision 1.28  2005/10/10 23:50:20  customdesigned
+# Use logging module to make logging threadsafe (avoid splitting log lines)
+#
+# Revision 1.27  2005/10/10 20:15:33  customdesigned
+# Configure SPF policy via sendmail access file.
+#
+# Revision 1.26  2005/10/07 03:23:40  customdesigned
+# Banned users option.  Experimental feature to supply Sender when
+# missing and MFROM domain doesn't match From.  Log cipher bits for
+# SMTP AUTH.  Sketch access file feature.
+#
+# Revision 1.25  2005/09/08 03:55:08  customdesigned
+# Handle perverse MFROM quoting.
+#
+# Revision 1.24  2005/08/18 03:36:54  customdesigned
+# Don't innoculate with SCREENED mail.
+#
+# Revision 1.23  2005/08/17 19:35:27  customdesigned
+# Send DSN before adding message to quarantine.
+#
+# Revision 1.22  2005/08/11 22:17:58  customdesigned
+# Consider SMTP AUTH connections internal.
+#
+# Revision 1.21  2005/08/04 21:21:31  customdesigned
+# Treat fail like softfail for selected (braindead) domains.
+# Treat mail according to extended processing results, but
+# report any PermError that would officially result via DSN.
+#
+# Revision 1.20  2005/08/02 18:04:35  customdesigned
+# Keep screened honeypot mail, but optionally discard honeypot only mail.
+#
+# Revision 1.19  2005/07/20 03:30:04  customdesigned
+# Check pydspam version for honeypot, include latest pyspf changes.
+#
+# Revision 1.18  2005/07/17 01:25:44  customdesigned
+# Log as well as use extended result for best guess.
+#
+# Revision 1.17  2005/07/15 20:25:36  customdesigned
+# Use extended results processing for best_guess.
+#
+# Revision 1.16  2005/07/14 03:23:33  customdesigned
+# Make SES package optional.  Initial honeypot support.
+#
+# Revision 1.15  2005/07/06 04:05:40  customdesigned
+# Initial SES integration.
+#
+# Revision 1.14  2005/07/02 23:27:31  customdesigned
+# Don't match hostnames for internal connects.
+#
+# Revision 1.13  2005/07/01 16:30:24  customdesigned
+# Always log trusted Received and Received-SPF headers.
+#
+# Revision 1.12  2005/06/20 22:35:35  customdesigned
+# Setreply for rejectvirus.
+#
+# Revision 1.11  2005/06/17 02:07:20  customdesigned
+# Release 0.8.1
+#
+# Revision 1.10  2005/06/16 18:35:51  customdesigned
+# Ignore HeaderParseError decoding header
+#
+# Revision 1.9  2005/06/14 21:55:29  customdesigned
+# Check internal_domains for outgoing mail.
+#
+# Revision 1.8  2005/06/06 18:24:59  customdesigned
+# Properly log exceptions from pydspam
+#
+# Revision 1.7  2005/06/04 19:41:16  customdesigned
+# Fix bugs from testing RPM
+#
+# Revision 1.6  2005/06/03 04:57:05  customdesigned
+# Organize config reader by section.  Create defang section.
+#
+# Revision 1.5  2005/06/02 15:00:17  customdesigned
+# Configure banned extensions.  Scan zipfile option with test case.
+#
+# Revision 1.4  2005/06/02 04:18:55  customdesigned
+# Update copyright notices after reading article on /.
+#
+# Revision 1.3  2005/06/02 02:09:00  customdesigned
+# Record timestamp in send_dsn.log
+#
+# Revision 1.2  2005/06/02 01:00:36  customdesigned
+# Support configurable templates for DSNs.
diff --git a/NEWS b/NEWS
index f55d63d..5069886 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,7 @@
 Here is a history of user visible changes to Python milter.
+0.8.7	Move spf module to pyspf
+	Prevent PTR cache poisoning
+	More lame bounce heuristics
 0.8.6	Support CBV timeout
 	Support fail template, headers in templates
 	Create GOSSiP record only when connection will procede to DATA.
diff --git a/cid2spf.py b/cid2spf.py
deleted file mode 100644
index 2140aa5..0000000
--- a/cid2spf.py
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/python2.3
-
-# Convert a MS Caller-ID entry (XML) to a SPF entry
-#
-# (c) 2004 by Ernesto Baschny
-# (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:
-# - PTR and EXISTS mechanism 
-# - MX mechanism with an different domain as argument
-# - macros
-# 
-# References:
-# http://www.microsoft.com/mscorp/twc/privacy/spam_callerid.mspx
-# http://spf.pobox.com/
-#
-# Known bugs:
-# - Currently it won't handle the exclusions provided in the A and R
-#   tags (prefix '!'). They will show up "as-is" in the SPF record
-# - I really haven't read the MS-CID specs in-depth, so there are probably
-#   other bugs too :)
-#
-# Ernesto Baschny <ernst@baschny.de>
-#
-
-import xml.sax
-import spf
-
-# -------------------------------------------------------------------------
-class CIDParser(xml.sax.ContentHandler):
-  "Convert a MS Caller-ID entry (XML) to a SPF entry"
-
-  def __init__(self,q=None):
-    self.spf = []
-    self.action = '-all'
-    self.has_servers = None
-    self.spf_entry = None
-    if q:
-      self.spf_query = q
-    else:
-      self.spf_query = spf.query(i='127.0.0.1', s='localhost', h='unknown')
-
-  def startElement(self,tag,attr):
-      if tag == 'm':
-	if self.has_servers != None and not self.has_servers:
-	  raise ValueError(
-    "Declared <noMailServers\> and later <m>, this CID entry is not valid."
-	  )
-	self.has_servers = True
-      elif tag == 'noMailServers':
-	if self.has_servers:
-	  raise ValueError(
-    "Declared <m> and later <noMailServers\>, this CID entry is not valid."
-	  )
-	self.has_servers = False
-      elif tag == 'ep':
-	if attr.has_key('testing') and attr.getValue('testing') == 'true':
-	  # A CID with 'testing' found:
-	  # From the MS-specs:
-	  #  "Documents in which such attribute is present with a true
-	  #  value SHOULD be entirely ignored (one should act as if the
-	  #  document were absent)"
-	  # From the SPF-specs:
-	  #  "Neutral (?): The SPF client MUST proceed as if a domain did
-	  #  not publish SPF data."
-	  # So we set SPF action to "neutral":
-	  self.action = '?all'
-      elif tag == 'mx':
-	  # The empty MX-tag, same as SPF's MX-mechanism
-	  self.spf.append('mx')
-      self.tag = tag
-
-  def characters(self,text):
-	tag = self.tag
-	# Remove starting and trailing spaces from text:
-	text = text.strip()
-
-	if tag == 'a' or tag == 'r':
-	    # The A and R tags from MS-CID are both handled by the 
-	    # ipv4/6-mechanisms from SPF:
-	    if text.find(':') < 0:
-	      mechanism = 'ip4'
-	    else:
-	      mechanism = 'ip6'
-	    self.spf.append(mechanism + ':' + text)
-	elif tag == 'indirect':
-	    # MS-CID's indirect is "sort of" the include from SPF:
-	    # Not really true, because the <indirect> tag from MS-CID also 
-	    # provides a fallback in case the included domain doesn't provide
-	    # _ep-records: The inbound MX-servers of the included domains
-	    # are added to the list of allowed outgoing mailservers for the
-	    # domain that declared the _ep-record with the <indirect> tag.
-	    # In SPF you would use the 'mx:domain' to handle this, but this
-	    # wouldn't depend on referred domain having or not SPF-records.
-	    cid_xml = self.cid_txt(text)
-	    if cid_xml:
-	      p = CIDParser()
-	      xml.sax.parseString(cid_xml,p)
-	      if p.has_servers != False:
-		self.spf += p.spf
-	    else:
-	      self.spf.append('mx:' + text)
-
-  def cid_txt(self,domain):
-    q = self.spf_query
-    domain='_ep.' + domain
-    a = q.dns_txt(domain)
-    if not a: return None
-    if a[0].lower().startswith('<ep ') and a[-1].lower().endswith('</ep>'):
-      return ''.join(a)
-    return None
-
-  def endElement(self,tag):
-      if tag == 'ep':
-	# This is the end... assemble what we've got
-	spf_entry = ['v=spf1']
-	if self.has_servers != False:
-	  spf_entry += self.spf
-	spf_entry.append(self.action)
-	self.spf_entry = ' '.join(spf_entry)
-
-  def spf_txt(self,cid_xml):
-    if not cid_xml.startswith('<'):
-      cid_xml = self.cid_txt(cid_xml)
-      if not cid_xml: return None
-    # Parse the beast. Any XML-problem will be reported by xlm.sax
-    self.spf_entry = None
-    xml.sax.parseString(cid_xml,self)
-    return self.spf_entry
-
-if __name__ == '__main__':
-  import sys
-  if len(sys.argv) < 2:
-    print >>sys.stderr, \
-      """Usage: %s "<ep xmlns='http://ms.net/1'>...</ep>" """ % sys.argv[0]
-    sys.exit(1)
-
-  cid_xml = sys.argv[1]
-
-  p = CIDParser()
-  print p.spf_txt(cid_xml)
diff --git a/milter.spec b/milter.spec
index c2b9217..a41f9b7 100644
--- a/milter.spec
+++ b/milter.spec
@@ -1,23 +1,20 @@
 %define name milter
-%define version 0.8.6
-%define release 2.RH7
+%define version 0.8.7
+%define release 1
 # what version of RH are we building for?
-%define redhat9 0
-%define redhat7 1
-%define redhat6 0
+%define redhat7 0
 
 # Options for Redhat version 6.x:
-# rpm -ba|--rebuild --define "rh6 1"
-%{?rh6:%define redhat7 0}
-%{?rh6:%define redhat6 1}
+# rpm -ba|--rebuild --define "rh7 1"
+%{?rh7:%define redhat7 1}
 
 # some systems dont have initrddir defined
 %{?_initrddir:%define _initrddir /etc/rc.d/init.d}
 
-%if %{redhat9}
-%define sysvinit milter.rc
-%else	# 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
 %endif
 # RH9, other systems (single ps line per process)
 %ifos Linux
@@ -43,22 +40,23 @@ Requires: %{python} >= 2.4, sendmail >= 8.13
 %ifos Linux
 Requires: chkconfig
 %endif
-BuildRequires: %{python}-devel , sendmail-devel >= 8.13
+BuildRequires: %{python}-devel >= 2.4, sendmail-devel >= 8.13
 
 %description
 This is a python extension module to enable python scripts to
 attach to sendmail's libmilter functionality.  Additional python
-modules provide for navigating and modifying MIME parts.
+modules provide for navigating and modifying MIME parts, sending
+DSNs, and doing CBV.
 
 %prep
 %setup
 #patch -p0 -b .bms
 
 %build
-%if %{redhat9}
-  LDFLAGS="-g"
-%else
+%if %{redhat7}
   LDFLAGS="-s"
+%else # Redhat builds debug packages after 7.3
+  LDFLAGS="-g"
 %endif
 env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build
 
@@ -176,6 +174,10 @@ rm -rf $RPM_BUILD_ROOT
 /usr/share/sendmail-cf/hack/rhsbl.m4
 
 %changelog
+* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
+- Prevent PTR cache poisoning
+- More lame bounce heuristics
+- SPF moved to pyspf RPM
 * Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
 - Support CBV timeout
 - Support fail template, headers in templates
diff --git a/quarantine.txt b/quarantine.txt
index b060d75..cdf3f73 100644
--- a/quarantine.txt
+++ b/quarantine.txt
@@ -22,6 +22,19 @@ their quarantined mail and may notice your message.  If your message is
 important, please contact them via other means.  You may also try sending
 them a simple plain text message.
 
+If you never sent the above message, then your domain, %(sender_domain)s,
+was forged - i.e. used without your knowlege or authorization by
+someone attempting to steal your mail identity.  This is a very
+serious problem, and you need to provide authentication for your
+SMTP (email) servers to prevent criminals from forging your
+domain.  The simplest step is usually to publish an SPF record
+with your Sender Policy.  
+
+For more information, see: http://www.openspf.org
+
+Your mail admin needs to publish a strict SPF record so that I can reject
+those forgeries instead of bugging you with them.
+
 If you need further assistance, please do not hesitate to contact me.
 
 Kind regards,
diff --git a/setup.py b/setup.py
index 2c5716b..ffd1217 100644
--- a/setup.py
+++ b/setup.py
@@ -15,13 +15,13 @@ if sys.version < '2.2.3':
   DistributionMetadata.download_url = None
 
 # NOTE: importing Milter to obtain version fails when milter.so not built
-setup(name = "milter", version = '0.8.6',
+setup(name = "milter", version = '0.8.7',
 	description="Python interface to sendmail milter API",
 	long_description="""\
 This is a python extension module to enable python scripts to
 attach to sendmail's libmilter functionality.  Additional python
 modules provide for navigating and modifying MIME parts, and
-querying SPF records.
+sending DSNs or doing CBVs.
 """,
 	author="Jim Niemira",
 	author_email="urmane@urmane.org",
@@ -29,7 +29,7 @@ querying SPF records.
 	maintainer_email="stuart@bmsi.com",
 	license="GPL",
 	url="http://www.bmsi.com/python/milter.html",
-	py_modules=["mime","spf"],
+	py_modules=["mime"],
 	packages = ['Milter'],
 	ext_modules=[
 	  Extension("milter", ["miltermodule.c"],
diff --git a/softfail.txt b/softfail.txt
index 18b9643..62c9834 100644
--- a/softfail.txt
+++ b/softfail.txt
@@ -17,7 +17,7 @@ Subject: %(subject)s
 Received-SPF: %(spf_result)s
 
 Your sender policy indicated that the above email was likely forged and that
-feedback was desired.  If you are sending from a foreign ISP,
+feedback was desired for debugging.  If you are sending from a foreign ISP,
 then you may need to follow your home ISPs instructions for configuring
 your outgoing mail server.
 
diff --git a/spf.py b/spf.py
deleted file mode 100755
index 3fb15db..0000000
--- a/spf.py
+++ /dev/null
@@ -1,1514 +0,0 @@
-#!/usr/bin/env python
-"""SPF (Sender Policy Framework) implementation.
-
-Copyright (c) 2003, Terence Way
-Portions Copyright (c) 2004,2005,2006 Stuart Gathman <stuart@bmsi.com>
-Portions Copyright (c) 2005,2006 Scott Kitterman <scott@kitterman.com>
-This module is free software, and you may redistribute it and/or modify
-it under the same terms as Python itself, so long as this copyright message
-and disclaimer are retained in their original form.
-
-IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
-SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
-THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
-DAMAGE.
-
-THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
-PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
-AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
-SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-
-For more information about SPF, a tool against email forgery, see
-    http://www.openspf.org/
-
-For news, bugfixes, etc. visit the home page for this implementation at
-    http://www.wayforward.net/spf/
-    http://sourceforge.net/projects/pymilter/
-"""
-
-# Changes:
-#    9-dec-2003, v1.1, Meng Weng Wong added PTR code, THANK YOU
-#   11-dec-2003, v1.2, ttw added macro expansion, exp=, and redirect=
-#   13-dec-2003, v1.3, ttw added %{o} original domain macro,
-#                      print spf result on command line, support default=,
-#                      support localhost, follow DNS CNAMEs, cache DNS results
-#                      during query, support Python 2.2 for Mac OS X
-#   16-dec-2003, v1.4, ttw fixed include handling (include is a mechanism,
-#                      complete with status results, so -include: should work.
-#                      Expand macros AFTER looking for status characters ?-+
-#                      so altavista.com SPF records work.
-#   17-dec-2003, v1.5, ttw use socket.inet_aton() instead of DNS.addr2bin, so
-#                      n, n.n, and n.n.n forms for IPv4 addresses work, and to
-#                      ditch the annoying Python 2.4 FutureWarning
-#   18-dec-2003, v1.6, Failures on Intel hardware: endianness.  Use ! on
-#                      struct.pack(), struct.unpack().
-#
-# Development taken over by Stuart Gathman <stuart@bmsi.com>.
-#
-# $Log$
-# Revision 1.107  2006/11/04 21:58:12  customdesigned
-# Prevent cache poisoning by bogus additional RRs in PTR DNS response.
-#
-# See spf_changelog.txt for earlier changes.
-
-__author__ = "Terence Way"
-__email__ = "terry@wayforward.net"
-__version__ = "1.7: July 22, 2005"
-MODULE = 'spf'
-
-USAGE = """To check an incoming mail request:
-    % python spf.py {ip} {sender} {helo}
-    % python spf.py 69.55.226.139 tway@optsw.com mx1.wayforward.net
-
-To test an SPF record:
-    % python spf.py "v=spf1..." {ip} {sender} {helo}
-    % python spf.py "v=spf1 +mx +ip4:10.0.0.1 -all" 10.0.0.1 tway@foo.com a    
-
-To fetch an SPF record:
-    % python spf.py {domain}
-    % python spf.py wayforward.net
-
-To test this script (and to output this usage message):
-    % python spf.py
-"""
-
-import re
-import socket  # for inet_ntoa() and inet_aton()
-import struct  # for pack() and unpack()
-import time    # for time()
-import urllib  # for quote()
-
-import DNS    # http://pydns.sourceforge.net
-if not hasattr(DNS.Type, 'SPF'):
-    # patch in type99 support
-    DNS.Type.SPF = 99
-    DNS.Type.typemap[99] = 'SPF'
-    DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata
-
-def DNSLookup(name, qtype, strict=True):
-    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 TempError, 'DNS ' + str(x)
-    except DNS.DNSError, x:
-        raise TempError, 'DNS ' + str(x)
-
-RE_SPF = re.compile(r'^v=spf1$|^v=spf1 ',re.IGNORECASE)
-
-# Regular expression to look for modifiers
-RE_MODIFIER = re.compile(r'^([a-z][a-z0-9_\-\.]*)=', re.IGNORECASE)
-
-# Regular expression to find macro expansions
-PAT_CHAR = r'%(%|_|-|(\{[^\}]*\}))'
-RE_CHAR = re.compile(PAT_CHAR)
-
-# Regular expression to break up a macro expansion
-RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)')
-
-RE_DUAL_CIDR = re.compile(r'//(0|[1-9]\d*)$')
-RE_CIDR = re.compile(r'/(0|[1-9]\d*)$')
-
-PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
-RE_IP4 = re.compile(PAT_IP4+'$')
-
-RE_TOPLAB = re.compile(
-    r'\.(?:[0-9a-z]*[a-z][0-9a-z]*|[0-9a-z]+-[0-9a-z-]*[0-9a-z])\.?$|%s'
-    	% PAT_CHAR, re.IGNORECASE)
-
-RE_IP6 = re.compile(                 '(?:%(hex4)s:){6}%(ls32)s$'
-                   '|::(?:%(hex4)s:){5}%(ls32)s$'
-                  '|(?:%(hex4)s)?::(?:%(hex4)s:){4}%(ls32)s$'
-    '|(?:(?:%(hex4)s:){0,1}%(hex4)s)?::(?:%(hex4)s:){3}%(ls32)s$'
-    '|(?:(?:%(hex4)s:){0,2}%(hex4)s)?::(?:%(hex4)s:){2}%(ls32)s$'
-    '|(?:(?:%(hex4)s:){0,3}%(hex4)s)?::%(hex4)s:%(ls32)s$'
-    '|(?:(?:%(hex4)s:){0,4}%(hex4)s)?::%(ls32)s$'
-    '|(?:(?:%(hex4)s:){0,5}%(hex4)s)?::%(hex4)s$'
-    '|(?:(?:%(hex4)s:){0,6}%(hex4)s)?::$'
-  % {
-    'ls32': r'(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|%s)'%PAT_IP4,
-    'hex4': r'[0-9a-f]{1,4}'
-    }, re.IGNORECASE)
-
-# Local parts and senders have their delimiters replaced with '.' during
-# macro expansion
-#
-JOINERS = {'l': '.', 's': '.'}
-
-RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail',
-           'pass': 'pass', 'fail': 'fail', 'permerror': 'permerror',
-       'error': 'error', 'neutral': 'neutral', 'softfail': 'softfail',
-       'none': 'none', 'local': 'local', 'trusted': 'trusted',
-           'ambiguous': 'ambiguous'}
-
-EXPLANATIONS = {'pass': 'sender SPF authorized',
-                'fail': 'SPF fail - not authorized',
-                'permerror': 'permanent error in processing',
-                'temperror': 'temporary DNS error in processing',
-        'softfail': 'domain owner discourages use of this host',
-        'neutral': 'access neither permitted nor denied',
-        'none': '',
-                #Note: The following are not formally SPF results
-                'local': 'No SPF result due to local policy',
-                'trusted': 'No SPF check - trusted-forwarder.org',
-                #Ambiguous only used in harsh mode for SPF validation
-                'ambiguous': 'No error, but results may vary'
-        }
-
-# support pre 2.2.1....
-try:
-    bool, True, False = bool, True, False
-except NameError:
-    False, True = 0, 1
-    def bool(x): return not not x
-# ...pre 2.2.1
-
-DELEGATE = None
-
-# standard default SPF record for best_guess
-DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
-
-#Whitelisted forwarders here.  Additional locally trusted forwarders can be
-#added to this record.
-TRUSTED_FORWARDERS = 'v=spf1 ?include:spf.trusted-forwarder.org -all'
-
-# maximum DNS lookups allowed
-MAX_LOOKUP = 10 #RFC 4408 Para 10.1
-MAX_MX = 10 #RFC 4408 Para 10.1
-MAX_PTR = 10 #RFC 4408 Para 10.1
-MAX_CNAME = 10 # analogous interpretation to MAX_PTR
-MAX_RECURSION = 20
-
-ALL_MECHANISMS = ('a', 'mx', 'ptr', 'exists', 'include', 'ip4', 'ip6', 'all')
-COMMON_MISTAKES = {
-  'prt': 'ptr', 'ip': 'ip4', 'ipv4': 'ip4', 'ipv6': 'ip6', 'all.': 'all'
-}
-
-#If harsh processing, for the validator, is invoked, warn if results
-#likely deviate from the publishers intention.
-class AmbiguityWarning(Exception):
-    "SPF Warning - ambiguous results"
-    def __init__(self, msg, mech=None, ext=None):
-        Exception.__init__(self, msg, mech)
-        self.msg = msg
-        self.mech = mech
-        self.ext = ext
-    def __str__(self):
-        if self.mech:
-            return '%s: %s' %(self.msg, self.mech)
-        return self.msg
-
-class TempError(Exception):
-    "Temporary SPF error"
-    def __init__(self, msg, mech=None, ext=None):
-        Exception.__init__(self, msg, mech)
-        self.msg = msg
-        self.mech = mech
-        self.ext = ext
-    def __str__(self):
-        if self.mech:
-            return '%s: %s '%(self.msg, self.mech)
-        return self.msg
-
-class PermError(Exception):
-    "Permanent SPF error"
-    def __init__(self, msg, mech=None, ext=None):
-        Exception.__init__(self, msg, mech)
-        self.msg = msg
-        self.mech = mech
-        self.ext = ext
-    def __str__(self):
-        if self.mech:
-            return '%s: %s'%(self.msg, self.mech)
-        return self.msg
-
-def check2(i, s, h, local=None, receiver=None):
-    """Test an incoming MAIL FROM:<s>, from a client with ip address i.
-    h is the HELO/EHLO domain name.  This is the RFC4408 compliant pySPF2.0
-    interface.  The interface returns an SPF result and explanation only.
-    SMTP response codes are not returned since RFC 4408 does not specify
-    receiver policy.  Applications updated for RFC 4408 should use this
-    interface.
-
-    Returns (result, explanation) where result in
-    ['pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', 'neutral' ].
-
-    Example:
-    #>>> check2(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')
-
-    """
-    res,_,exp = query(i=i, s=s, h=h, local=local, receiver=receiver).check()
-    return res,exp
-
-def check(i, s, h, local=None, receiver=None):
-    """Test an incoming MAIL FROM:<s>, from a client with ip address i.
-    h is the HELO/EHLO domain name.  This is the pre-RFC SPF Classic interface.
-    Applications written for pySPF 1.6/1.7 can use this interface to allow
-    pySPF2 to be a drop in replacement for older versions.  With the exception
-    of result codes, performance in RFC 4408 compliant.
-
-    Returns (result, code, explanation) where result in
-    ['pass', 'unknown', 'fail', 'error', 'softfail', 'none', 'neutral' ].
-
-    Example:
-    #>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')
-
-    """
-    res,code,exp = query(i=i, s=s, h=h, local=local, receiver=receiver).check()
-    if res == 'permerror':
-        res = 'unknown'
-    elif res == 'tempfail':
-        res =='error'
-    return res, code, exp
-
-class query(object):
-    """A query object keeps the relevant information about a single SPF
-    query:
-
-    i: ip address of SMTP client in dotted notation
-    s: sender declared in MAIL FROM:<>
-    l: local part of sender s
-    d: current domain, initially domain part of sender s
-    h: EHLO/HELO domain
-    v: 'in-addr' for IPv4 clients and 'ip6' for IPv6 clients
-    t: current timestamp
-    p: SMTP client domain name
-    o: domain part of sender s
-    r: receiver
-    c: pretty ip address (different from i for IPv6)
-
-    This is also, by design, the same variables used in SPF macro
-    expansion.
-
-    Also keeps cache: DNS cache.  
-    """
-    def __init__(self, i, s, h, local=None, receiver=None, strict=True):
-        self.s, self.h = s, h
-        if not s and h:
-            self.s = 'postmaster@' + h
-        self.l, self.o = split_email(s, h)
-        self.t = str(int(time.time()))
-        self.d = self.o
-        self.p = None	# lazy evaluation
-        if receiver:
-            self.r = receiver
-        else:
-            self.r = 'unknown'
-        # Since the cache does not track Time To Live, it is created
-        # fresh for each query.  It is important for efficiently using
-        # multiple results provided in DNS answers.
-        self.cache = {}
-        self.defexps = dict(EXPLANATIONS)
-        self.exps = dict(EXPLANATIONS)
-        self.libspf_local = local    # local policy
-        self.lookups = 0
-        # strict can be False, True, or 2 (numeric) for harsh
-        self.strict = strict
-	if i:
-	    self.set_ip(i)
-
-    def set_ip(self, i):
-        "Set connect ip, and ip6 or ip4 mode."
-	if RE_IP4.match(i):
-	    self.ip = addr2bin(i)
-	    ip6 = False
-	else:
-	    self.ip = bin2long6(inet_pton(i))
-	    if (self.ip >> 32) == 0xFFFF:	# IP4 mapped address
-		self.ip = self.ip & 0xFFFFFFFFL
-		ip6 = False
-	    else:
-		ip6 = True
-	# NOTE: self.A is not lowercase, so isn't a macro.  See query.expand()
-	if ip6:
-	    self.c = inet_ntop(
-	    	struct.pack("!QQ", self.ip>>64, self.ip&0xFFFFFFFFFFFFFFFFL))
-	    self.i = '.'.join(list('%032X'%self.ip))
-	    self.A = 'AAAA'
-	    self.v = 'ip6'
-	    self.cidrmax = 128
-	else:
-	    self.c = socket.inet_ntoa(struct.pack("!L", self.ip))
-	    self.i = self.c
-	    self.A = 'A'
-	    self.v = 'in-addr'
-	    self.cidrmax = 32
-
-    def set_default_explanation(self, exp):
-        exps = self.exps
-        defexps = self.defexps
-        for i in 'softfail', 'fail', 'permerror':
-            exps[i] = exp
-            defexps[i] = exp
-
-    def set_explanation(self, exp):
-        exps = self.exps
-        for i in 'softfail', 'fail', 'permerror':
-            exps[i] = exp
-
-    # Compute p macro only if needed
-    def getp(self):
-        if not self.p:
-            p = self.validated_ptrs()
-            if not p:
-                self.p = "unknown"
-	    elif self.d in p:
-	        self.p = self.d
-	    else:
-	        sfx = '.' + self.d
-	        for d in p:
-		    if d.endswith(sfx):
-		        self.p = d
-			break
-		else:
-		    self.p = p[0]
-        return self.p
-
-    def best_guess(self, spf=DEFAULT_SPF):
-        """Return a best guess based on a default SPF record"""
-        return self.check(spf)
-
-
-    def check(self, spf=None):
-        """
-    Returns (result, mta-status-code, explanation) where result
-    in ['fail', 'softfail', 'neutral' 'permerror', 'pass', 'temperror', 'none']
-
-    Examples:
-    >>> q = query(s='strong-bad@email.example.com',
-    ...           h='mx.example.org', i='192.0.2.3')
-    >>> q.check(spf='v=spf1 ?all')
-    ('neutral', 250, 'access neither permitted nor denied')
-
-    >>> q.check(spf='v=spf1 redirect=controlledmail.com exp=_exp.controlledmail.com')
-    ('fail', 550, 'SPF fail - not authorized')
-    
-    >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ?all moo')
-    ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
-
-    >>> q.check(spf='v=spf1 =a ?all moo')
-    ('permerror', 550, 'SPF Permanent Error: Unknown qualifier, RFC 4408 para 4.6.1, found in: =a')
-
-    >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ~all')
-    ('pass', 250, 'sender SPF authorized')
-
-    >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo=')
-    ('pass', 250, 'sender SPF authorized')
-
-    >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all match.sub-domains_9=yes')
-    ('pass', 250, 'sender SPF authorized')
-
-    >>> q.strict = False
-    >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo')
-    ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
-    >>> q.perm_error.ext
-    ('pass', 250, 'sender SPF authorized')
-
-    >>> q.strict = True
-    >>> q.check(spf='v=spf1 ip4:192.1.0.0/16 moo -all')
-    ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
-
-    >>> q.check(spf='v=spf1 ip4:192.1.0.0/16 ~all')
-    ('softfail', 250, 'domain owner discourages use of this host')
-
-    >>> q.check(spf='v=spf1 -ip4:192.1.0.0/6 ~all')
-    ('fail', 550, 'SPF fail - not authorized')
-
-    # Assumes DNS available
-    >>> q.check()
-    ('none', 250, '')
-
-    >>> q.check(spf='v=spf1 ip4:1.2.3.4 -a:example.net -all')
-    ('fail', 550, 'SPF fail - not authorized')
-    >>> q.libspf_local='ip4:192.0.2.3 a:example.org'
-    >>> q.check(spf='v=spf1 ip4:1.2.3.4 -a:example.net -all')
-    ('pass', 250, 'sender SPF authorized')
-
-    >>> q.check(spf='v=spf1 ip4:1.2.3.4 -all exp=_exp.controlledmail.com')
-    ('fail', 550, 'Controlledmail.com does not send mail from itself.')
-    
-    >>> q.check(spf='v=spf1 ip4:1.2.3.4 ?all exp=_exp.controlledmail.com')
-    ('neutral', 250, 'access neither permitted nor denied')
-        """
-        self.mech = []        # unknown mechanisms
-        # If not strict, certain PermErrors (mispelled
-        # mechanisms, strict processing limits exceeded)
-        # will continue processing.  However, the exception
-        # that strict processing would raise is saved here
-        self.perm_error = None
-
-        try:
-            self.lookups = 0
-            if not spf:
-                spf = self.dns_spf(self.d)
-            if self.libspf_local and spf: 
-                spf = insert_libspf_local_policy(
-                    spf, self.libspf_local)
-            rc = self.check1(spf, self.d, 0)
-	    if self.perm_error:
-		# lax processing encountered a permerror, but continued
-		self.perm_error.ext = rc
-		raise self.perm_error
-	    return rc
-	        
-        except TempError, x:
-            self.prob = x.msg
-            if x.mech:
-                self.mech.append(x.mech)
-            return ('temperror', 451, 'SPF Temporary Error: ' + str(x))
-        except PermError, x:
-            if not self.perm_error:
-                self.perm_error = x
-            self.prob = x.msg
-            if x.mech:
-                self.mech.append(x.mech)
-            # Pre-Lentczner draft treats this as an unknown result
-            # and equivalent to no SPF record.
-            return ('permerror', 550, 'SPF Permanent Error: ' + str(x))
-
-    def check1(self, spf, domain, recursion):
-        # spf rfc: 3.7 Processing Limits
-        #
-        if recursion > MAX_RECURSION:
-            # This should never happen in strict mode
-            # because of the other limits we check,
-            # so if it does, there is something wrong with
-            # our code.  It is not a PermError because there is not
-            # necessarily anything wrong with the SPF record.
-            if self.strict:
-                raise AssertionError('Too many levels of recursion')
-            # As an extended result, however, it should be
-            # a PermError.
-            raise PermError('Too many levels of recursion')
-        try:
-            try:
-                tmp, self.d = self.d, domain
-                return self.check0(spf, recursion)
-            finally:
-                self.d = tmp
-        except AmbiguityWarning,x:
-            self.prob = x.msg
-            if x.mech:
-                self.mech.append(x.mech)
-            return ('ambiguous', 000, 'SPF Ambiguity Warning: %s' % x)
-
-    def note_error(self, *msg):
-        if self.strict:
-            raise PermError(*msg)
-        # if lax mode, note error and continue
-        if not self.perm_error:
-            try:
-                raise PermError(*msg)
-            except PermError, x:
-                # FIXME: keep a list of errors for even friendlier diagnostics.
-                self.perm_error = x
-        return self.perm_error
-
-    def validate_mechanism(self, mech):
-        """Parse and validate a mechanism.
-    Returns mech,m,arg,cidrlength,result
-
-    Examples:
-    >>> q = query(s='strong-bad@email.example.com.',
-    ...           h='mx.example.org', i='192.0.2.3')
-    >>> q.validate_mechanism('A')
-    ('A', 'a', 'email.example.com', 32, 'pass')
-    
-    >>> q = query(s='strong-bad@email.example.com',
-    ...           h='mx.example.org', i='192.0.2.3')    
-    >>> q.validate_mechanism('A')
-    ('A', 'a', 'email.example.com', 32, 'pass')
-
-    >>> q.validate_mechanism('?mx:%{d}/27')
-    ('?mx:%{d}/27', 'mx', 'email.example.com', 27, 'neutral')
-
-    >>> try: q.validate_mechanism('ip4:1.2.3.4/247')
-    ... except PermError,x: print x
-    Invalid IP4 CIDR length: ip4:1.2.3.4/247
-    
-    >>> try: q.validate_mechanism('ip4:1.2.3.4/33')
-    ... except PermError,x: print x
-    Invalid IP4 CIDR length: ip4:1.2.3.4/33
-
-    >>> try: q.validate_mechanism('a:example.com:8080')
-    ... except PermError,x: print x
-    Invalid domain found (use FQDN): example.com:8080
-    
-    >>> try: q.validate_mechanism('ip4:1.2.3.444/24')
-    ... except PermError,x: print x
-    Invalid IP4 address: ip4:1.2.3.444/24
-    
-    >>> try: q.validate_mechanism('ip4:1.2.03.4/24')
-    ... except PermError,x: print x
-    Invalid IP4 address: ip4:1.2.03.4/24
-    
-    >>> try: q.validate_mechanism('-all:3030')
-    ... except PermError,x: print x
-    Invalid all mechanism format - only qualifier allowed with all: -all:3030
-
-    >>> q.validate_mechanism('-mx:%%%_/.Clara.de/27')
-    ('-mx:%%%_/.Clara.de/27', 'mx', '% /.Clara.de', 27, 'fail')
-
-    >>> q.validate_mechanism('~exists:%{i}.%{s1}.100/86400.rate.%{d}')
-    ('~exists:%{i}.%{s1}.100/86400.rate.%{d}', 'exists', '192.0.2.3.com.100/86400.rate.email.example.com', 32, 'softfail')
-
-    >>> q.validate_mechanism('a:mail.example.com.')
-    ('a:mail.example.com.', 'a', 'mail.example.com', 32, 'pass')
-        """
-        # a mechanism
-        m, arg, cidrlength, cidr6length = parse_mechanism(mech, self.d)
-        # map '?' '+' or '-' to 'neutral' 'pass' or 'fail'
-        if m:
-            result = RESULTS.get(m[0])
-            if result:
-                # eat '?' '+' or '-'
-                m = m[1:]
-            else:
-                # default pass
-                result = 'pass'
-        if m in COMMON_MISTAKES:
-            self.note_error('Unknown mechanism found', mech)
-            m = COMMON_MISTAKES[m]
-
-        if m == 'a' and RE_IP4.match(arg):
-            x = self.note_error(
-              'Use the ip4 mechanism for ip4 addresses', mech)
-            m = 'ip4'
-
-
-        # validate cidr and dual-cidr
-        if m in ('a', 'mx'):
-            if cidrlength is None:
-                cidrlength = 32;
-            elif cidrlength > 32:
-                raise PermError('Invalid IP4 CIDR length', mech)
-            if cidr6length is None:
-                cidr6length = 128
-            elif cidr6length > 128:
-                raise PermError('Invalid IP6 CIDR length', mech)
-	    if self.v == 'ip6':
-	    	cidrlength = cidr6length
-        elif m == 'ip4':
-            if cidr6length is not None:
-                raise PermError('Dual CIDR not allowed', mech)
-            if cidrlength is None:
-                cidrlength = 32;
-            elif cidrlength > 32:
-                raise PermError('Invalid IP4 CIDR length', mech)
-            if not RE_IP4.match(arg):
-                raise PermError('Invalid IP4 address', mech)
-        elif m == 'ip6':
-            if cidr6length is not None:
-                raise PermError('Dual CIDR not allowed', mech)
-            if cidrlength is None:
-                cidrlength = 128
-            elif cidrlength > 128:
-                raise PermError('Invalid IP6 CIDR length', mech)
-            if not RE_IP6.match(arg):
-                raise PermError('Invalid IP6 address', mech)
-        else:
-            if cidrlength is not None or cidr6length is not None:
-                raise PermError('CIDR not allowed', mech)
-	    cidrlength = self.cidrmax
-
-        # validate domain-spec
-        if m in ('a', 'mx', 'ptr', 'exists', 'include'):
-            # any trailing dot was removed by expand()
-            if RE_TOPLAB.split(arg)[-1]:
-                raise PermError('Invalid domain found (use FQDN)', arg)
-            arg = self.expand(arg)
-            if m == 'include':
-                if arg == self.d:
-                    if mech != 'include':
-                        raise PermError('include has trivial recursion', mech)
-                    raise PermError('include mechanism missing domain', mech)
-            return mech, m, arg, cidrlength, result
-
-        # validate 'all' mechanism per RFC 4408 ABNF
-        if m == 'all' and mech.count(':'):
-            # print '|'+ arg + '|', mech, self.d,
-            self.note_error(
-            'Invalid all mechanism format - only qualifier allowed with all'
-              , mech)
-        if m in ALL_MECHANISMS:
-            return mech, m, arg, cidrlength, result
-        if m[1:] in ALL_MECHANISMS:
-            x = self.note_error(
-                'Unknown qualifier, RFC 4408 para 4.6.1, found in', mech)
-        else:
-            x = self.note_error('Unknown mechanism found', mech)
-        return mech, m, arg, cidrlength, x
-
-    def check0(self, spf, recursion):
-        """Test this query information against SPF text.
-
-        Returns (result, mta-status-code, explanation) where
-        result in ['fail', 'unknown', 'pass', 'none']
-        """
-
-        if not spf:
-            return ('none', 250, EXPLANATIONS['none'])
-
-        # split string by whitespace, drop the 'v=spf1'
-        spf = spf.split()
-        # Catch case where SPF record has no spaces.
-        # Can never happen with conforming dns_spf(), however
-        # in the future we might want to give warnings
-        # for common mistakes like IN TXT "v=spf1" "mx" "-all"
-        # in relaxed mode.
-        if spf[0].lower() != 'v=spf1':
-	    assert strict > 1
-	    raise AmbiguityWarning('Invalid SPF record in', self.d)
-        spf = spf[1:]
-
-        # copy of explanations to be modified by exp=
-        exps = self.exps
-        redirect = None
-
-        # no mechanisms at all cause unknown result, unless
-        # overridden with 'default=' modifier
-        #
-        default = 'neutral'
-        mechs = []
-
-        # Look for modifiers
-        #
-        for mech in spf:
-            m = RE_MODIFIER.split(mech)[1:]
-            if len(m) != 2:
-                mechs.append(self.validate_mechanism(mech))
-                continue
-
-            if m[0] == 'exp':
-	        # always fetch explanation to check permerrors
-	        exp = self.get_explanation(m[1])
-	        if not recursion:
-		    # only set explanation in base recursion level
-		    self.set_explanation(exp)
-            elif m[0] == 'redirect':
-                self.check_lookups()
-                redirect = self.expand(m[1])
-            elif m[0] == 'default':
-		arg = self.expand(m[1])
-                # default=- is the same as default=fail
-                default = RESULTS.get(arg, default)
-	    else:
-		# spf rfc: 3.6 Unrecognized Mechanisms and Modifiers
-		self.expand(m[1])	# syntax error on invalid macro
-
-
-        # Evaluate mechanisms
-        #
-        for mech, m, arg, cidrlength, result in mechs:
-
-            if m == 'include':
-                self.check_lookups()
-                res, code, txt = self.check1(self.dns_spf(arg),
-                      arg, recursion + 1)
-                if res == 'pass':
-                    break
-                if res == 'none':
-                    self.note_error(
-                        'No valid SPF record for included domain: %s' %arg,
-                      mech)
-                res = 'neutral'
-                continue
-            elif m == 'all':
-                break
-
-            elif m == 'exists':
-                self.check_lookups()
-                try:
-                    if len(self.dns_a(arg,'A')) > 0:
-                        break
-                except AmbiguityWarning:
-                    # Exists wants no response sometimes so don't raise
-                    # the warning.
-                    pass
-
-            elif m == 'a':
-                self.check_lookups()
-		if self.cidrmatch(self.dns_a(arg,self.A), cidrlength):
-		    break
-
-            elif m == 'mx':
-                self.check_lookups()
-                if self.cidrmatch(self.dns_mx(arg), cidrlength):
-                    break
-
-            elif m == 'ip4':
-	        if self.v == 'in-addr': # match own connection type only
-		    try:
-			if self.cidrmatch([arg], cidrlength): break
-		    except socket.error:
-			raise PermError('syntax error', mech)
-
-            elif m == 'ip6':
-	        if self.v == 'ip6': # match own connection type only
-		    try:
-			arg = inet_pton(arg)
-			if self.cidrmatch([arg], cidrlength): break
-		    except socket.error:
-			raise PermError('syntax error', mech)
-
-            elif m == 'ptr':
-                self.check_lookups()
-                if domainmatch(self.validated_ptrs(), arg):
-                    break
-
-        else:
-            # no matches
-            if redirect:
-                #Catch redirect to a non-existant SPF record.
-                redirect_record = self.dns_spf(redirect)
-                if not redirect_record:
-                    raise PermError('redirect domain has no SPF record',
-                        redirect)
-                self.exps = dict(self.defexps)
-                return self.check1(redirect_record, redirect, recursion)
-            else:
-                result = default
-
-        if result == 'fail':
-            return (result, 550, exps[result])
-        else:
-            return (result, 250, exps[result])
-
-    def check_lookups(self):
-        self.lookups = self.lookups + 1
-        if self.lookups > MAX_LOOKUP*4:
-            raise PermError('More than %d DNS lookups'%MAX_LOOKUP*4)
-        if self.lookups > MAX_LOOKUP:
-            self.note_error('Too many DNS lookups')
-
-    def get_explanation(self, spec):
-        """Expand an explanation."""
-        if spec:
-            txt = ''.join(self.dns_txt(self.expand(spec)))
-            return self.expand(txt, stripdot=False)
-        else:
-            return 'explanation : Required option is missing'
-
-    def expand(self, str, stripdot=True): # macros='slodipvh'
-        """Do SPF RFC macro expansion.
-
-        Examples:
-        >>> q = query(s='strong-bad@email.example.com',
-        ...           h='mx.example.org', i='192.0.2.3')
-        >>> q.p = 'mx.example.org'
-        >>> q.r = 'example.net'
-
-        >>> q.expand('%{d}')
-        'email.example.com'
-
-        >>> q.expand('%{d4}')
-        'email.example.com'
-
-        >>> q.expand('%{d3}')
-        'email.example.com'
-
-        >>> q.expand('%{d2}')
-        'example.com'
-
-        >>> q.expand('%{d1}')
-        'com'
-
-        >>> q.expand('%{p}')
-        'mx.example.org'
-
-        >>> q.expand('%{p2}')
-        'example.org'
-
-        >>> q.expand('%{dr}')
-        'com.example.email'
-    
-        >>> q.expand('%{d2r}')
-        'example.email'
-
-        >>> q.expand('%{l}')
-        'strong-bad'
-
-        >>> q.expand('%{l-}')
-        'strong.bad'
-
-        >>> q.expand('%{lr}')
-        'strong-bad'
-
-        >>> q.expand('%{lr-}')
-        'bad.strong'
-
-        >>> q.expand('%{l1r-}')
-        'strong'
-
-        >>> q.expand('%{c}',stripdot=False)
-        '192.0.2.3'
-
-        >>> q.expand('%{r}',stripdot=False)
-        'example.net'
-
-        >>> q.expand('%{ir}.%{v}._spf.%{d2}')
-        '3.2.0.192.in-addr._spf.example.com'
-
-        >>> q.expand('%{lr-}.lp._spf.%{d2}')
-        'bad.strong.lp._spf.example.com'
-
-        >>> q.expand('%{lr-}.lp.%{ir}.%{v}._spf.%{d2}')
-        'bad.strong.lp.3.2.0.192.in-addr._spf.example.com'
-
-        >>> q.expand('%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}')
-        '3.2.0.192.in-addr.strong.lp._spf.example.com'
-
-        >>> try: q.expand('%(ir).%{v}.%{l1r-}.lp._spf.%{d2}')
-        ... except PermError,x: print x
-        invalid-macro-char : %(ir)
-
-        >>> q.expand('%{p2}.trusted-domains.example.net')
-        'example.org.trusted-domains.example.net'
-
-        >>> q.expand('%{p2}.trusted-domains.example.net.')
-        'example.org.trusted-domains.example.net'
-
-        >>> q = query(s='@email.example.com',
-        ...           h='mx.example.org', i='192.0.2.3')
-        >>> q.p = 'mx.example.org'
-        >>> q.expand('%{l}')
-        'postmaster'
-
-        """
-        macro_delimiters = ['{', '%', '-', '_']
-        end = 0
-        result = ''
-        macro_count = str.count('%')
-        if macro_count != 0:
-            labels = str.split('.')
-            for label in labels:
-                is_macro = False
-                if len(label) > 1:
-                    if label[0] == '%':
-                        for delimit in macro_delimiters:
-                            if label[1] == delimit:
-                                is_macro = True
-                        if not is_macro:
-                            raise PermError ('invalid-macro-char ', label)
-                            break
-        for i in RE_CHAR.finditer(str):
-            result += str[end:i.start()]
-            macro = str[i.start():i.end()]
-            if macro == '%%':
-                result += '%'
-            elif macro == '%_':
-                result += ' '
-            elif macro == '%-':
-                result += '%20'
-            else:
-                letter = macro[2].lower()
-#                print letter
-                if letter == 'p':
-                    self.getp()
-		elif letter in 'crt' and stripdot:
-		    raise PermError(
-		        'c,r,t macros allowed in exp= text only', macro)
-                expansion = getattr(self, letter, self)
-                if expansion:
-                    if expansion == self:
-                        raise PermError('Unknown Macro Encountered', macro) 
-		    e = expand_one(expansion, macro[3:-1], JOINERS.get(letter))
-		    if letter != macro[2]:
-		        e = urllib.quote(e)
-                    result += e
-
-            end = i.end()
-        result += str[end:]
-        if stripdot and result.endswith('.'):
-            result =  result[:-1]
-        if result.count('.') != 0:
-            if len(result) > 253:
-                result = result[(result.index('.')+1):]
-        return result
-
-    def dns_spf(self, domain):
-        """Get the SPF record recorded in DNS for a specific domain
-        name.  Returns None if not found, or if more than one record
-        is found.
-        """
-	# Per RFC 4.3/1, check for malformed domain.  This produces
-	# no results as a special case.
-	for label in domain.split('.'):
-	  if not label or len(label) > 63:
-	    return None
-        # for performance, check for most common case of TXT first
-        a = [t for t in self.dns_txt(domain) if RE_SPF.match(t)]
-        if len(a) > 1:
-            raise PermError('Two or more type TXT spf records found.')
-        if len(a) == 1 and self.strict < 2:
-            return a[0]               
-        # check official SPF type first when it becomes more popular
-        try:
-            b = [t for t in self.dns_99(domain) if RE_SPF.match(t)]
-        except TempError,x:
-            # some braindead DNS servers hang on type 99 query
-            if self.strict > 1: raise TempError(x)
-            b = []
-
-        if len(b) > 1:
-            raise PermError('Two or more type SPF spf records found.')
-        if len(b) == 1:
-            if self.strict > 1 and len(a) == 1 and a[0] != b[0]:
-            #Changed from permerror to warning based on RFC 4408 Auth 48 change
-                raise AmbiguityWarning(
-'v=spf1 records of both type TXT and SPF (type 99) present, but not identical')
-            return b[0]
-        if len(a) == 1:
-            return a[0]    # return TXT if SPF wasn't found
-        if DELEGATE:    # use local record if neither found
-            a = [t
-              for t in self.dns_txt(domain+'._spf.'+DELEGATE)
-            if RE_SPF.match(t)
-            ]
-            if len(a) == 1: return a[0]
-        return None
-
-    def dns_txt(self, domainname):
-        "Get a list of TXT records for a domain name."
-        if domainname:
-            return [''.join(a) for a in self.dns(domainname, 'TXT')]
-        return []
-    def dns_99(self, domainname):
-        "Get a list of type SPF=99 records for a domain name."
-        if domainname:
-            return [''.join(a) for a in self.dns(domainname, 'SPF')]
-        return []
-
-    def dns_mx(self, domainname):
-        """Get a list of IP addresses for all MX exchanges for a
-        domain name.
-        """
-        # RFC 4408 section 5.4 "mx"
-        # To prevent DoS attacks, more than 10 MX names MUST NOT be looked up
-        mxnames = self.dns(domainname, 'MX')
-        if self.strict:
-            max = MAX_MX
-            if self.strict > 1:
-                if len(mxnames) > MAX_MX:
-                    raise AmbiguityWarning(
-                        'More than %d MX records returned'%MAX_MX)
-                if len(mxnames) == 0:
-                    raise AmbiguityWarning(
-                        'No MX records found for mx mechanism', domainname)
-        else:
-            max = MAX_MX * 4
-        return [a for mx in mxnames[:max] for a in self.dns_a(mx[1],self.A)]
-
-    def dns_a(self, domainname, A='A'):
-        """Get a list of IP addresses for a domainname.
-	"""
-        if not domainname: return []
-        if self.strict > 1:
-            alist = self.dns(domainname, A)
-            if len(alist) == 0:
-                raise AmbiguityWarning(
-			'No %s records found for'%A, domainname)
-            else:
-                return alist
-        return self.dns(domainname, A)
-
-    def validated_ptrs(self):
-        """Figure out the validated PTR domain names for the connect IP."""
-# To prevent DoS attacks, more than 10 PTR names MUST NOT be looked up
-        if self.strict:
-            max = MAX_PTR
-            if self.strict > 1:
-                #Break out the number of PTR records returned for testing
-                try:
-                    ptrnames = self.dns_ptr(self.i)
-                    if len(ptrnames) > max:
-                        warning = 'More than %d PTR records returned' % max
-                        raise AmbiguityWarning(warning, i)
-                    else:
-                        if len(ptrnames) == 0:
-                            raise AmbiguityWarning(
-                                'No PTR records found for ptr mechanism', self.c)
-                except:
-                    raise AmbiguityWarning(
-                      'No PTR records found for ptr mechanism', i)
-        else:
-            max = MAX_PTR * 4
-	cidrlength = self.cidrmax
-        return [p for p in self.dns_ptr(self.i)[:max]
-	    if self.cidrmatch(self.dns_a(p,self.A),cidrlength)]
-
-    def dns_ptr(self, i):
-        """Get a list of domain names for an IP address."""
-        return self.dns('%s.%s.arpa'%(reverse_dots(i),self.v), 'PTR')
-
-    # 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.  
-
-    SAFE2CACHE = {
-      ('MX','A'): None,
-      ('MX','MX'): None,
-      ('CNAME','A'): None,
-      ('CNAME','CNAME'): None,
-      ('A','A'): None,
-      ('AAAA','AAAA'): None,
-      ('PTR','PTR'): None,
-      ('TXT','TXT'): None,
-      ('SPF','SPF'): None
-    }
-
-    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 PermError('Length of CNAME chain exceeds %d' % MAX_CNAME)
-            cnames[name] = cname
-            if cname in cnames:
-                raise PermError, 'CNAME loop'
-            result = self.dns(cname, qtype, cnames=cnames)
-        return result
-
-    def cidrmatch(self, ipaddrs, n):
-	"""Match connect IP against a list of other IP addresses."""
-	try:
-	    if self.v == 'ip6':
-	        MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
-		bin = bin2long6
-	    else:
-	        MASK = 0xFFFFFFFFL
-		bin = addr2bin
-	    c = ~(MASK >> n) & MASK & self.ip
-	    for ip in [bin(ip) for ip in ipaddrs]:
-		if c == ~(MASK >> n) & MASK & ip: return True
-	except socket.error: pass
-	return False
-
-    def get_header(self, res, receiver=None):
-        if not receiver:
-            receiver = self.r
-        if res in ('pass', 'fail',' softfail'):
-            return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
-                res, receiver, self.get_header_comment(res), self.c,
-                self.l + '@' + self.o, self.h)
-        if res == 'permerror':
-            return '%s (%s: %s)' % (' '.join([res] + self.mech),
-            receiver,self.get_header_comment(res))
-        return '%s (%s: %s)' % (res, receiver, self.get_header_comment(res))
-
-    def get_header_comment(self, res):
-        """Return comment for Received-SPF header.
-        """
-        sender = self.o
-        if res == 'pass':
-            return \
-                "domain of %s designates %s as permitted sender" \
-                % (sender, self.c)
-        elif res == 'softfail': return \
-      "transitioning domain of %s does not designate %s as permitted sender" \
-            % (sender, self.c)
-        elif res == 'neutral': return \
-            "%s is neither permitted nor denied by domain of %s" \
-                % (self.c, sender)
-        elif res == 'none': return \
-            "%s is neither permitted nor denied by domain of %s" \
-                  % (self.c, sender)
-            #"%s does not designate permitted sender hosts" % sender
-        elif res == 'permerror': return \
-            "permanent error in processing domain of %s: %s" \
-                  % (sender, self.prob)
-        elif res == 'error': return \
-              "temporary error in processing during lookup of %s" % sender
-        elif res == 'fail': return \
-              "domain of %s does not designate %s as permitted sender" \
-              % (sender, self.c)
-        raise ValueError("invalid SPF result for header comment: "+res)
-
-def split_email(s, h):
-    """Given a sender email s and a HELO domain h, create a valid tuple
-    (l, d) local-part and domain-part.
-
-    Examples:
-    >>> split_email('', 'wayforward.net')
-    ('postmaster', 'wayforward.net')
-
-    >>> split_email('foo.com', 'wayforward.net')
-    ('postmaster', 'foo.com')
-
-    >>> split_email('terry@wayforward.net', 'optsw.com')
-    ('terry', 'wayforward.net')
-    """
-    if not s:
-        return 'postmaster', h
-    else:
-        parts = s.split('@', 1)
-        if parts[0] == '':
-            parts[0] = 'postmaster'
-        if len(parts) == 2:
-            return tuple(parts)
-        else:
-            return 'postmaster', s
-
-def parse_mechanism(str, d):
-    """Breaks A, MX, IP4, and PTR mechanisms into a (name, domain,
-    cidr,cidr6) tuple.  The domain portion defaults to d if not present,
-    the cidr defaults to 32 if not present.
-
-    Examples:
-    >>> parse_mechanism('a', 'foo.com')
-    ('a', 'foo.com', None, None)
-
-    >>> parse_mechanism('a:bar.com', 'foo.com')
-    ('a', 'bar.com', None, None)
-
-    >>> parse_mechanism('a/24', 'foo.com')
-    ('a', 'foo.com', 24, None)
-
-    >>> parse_mechanism('A:foo:bar.com/16', 'foo.com')
-    ('a', 'foo:bar.com', 16, None)
-
-    >>> parse_mechanism('-exists:%{i}.%{s1}.100/86400.rate.%{d}','foo.com')
-    ('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', None, None)
-
-    >>> parse_mechanism('mx:%%%_/.Claranet.de/27','foo.com')
-    ('mx', '%%%_/.Claranet.de', 27, None)
-
-    >>> parse_mechanism('mx:%{d}/27','foo.com')
-    ('mx', '%{d}', 27, None)
-
-    >>> parse_mechanism('iP4:192.0.0.0/8','foo.com')
-    ('ip4', '192.0.0.0', 8, None)
-    """
-
-    a = RE_DUAL_CIDR.split(str)
-    if len(a) == 3:
-        str, cidr6 = a[0], int(a[1])
-    else:
-        cidr6 = None
-    a = RE_CIDR.split(str)
-    if len(a) == 3:
-        str, cidr = a[0], int(a[1])
-    else:
-        cidr = None
-
-    a = str.split(':', 1)
-    if len(a) < 2:
-        return str.lower(), d, cidr, cidr6
-    return a[0].lower(), a[1], cidr, cidr6
-
-def reverse_dots(name):
-    """Reverse dotted IP addresses or domain names.
-
-    Example:
-    >>> reverse_dots('192.168.0.145')
-    '145.0.168.192'
-
-    >>> reverse_dots('email.example.com')
-    'com.example.email'
-    """
-    a = name.split('.')
-    a.reverse()
-    return '.'.join(a)
-
-def domainmatch(ptrs, domainsuffix):
-    """grep for a given domain suffix against a list of validated PTR
-    domain names.
-
-    Examples:
-    >>> domainmatch(['FOO.COM'], 'foo.com')
-    1
-
-    >>> domainmatch(['moo.foo.com'], 'FOO.COM')
-    1
-
-    >>> domainmatch(['moo.bar.com'], 'foo.com')
-    0
-
-    """
-    domainsuffix = domainsuffix.lower()
-    for ptr in ptrs:
-        ptr = ptr.lower()
-
-        if ptr == domainsuffix or ptr.endswith('.' + domainsuffix):
-            return True
-
-    return False
-
-def addr2bin(str):
-    """Convert a string IPv4 address into an unsigned integer.
-
-    Examples::
-    >>> addr2bin('127.0.0.1')
-    2130706433L
-
-    >>> addr2bin('127.0.0.1') == socket.INADDR_LOOPBACK
-    1
-
-    >>> addr2bin('255.255.255.254')
-    4294967294L
-
-    >>> addr2bin('192.168.0.1')
-    3232235521L
-
-    Unlike DNS.addr2bin, the n, n.n, and n.n.n forms for IP addresses
-    are handled as well::
-    >>> addr2bin('10.65536')
-    167837696L
-    >>> 10 * (2 ** 24) + 65536
-    167837696
-
-    >>> addr2bin('10.93.512')
-    173867520L
-    >>> 10 * (2 ** 24) + 93 * (2 ** 16) + 512
-    173867520
-    """
-    return struct.unpack("!L", socket.inet_aton(str))[0]
-
-def bin2long6(str):
-    h, l = struct.unpack("!QQ", str)
-    return h << 64 | l
-
-if socket.has_ipv6:
-    def inet_ntop(s):
-        return socket.inet_ntop(socket.AF_INET6,s)
-    def inet_pton(s):
-        return socket.inet_pton(socket.AF_INET6,s)
-else:
-    def inet_ntop(s):
-      """Convert ip6 address to standard hex notation.
-      Examples:
-      >>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0xFFFF,0x0102,0x0304))
-      '::FFFF:1.2.3.4'
-      >>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0,0,0,0x0102,0x0304))
-      '1234:5678::102:304'
-      >>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0x1234,0x5678,0,0x0102,0x0304))
-      '::1234:5678:0:102:304'
-      >>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0x0102,0x0304,0,0,0))
-      '1234:5678:0:102:304::'
-      >>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0,0,0))
-      '::'
-      """
-      # convert to 8 words
-      a = struct.unpack("!HHHHHHHH",s)
-      n = (0,0,0,0,0,0,0,0)	# null ip6
-      if a == n: return '::'
-      # check for ip4 mapped
-      if a[:5] == (0,0,0,0,0) and a[5] in (0,0xFFFF):
-	ip4 = '.'.join([str(i) for i in struct.unpack("!HHHHHHBBBB",s)[6:]])
-	if a[5]:
-	  return "::FFFF:" + ip4
-	return "::" + ip4
-      # find index of longest sequence of 0
-      for l in (7,6,5,4,3,2,1):
-	e = n[:l]
-	for i in range(9-l):
-	  if a[i:i+l] == e:
-	    if i == 0:
-	      return ':'+':%x'*(8-l) % a[l:]
-	    if i == 8 - l:
-	      return '%x:'*(8-l) % a[:-l] + ':'
-	    return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:]
-      return "%x:%x:%x:%x:%x:%x:%x:%x" % a
-
-    def inet_pton(p):
-      """Convert ip6 standard hex notation to ip6 address.
-      Examples:
-      >>> struct.unpack('!HHHHHHHH',inet_pton('::'))
-      (0, 0, 0, 0, 0, 0, 0, 0)
-      >>> struct.unpack('!HHHHHHHH',inet_pton('::1234'))
-      (0, 0, 0, 0, 0, 0, 0, 4660)
-      >>> struct.unpack('!HHHHHHHH',inet_pton('1234::'))
-      (4660, 0, 0, 0, 0, 0, 0, 0)
-      >>> struct.unpack('!HHHHHHHH',inet_pton('1234::5678'))
-      (4660, 0, 0, 0, 0, 0, 0, 22136)
-      >>> struct.unpack('!HHHHHHHH',inet_pton('::FFFF:1.2.3.4'))
-      (0, 0, 0, 0, 0, 65535, 258, 772)
-      >>> struct.unpack('!HHHHHHHH',inet_pton('1.2.3.4'))
-      (0, 0, 0, 0, 0, 65535, 258, 772)
-      >>> try: inet_pton('::1.2.3.4.5')
-      ... except ValueError,x: print x
-      ::1.2.3.4.5
-      """
-      if p == '::':
-	return '\0'*16
-      s = p
-      m = RE_IP4.search(s)
-      try:
-	  if m:
-	      pos = m.start()
-	      ip4 = [int(i) for i in s[pos:].split('.')]
-	      if not pos:
-	          return struct.pack('!QLBBBB',0,65535,*ip4)
-	      s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4)
-	  a = s.split('::')
-	  if len(a) == 2:
-	    l,r = a
-	    if not l:
-	      r = r.split(':')
-	      return struct.pack('!HHHHHHHH',
-		*[0]*(8-len(r)) + [int(s,16) for s in r])
-	    if not r:
-	      l = l.split(':')
-	      return struct.pack('!HHHHHHHH',
-		*[int(s,16) for s in l] + [0]*(8-len(l)))
-	    l = l.split(':')
-	    r = r.split(':')
-	    return struct.pack('!HHHHHHHH',
-		*[int(s,16) for s in l] + [0]*(8-len(l)-len(r))
-		+ [int(s,16) for s in r])
-	  if len(a) == 1:
-	    return struct.pack('!HHHHHHHH',
-		*[int(s,16) for s in a[0].split(':')])
-      except ValueError: pass
-      raise ValueError,p
-
-def expand_one(expansion, str, joiner):
-    if not str:
-        return expansion
-    ln, reverse, delimiters = RE_ARGS.split(str)[1:4]
-    if not delimiters:
-        delimiters = '.'
-    expansion = split(expansion, delimiters, joiner)
-    if reverse: expansion.reverse()
-    if ln: expansion = expansion[-int(ln)*2+1:]
-    return ''.join(expansion)
-
-def split(str, delimiters, joiner=None):
-    """Split a string into pieces by a set of delimiter characters.  The
-    resulting list is delimited by joiner, or the original delimiter if
-    joiner is not specified.
-
-    Examples:
-    >>> split('192.168.0.45', '.')
-    ['192', '.', '168', '.', '0', '.', '45']
-
-    >>> split('terry@wayforward.net', '@.')
-    ['terry', '@', 'wayforward', '.', 'net']
-
-    >>> split('terry@wayforward.net', '@.', '.')
-    ['terry', '.', 'wayforward', '.', 'net']
-    """
-    result, element = [], ''
-    for c in str:
-        if c in delimiters:
-            result.append(element)
-            element = ''
-            if joiner:
-                result.append(joiner)
-            else:
-                result.append(c)
-        else:
-            element += c
-    result.append(element)
-    return result
-
-def insert_libspf_local_policy(spftxt, local=None):
-    """Returns spftxt with local inserted just before last non-fail
-    mechanism.  This is how the libspf{2} libraries handle "local-policy".
-    
-    Examples:
-    >>> insert_libspf_local_policy('v=spf1 -all')
-    'v=spf1 -all'
-    >>> insert_libspf_local_policy('v=spf1 -all','mx')
-    'v=spf1 -all'
-    >>> insert_libspf_local_policy('v=spf1','a mx ptr')
-    'v=spf1 a mx ptr'
-    >>> insert_libspf_local_policy('v=spf1 mx -all','a ptr')
-    'v=spf1 mx a ptr -all'
-    >>> insert_libspf_local_policy('v=spf1 mx -include:foo.co +all','a ptr')
-    'v=spf1 mx a ptr -include:foo.co +all'
-
-    # FIXME: is this right?  If so, "last non-fail" is a bogus description.
-    >>> insert_libspf_local_policy('v=spf1 mx ?include:foo.co +all','a ptr')
-    'v=spf1 mx a ptr ?include:foo.co +all'
-    >>> spf='v=spf1 ip4:1.2.3.4 -a:example.net -all'
-    >>> local='ip4:192.0.2.3 a:example.org'
-    >>> insert_libspf_local_policy(spf,local)
-    'v=spf1 ip4:1.2.3.4 ip4:192.0.2.3 a:example.org -a:example.net -all'
-    """
-    # look to find the all (if any) and then put local
-    # just after last non-fail mechanism.  This is how
-    # libspf2 handles "local policy", and some people
-    # apparently find it useful (don't ask me why).
-    if not local: return spftxt
-    spf = spftxt.split()[1:]
-    if spf:
-        # local policy is SPF mechanisms/modifiers with no
-        # 'v=spf1' at the start
-        spf.reverse() #find the last non-fail mechanism
-        for mech in spf:
-        # map '?' '+' or '-' to 'neutral' 'pass'
-        # or 'fail'
-            if not RESULTS.get(mech[0]):
-                # actually finds last mech with default result
-                where = spf.index(mech)
-                spf[where:where] = [local]
-                spf.reverse()
-                local = ' '.join(spf)
-                break
-        else:
-            return spftxt # No local policy adds for v=spf1 -all
-    # Processing limits not applied to local policy.  Suggest
-    # inserting 'local' mechanism to handle this properly
-    #MAX_LOOKUP = 100 
-    return 'v=spf1 '+local
-
-def _test():
-    import doctest, spf
-    return doctest.testmod(spf)
-
-DNS.DiscoverNameServers() # Fails on Mac OS X? Add domain to /etc/resolv.conf
-
-if __name__ == '__main__':
-    import sys
-    if len(sys.argv) == 1:
-        print USAGE
-        _test()
-    elif len(sys.argv) == 2:
-        q = query(i='127.0.0.1', s='localhost', h='unknown',
-            receiver=socket.gethostname())
-        print q.dns_spf(sys.argv[1])
-    elif len(sys.argv) == 4:
-        print check(i=sys.argv[1], s=sys.argv[2], h=sys.argv[3],
-            receiver=socket.gethostname())
-    elif len(sys.argv) == 5:
-        i, s, h = sys.argv[2:]
-        q = query(i=i, s=s, h=h, receiver=socket.gethostname(),
-            strict=False)
-        print q.check(sys.argv[1])
-        if q.perm_error and q.perm_error.ext:
-            print q.perm_error.ext
-    else:
-        print USAGE
diff --git a/spfquery.py b/spfquery.py
deleted file mode 100755
index 96f813c..0000000
--- a/spfquery.py
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/python2.3
-
-# Author: Stuart D. Gathman <stuart@bmsi.com>
-# Copyright 2004 Business Management Systems, Inc.
-# This code is under the GNU General Public License.  See COPYING for details.
-
-# $Log$
-# Revision 1.1.1.1  2005/05/31 18:07:19  customdesigned
-# Release 0.6.9
-#
-# Revision 2.3  2004/04/19 22:12:11  stuart
-# Release 0.6.9
-#
-# Revision 2.2  2004/04/18 03:29:35  stuart
-# Pass most tests except -local and -rcpt-to
-#
-# Revision 2.1  2004/04/08 18:41:15  stuart
-# Reject numeric hello names
-#
-# Driver for SPF test system
-
-import spf
-import sys
-
-from optparse import OptionParser
-
-class PerlOptionParser(OptionParser):
-    def _process_args (self, largs, rargs, values):
-        """_process_args(largs : [string],
-                         rargs : [string],
-                         values : Values)
-
-        Process command-line arguments and populate 'values', consuming
-        options and arguments from 'rargs'.  If 'allow_interspersed_args' is
-        false, stop at the first non-option argument.  If true, accumulate any
-        interspersed non-option arguments in 'largs'.
-        """
-        while rargs:
-            arg = rargs[0]
-            # We handle bare "--" explicitly, and bare "-" is handled by the
-            # standard arg handler since the short arg case ensures that the
-            # len of the opt string is greater than 1.
-            if arg == "--":
-                del rargs[0]
-                return
-            elif arg[0:2] == "--":
-                # process a single long option (possibly with value(s))
-                self._process_long_opt(rargs, values)
-            elif arg[:1] == "-" and len(arg) > 1:
-                # process a single perl style long option
-		rargs[0] = '-' + arg
-                self._process_long_opt(rargs, values)
-            elif self.allow_interspersed_args:
-                largs.append(arg)
-                del rargs[0]
-            else:
-		return
-
-def format(q):
-  res,code,txt = q.check()
-  print res
-  if res in ('pass','neutral','unknown'): print
-  else: print txt
-  print 'spfquery:',q.get_header_comment(res)
-  print 'Received-SPF:',q.get_header(res,'spfquery')
-
-def main(argv):
-  parser = PerlOptionParser()
-  parser.add_option("--file",dest="file")
-  parser.add_option("--ip",dest="ip")
-  parser.add_option("--sender",dest="sender")
-  parser.add_option("--helo",dest="hello_name")
-  parser.add_option("--local",dest="local_policy")
-  parser.add_option("--rcpt-to",dest="rcpt")
-  parser.add_option("--default-explanation",dest="explanation")
-  parser.add_option("--sanitize",type="int",dest="sanitize")
-  parser.add_option("--debug",type="int",dest="debug")
-  opts,args = parser.parse_args(argv)
-  if opts.ip:
-    q = spf.query(opts.ip,opts.sender,opts.hello_name,local=opts.local_policy)
-    if opts.explanation:
-      q.set_default_explanation(opts.explanation)
-    format(q)
-  if opts.file:
-    if opts.file == '0':
-      fp = sys.stdin
-    else:
-      fp = open(opts.file,'r')
-    for ln in fp:
-      ip,sender,helo,rcpt = ln.split(None,3)
-      q = spf.query(ip,sender,helo,local=opts.local_policy)
-      if opts.explanation:
-	q.set_default_explanation(opts.explanation)
-      format(q)
-    fp.close()
-    
-if __name__ == "__main__":
-  import sys
-  main(sys.argv[1:])
-- 
GitLab