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 "..."
-#
-# 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
-#
-
-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 and later , this CID entry is not valid."
- )
- self.has_servers = True
- elif tag == 'noMailServers':
- if self.has_servers:
- raise ValueError(
- "Declared and later , 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 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 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(''):
- 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 "..." """ % 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 0.8.7-1
+- Prevent PTR cache poisoning
+- More lame bounce heuristics
+- SPF moved to pyspf RPM
* Tue May 23 2006 Stuart Gathman 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
-Portions Copyright (c) 2005,2006 Scott Kitterman
-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 .
-#
-# $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:, 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:, 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
-# 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:])