diff --git a/spf.py b/spf.py
index e21f84e73e0a72fdd456b7e09413497f8ee79e16..a18af65785124fdbe0f2816f283b6e5084febc86 100755
--- a/spf.py
+++ b/spf.py
@@ -2,8 +2,8 @@
 """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>
+Portions Copyright (c) 2004,2005 Stuart Gathman <stuart@bmsi.com>
+Portions Copyright (c) 2005 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.
@@ -48,6 +48,9 @@ For news, bugfixes, etc. visit the home page for this implementation at
 # Terrence is not responding to email.
 #
 # $Log$
+# Revision 1.25  2006/07/31 15:25:39  customdesigned
+# Permerror for multiple TXT SPF records.
+#
 # Revision 1.24  2006/07/28 01:21:33  customdesigned
 # Remove debug print
 #
@@ -334,7 +337,7 @@ def isSPF(txt):
 MASK = 0xFFFFFFFFL
 
 # Regular expression to look for modifiers
-RE_MODIFIER = re.compile(r'^([a-zA-Z0-9_\-\.]+)=')
+RE_MODIFIER = re.compile(r'^([a-z][a-z0-9_\-\.]*)=',re.IGNORECASE)
 
 # Regular expression to find macro expansions
 RE_CHAR = re.compile(r'%(%|_|-|(\{[a-zA-Z][0-9]*r?[^\}]*\}))')
@@ -342,10 +345,28 @@ RE_CHAR = re.compile(r'%(%|_|-|(\{[a-zA-Z][0-9]*r?[^\}]*\}))')
 # Regular expression to break up a macro expansion
 RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)')
 
-RE_CIDR = re.compile(r'/([1-9]|1[0-9]|2[0-9]|3[0-2])$')
-
-RE_IP4 = re.compile(r'\.'.join(
-        [r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)+'$')
+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]$',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
@@ -353,18 +374,55 @@ RE_IP4 = re.compile(r'\.'.join(
 JOINERS = {'l': '.', 's': '.'}
 
 RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail',
-           'pass': 'pass', 'fail': 'fail', 'unknown': 'unknown',
+           'pass': 'pass', 'fail': 'fail', 'permerror': 'permerror',
 	   'error': 'error', 'neutral': 'neutral', 'softfail': 'softfail',
-	   'none': 'none', 'deny': 'fail' }
-
-EXPLANATIONS = {'pass': 'sender SPF verified', 'fail': 'access denied',
-                'unknown': 'permanent error in processing',
-                'error': 'temporary error in processing',
-		'softfail': 'domain in transition',
+	   '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': ''
+		'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'
 		}
 
+#Default receiver policies - can be overridden.
+POLICY = {'tfwl': False, #Check trusted-forwarder.org
+          'skip_localhost': True, #Don't check SPF on local connections
+          'always_helo': False, #Only works if helo_first is also True.
+          'spf_helo_mustpass': True, #Treat HELO test returning softfail or
+          #neutral as Fail - HELO should be a single IP per name.  No reason to
+          #accept SPF relaxed provisions for HELO.  No affect if None.
+          'reject_helo_fail': False, 
+          'spf_reject_fail': True,
+          'spf_reject_neutral': False,
+          'spf_accept_softfail': True,
+          'spf_best_guess': True,
+          'spf_strict': True,
+		}
+# Recommended SMTP codes for certain SPF results.  For results not in
+# this table the recommendation is to accept the message as authorized.
+# An SPF result is never enough to recommend that a message be accepted for
+# delivery.  Additional checks are generally required.
+# The softfail result requires special processing.
+
+SMTP_CODES = {
+  'fail': [550,'5.7.1'],
+  'temperror': [451,'4.4.3'],
+  'permerror': [550,'5.5.2'],
+  'softfail': [451,'4.3.0']
+		}
+if not POLICY['spf_accept_softfail']:
+    SMTP_CODES['softfail'] = (550,'5.7.1')
+if POLICY['spf_reject_neutral']:
+    SMTP_CODES['neutral'] = (550,'5.7.1')
 # if set to a domain name, search _spf.domain namespace if no SPF record
 # found in source domain.
 
@@ -381,17 +439,46 @@ except NameError:
 # 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' }
 
+
+#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"
@@ -409,13 +496,10 @@ 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.
 
-	Returns (result, mta-status-code, explanation) where result in
-	['pass', 'unknown', 'fail', 'error', 'softfail', 'none', 'neutral' ].
+	Returns (result,  code, explanation) where result in
+	['pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', 'neutral' ].
 
 	Example:
-	>>> check(i='127.0.0.1', s='terry@wayforward.net', h='localhost')
-	('pass', 250, 'local connections always pass')
-
 	#>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')
 
 	"""
@@ -438,7 +522,7 @@ class query(object):
 	This is also, by design, the same variables used in SPF macro
 	expansion.
 
-	Also keeps cache: DNS cache.
+	Also keeps cache: DNS cache.  
 	"""
 	def __init__(self, i, s, h,local=None,receiver=None,strict=True):
 		self.i, self.s, self.h = i, s, h
@@ -451,17 +535,22 @@ class query(object):
 		self.p = None
 		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.local = local	# local policy
+		self.libspf_local = local	# local policy
     		self.lookups = 0
-		# strict can be False, True, or 2 for harsh
+		# strict can be False, True, or 2 (numeric) for harsh
 		self.strict = strict
-		self.perm_error = None
 
 	def set_default_explanation(self,exp):
 		exps = self.exps
-		for i in 'softfail','fail','unknown':
+		for i in 'softfail','fail','permerror':
 		  exps[i] = exp
 
 	def getp(self):
@@ -477,10 +566,11 @@ class query(object):
 		"""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' 'unknown', 'pass', 'error', 'none']
+	in ['fail', 'softfail', 'neutral' 'permerror', 'pass', 'temperror', 'none']
 
 	Examples:
 	>>> q = query(s='strong-bad@email.example.com',
@@ -489,34 +579,42 @@ class query(object):
 	('neutral', 250, 'access neither permitted nor denied')
 
 	>>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ?all moo')
-	('unknown', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
+	('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
 
 	>>> q.check(spf='v=spf1 =a ?all moo')
-	('unknown', 550, 'SPF Permanent Error: Unknown qualifier, RFC 4408 para 4.6.1, found in: =a')
+	('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 verified')
+	('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')
-	('unknown', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
-	>>> q.perm_error.ext
-	('pass', 250, 'sender SPF verified')
+	('pass', 250, 'sender SPF authorized')
 
 	>>> q.check(spf='v=spf1 ip4:192.1.0.0/16 moo -all')
-	('unknown', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
-	>>> str(q.perm_error.ext)
-	'None'
+	('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
 
 	>>> q.check(spf='v=spf1 ip4:192.1.0.0/16 ~all')
-	('softfail', 250, 'domain in transition')
+	('softfail', 250, 'domain owner discourages use of this host')
 
 	>>> q.check(spf='v=spf1 -ip4:192.1.0.0/6 ~all')
-	('fail', 550, 'access denied')
+	('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')
 		"""
 		self.mech = []		# unknown mechanisms
 		# If not strict, certain PermErrors (mispelled
@@ -524,23 +622,20 @@ class query(object):
 		# will continue processing.  However, the exception
 		# that strict processing would raise is saved here
 		self.perm_error = None
-		if self.i.startswith('127.'):
-			return ('pass', 250, 'local connections always pass')
 
 		try:
 			self.lookups = 0
 			if not spf:
 			    spf = self.dns_spf(self.d)
-			if self.local and spf:
-			    spf += ' ' + self.local
-			rc = self.check1(spf, self.d, 0)
-			if self.perm_error:
-			  # extended processing succeeded, but strict failed
-			  self.perm_error.ext = rc
-			  raise self.perm_error
-			return rc
+			if self.libspf_local and spf: 
+			    spf = insert_libspf_local_policy(
+			    	spf,self.libspf_local)
+                        return self.check1(spf, self.d, 0)
 		except TempError,x:
-			return ('error', 450, 'SPF Temporary Error: ' + str(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
@@ -549,7 +644,7 @@ class query(object):
 		      self.mech.append(x.mech)
 		    # Pre-Lentczner draft treats this as an unknown result
 		    # and equivalent to no SPF record.
-		    return ('unknown', 550, 'SPF Permanent Error: ' + str(x))
+		    return ('permerror', 550, 'SPF Permanent Error: ' + str(x))
 
 	def check1(self, spf, domain, recursion):
 		# spf rfc: 3.7 Processing Limits
@@ -566,10 +661,16 @@ class query(object):
 			# a PermError.
 			raise PermError('Too many levels of recursion')
 		try:
-			tmp, self.d = self.d, domain
-			return self.check0(spf,recursion)
-		finally:
-			self.d = tmp
+		  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:
@@ -579,6 +680,8 @@ class query(object):
 	      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
 
@@ -591,9 +694,9 @@ class query(object):
 	...           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')
+	...           h='mx.example.org', i='192.0.2.3')	
 	>>> q.validate_mechanism('A')
 	('A', 'a', 'email.example.com', 32, 'pass')
 
@@ -602,11 +705,11 @@ class query(object):
 
 	>>> try: q.validate_mechanism('ip4:1.2.3.4/247')
 	... except PermError,x: print x
-	Invalid IP4 address: ip4:1.2.3.4/247
+	Invalid IP4 CIDR length: ip4:1.2.3.4/247
 
 	>>> try: q.validate_mechanism('a:example.com:8080')
 	... except PermError,x: print x
-	Too many :. Not allowed in domain name.: a:example.com:8080
+	Invalid domain found (use FQDN): example.com:8080
 	
 	>>> try: q.validate_mechanism('ip4:1.2.3.444/24')
 	... except PermError,x: print x
@@ -621,10 +724,13 @@ class query(object):
 
 	>>> 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 = parse_mechanism(mech, self.d)
-		# map '?' '+' or '-' to 'unknown' 'pass' or 'fail'
+		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:
@@ -641,35 +747,57 @@ class query(object):
 		  x = self.note_error(
 		    'Use the ip4 mechanism for ip4 addresses',mech)
 		  m = 'ip4'
-		# Check for : within the arguement
-		if arg.count(':') > 0:
-		  raise PermError('Too many :. Not allowed in domain name.',mech)
+
+
+		# validate cidr and dual-cidr
+		if m in ('a', 'mx', 'ptr'):
+		  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)
+		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('Dual CIDR not allowed',mech)
+		  cidrlength = 32
+
+		# validate domain-spec
 		if m in ('a', 'mx', 'ptr', 'exists', 'include'):
 		  arg = self.expand(arg)
-		  # FQDN must contain at least one '.'
-		  pos = arg.rfind('.')
-		  if not (0 < pos < len(arg) - 1):
-		    raise PermError('Invalid domain found (use FQDN)',
-			  arg)
-		  #Test for all numeric TLD as recommended by RFC 3696
-		  #Note this TLD test may pass non-existant TLDs.  3696
-		  #recommends using DNS lookups to test beyond this
-		  #initial test.
-                  if arg[pos+1:].isdigit(): 	 
-                    raise PermError('Top Level Domain may not be all numbers',
-			  arg) 	 
+		  # any trailing dot was removed by expand()
+		  if RE_TOPLAB.split(arg)[-1]:
+		    raise PermError('Invalid domain found (use FQDN)', 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
-		if m == 'ip4' and not RE_IP4.match(arg):
-		  raise PermError('Invalid IP4 address',mech)
-		#validate 'all' mechanism per RFC 4408 ABNF
-		if m == 'all' and \
-		    (arg != self.d  or mech.count(':') or mech.count('/')):
-# 		  print '|'+ arg + '|', mech, self.d,
+
+		# 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)
@@ -677,7 +805,7 @@ class query(object):
 		  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)
+		    '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
@@ -694,12 +822,12 @@ class query(object):
 
 		# split string by whitespace, drop the 'v=spf1'
 		spf = spf.split()
-		# Catch case where SPF record has no spaces
+		# Catch case where SPF record has no spaces.
 		# Can never happen with conforming dns_spf(), however
 		# in the future we might want to give permerror
 		# for common mistakes like IN TXT "v=spf1" "mx" "-all"
 		# in relaxed mode.
-		if spf[0] != 'v=spf1':   
+		if spf[0] != 'v=spf1':
 		    raise PermError('Invalid SPF record in', self.d)
 		spf = spf[1:]
 
@@ -755,10 +883,15 @@ class query(object):
 			    break
 
 		    elif m == 'exists':
-		        self.check_lookups()
-			if len(self.dns_a(arg)) > 0:
+                        self.check_lookups()
+                        try:
+                            if len(self.dns_a(arg)) > 0:
 				break
-
+		        except AmbiguityWarning:
+                            # Exists wants no response sometimes so don't raise
+                            # the warning.
+                            pass
+                        
 		    elif m == 'a':
 		        self.check_lookups()
 			if cidrmatch(self.i, self.dns_a(arg), cidrlength):
@@ -769,7 +902,10 @@ class query(object):
 			if cidrmatch(self.i, self.dns_mx(arg), cidrlength):
 			      break
 
-		    elif m == 'ip4' and arg != self.d:
+		    elif m == 'ip4':
+		        
+		        if arg == self.d:
+			  raise PermError('Missing IP4 arg',mech)
 			try:
 			    if cidrmatch(self.i, [arg], cidrlength):
 				break
@@ -777,6 +913,8 @@ class query(object):
 			    raise PermError('syntax error',mech)
 			    
 		    elif m == 'ip6':
+		        if arg == self.d:
+			  raise PermError('Missing IP6 arg',mech)
 			# Until we support IPV6, we should never
 			# get an IPv6 connection.  So this mech
 			# will never match.
@@ -792,8 +930,13 @@ class query(object):
 		else:
 		    # no matches
 		    if redirect:
-			return self.check1(self.dns_spf(redirect),
-					       redirect, recursion + 1)
+                        #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)
+			return self.check1(redirect_record, redirect,
+                                           recursion + 1)
 		    else:
 			result = default
 
@@ -882,9 +1025,6 @@ class query(object):
 		>>> 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.expand('%{p2}.trusted-domains.example.net.')
 		'example.org.trusted-domains.example.net'
 
@@ -930,12 +1070,19 @@ class query(object):
 		if len(a) == 1 and self.strict < 2:
 		    return a[0]   			
 		# check official SPF type first when it becomes more popular
-		b = [t for t in self.dns_99(domain) if isSPF(t)]
+		try:
+		  b = [t for t in self.dns_99(domain) if isSPF(t)]
+		except TempError,x:
+		  # some braindead DNS servers hang on type 99 query
+		  if self.strict > 1: raise x
+		  b = []
+
+		if len(b) > 1:
+                    raise PermError('Two or more type SPF spf records found.')
 		if len(b) == 1:
-		    # FIXME: really must fully parse each record
-		    # and compare with appropriate parts case insensitive.
-		    if self.strict >= 2 and len(a) == 1 and a[0] != b[0]:
-		        raise PermError(
+		    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:
@@ -956,14 +1103,7 @@ class query(object):
 	def dns_99(self, domainname):
 		"Get a list of type SPF=99 records for a domain name."
 		if domainname:
-		  try:
-		    return [''.join(a) for a in self.dns(domainname, 'SPF')]
-		  except TempError,x:
-		    if self.strict: raise x
-		    self.note_error(
-		      'DNS responds, but times out on type99 (SPF) query: %s'%
-		      domainname
-		    )
+		  return [''.join(a) for a in self.dns(domainname, 'SPF')]
 		return []
 
 	def dns_mx(self, domainname):
@@ -972,18 +1112,29 @@ class query(object):
 		"""
 # 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 len(mxnames) > MAX_MX:
+		  self.note_error('More than %d MX records returned'%MAX_MX)
 		if self.strict:
 		  max = MAX_MX
+		  if self.strict > 1 and len(mxnames) == 0:
+		    raise AmbiguityWarning(
+		      'No MX records found for mx mechanism', domainname)
 		else:
 		  max = MAX_MX * 4
-		return [a for mx in self.dns(domainname, 'MX')[:max] \
-		          for a in self.dns_a(mx[1])]
+		return [a for mx in mxnames[:max] for a in self.dns_a(mx[1])]
 
 	def dns_a(self, domainname):
 		"""Get a list of IP addresses for a domainname."""
-		if domainname:
-		  return self.dns(domainname, 'A')
-		return []
+		if not domainname: return []
+		if self.strict > 1:
+                    alist = self.dns(domainname, 'A')
+                    if len(alist) == 0:
+                        raise AmbiguityWarning('No A records found for',
+                                               domainname)
+                    else:
+                        return alist
+		return self.dns(domainname, 'A')
 
 	def dns_aaaa(self, domainname):
 		"""Get a list of IPv6 addresses for a domainname."""
@@ -996,6 +1147,20 @@ class query(object):
 # 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(i)
+                          ptrip = [p for p in ptrnames if i in self.dns_a(p)]
+                          if len(ptrnames) > max:
+                              warning = 'More than ' + str(max) + ' PTR records returned'
+                              raise AmbiguityWarning(warning, i)
+                          else:
+                              if len(ptrnames) == 0:
+                                  raise AmbiguityWarning('No PTR records found for ptr mechanism', ptrnames)
+                              return ptrip
+                      except:
+                          raise AmbiguityWarning('No PTR records found for ptr mechanism', ptrnames)
 		else:
 		  max = MAX_PTR * 4
 		return [p for p in self.dns_ptr(i)[:max] if i in self.dns_a(p)]
@@ -1030,6 +1195,7 @@ class query(object):
 			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
@@ -1041,22 +1207,21 @@ class query(object):
 	def get_header(self,res,receiver=None):
 	  if not receiver:
 	    receiver = self.r
-	  if res in ('unknown','permerror'):
-	    txt = ' '.join([res] + self.mech)
-	  else:
-	    txt = res
-	  return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
-	  	txt,receiver,self.get_header_comment(res),self.i,
+	  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.i,
 	        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':
-		  if self.i.startswith('127.'):
-		    return "localhost is always allowed."
-		  else: return \
+		  return \
 		    "domain of %s designates %s as permitted sender" \
 			% (sender,self.i)
 		elif res == 'softfail': return \
@@ -1069,10 +1234,10 @@ class query(object):
 		    "%s is neither permitted nor denied by domain of %s" \
 		    	% (self.i,sender)
 		    #"%s does not designate permitted sender hosts" % sender
-		elif res in ('unknown','permerror'): return \
+		elif res == 'permerror': return \
 		    "permanent error in processing domain of %s: %s" \
 		    	% (sender, self.prob)
-		elif res in ('error','temperror'): return \
+		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" \
@@ -1104,45 +1269,50 @@ def split_email(s, h):
 
 def parse_mechanism(str, d):
 	"""Breaks A, MX, IP4, and PTR mechanisms into a (name, domain,
-	cidr) tuple.  The domain portion defaults to d if not present,
+	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', 32)
+	('a', 'foo.com', None, None)
 
 	>>> parse_mechanism('a:bar.com', 'foo.com')
-	('a', 'bar.com', 32)
+	('a', 'bar.com', None, None)
 
 	>>> parse_mechanism('a/24', 'foo.com')
-	('a', 'foo.com', 24)
+	('a', 'foo.com', 24, None)
 
 	>>> parse_mechanism('A:foo:bar.com/16', 'foo.com')
-	('a', 'foo:bar.com', 16)
+	('a', 'foo:bar.com', 16, None)
 
 	>>> parse_mechanism('-exists:%{i}.%{s1}.100/86400.rate.%{d}','foo.com')
-	('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', 32)
+	('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', None, None)
 
 	>>> parse_mechanism('mx:%%%_/.Claranet.de/27','foo.com')
-	('mx', '%%%_/.Claranet.de', 27)
+	('mx', '%%%_/.Claranet.de', 27, None)
 
 	>>> parse_mechanism('mx:%{d}/27','foo.com')
-	('mx', '%{d}', 27)
+	('mx', '%{d}', 27, None)
 
 	>>> parse_mechanism('iP4:192.0.0.0/8','foo.com')
-	('ip4', '192.0.0.0', 8)
+	('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:
-		a, port = a[0], int(a[1])
+	    str, cidr = a[0], int(a[1])
 	else:
-		a, port = str, 32
+	    cidr = None
 
-	b = a.split(':',1)
-	if len(b) == 2:
-		return b[0].lower(), b[1], port
-	else:
-		return a.lower(), d, port
+	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.
@@ -1195,15 +1365,17 @@ def cidrmatch(i, ipaddrs, cidr_length = 32):
 	>>> cidrmatch('192.168.0.43', ['192.168.0.44', '192.168.0.45'], 24)
 	1
 	"""
-	c = cidr(i, cidr_length)
-	for ip in ipaddrs:
+	try:
+	    c = cidr(i, cidr_length)
+	    for ip in ipaddrs:
 		if cidr(ip, cidr_length) == c:
-			return True
+		    return True
+	except socket.error: pass
 	return False
 
 def cidr(i, n):
 	"""Convert an IP address string with a CIDR mask into a 32-bit
-	integer.
+	or 128-bit integer.
 
 	i must be a string of numbers 0..255 separated by dots '.'::
 	pre: forall([0 <= int(p) < 256 for p in i.split('.')])
@@ -1249,7 +1421,12 @@ def addr2bin(str):
 	>>> 10 * (2 ** 24) + 93 * (2 ** 16) + 512
 	173867520
 	"""
-	return struct.unpack("!L", socket.inet_aton(str))[0]
+	try:
+	  return struct.unpack("!L", socket.inet_aton(str))[0]
+	except socket.error:
+	  if not socket.has_ipv6: raise
+	h,l = struct.unpack("!QQ", socket.inet_pton(socket.AF_INET6,str))
+	return h << 64 | l;
 
 def bin2addr(addr):
 	"""Convert a numeric IPv4 address into string n.n.n.n form.
@@ -1267,7 +1444,7 @@ def bin2addr(addr):
 	return socket.inet_ntoa(struct.pack("!L", addr))
 
 def expand_one(expansion, str, joiner):
-	if not str:
+    	if not str:
 		return expansion
 	ln, reverse, delimiters = RE_ARGS.split(str)[1:4]
 	if not delimiters:
@@ -1306,6 +1483,57 @@ def split(str, delimiters, joiner=None):
 	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)
@@ -1329,6 +1557,7 @@ if __name__ == '__main__':
 		q = query(i=i, s=s, h=h, receiver=socket.gethostname(),
 			strict=False)
 		print q.check(sys.argv[1])
-		if q.perm_error: print q.perm_error.ext
+		if q.perm_error and q.perm_error.ext:
+			print q.perm_error.ext
 	else:
 		print USAGE