diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000000000000000000000000000000000000..8e7bf3b6bf68703a7b0431ab8d0ae5ad56f98f07 --- /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 f55d63daae185dba060aa067eea50114b6292cb7..5069886fbfdb6bf1d0c39606402485a71a87d26e 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 2140aa529b13b4e62bbf3f488df4cfb33bb2a81e..0000000000000000000000000000000000000000 --- a/cid2spf.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/python2.3 - -# Convert a MS Caller-ID entry (XML) to a SPF entry -# -# (c) 2004 by Ernesto Baschny -# (c) 2004 Python version by Stuart Gathman -# -# Date: 2004-02-25 -# Version: 1.0 -# -# Usage: -# ./cid2spf.pl "<ep xmlns='http://ms.net/1'>...</ep>" -# -# Note that the 'include' directives will also have to be checked and -# "translated". Future versions of this script might be able to get a -# domain name as an argument and "crawl" the DNS for the necessary -# information. -# -# A complete reverse translation (SPF -> CID) might be impossible, since -# there are no way to handle: -# - PTR and EXISTS mechanism -# - MX mechanism with an different domain as argument -# - macros -# -# References: -# http://www.microsoft.com/mscorp/twc/privacy/spam_callerid.mspx -# http://spf.pobox.com/ -# -# Known bugs: -# - Currently it won't handle the exclusions provided in the A and R -# tags (prefix '!'). They will show up "as-is" in the SPF record -# - I really haven't read the MS-CID specs in-depth, so there are probably -# other bugs too :) -# -# Ernesto Baschny <ernst@baschny.de> -# - -import xml.sax -import spf - -# ------------------------------------------------------------------------- -class CIDParser(xml.sax.ContentHandler): - "Convert a MS Caller-ID entry (XML) to a SPF entry" - - def __init__(self,q=None): - self.spf = [] - self.action = '-all' - self.has_servers = None - self.spf_entry = None - if q: - self.spf_query = q - else: - self.spf_query = spf.query(i='127.0.0.1', s='localhost', h='unknown') - - def startElement(self,tag,attr): - if tag == 'm': - if self.has_servers != None and not self.has_servers: - raise ValueError( - "Declared <noMailServers\> and later <m>, this CID entry is not valid." - ) - self.has_servers = True - elif tag == 'noMailServers': - if self.has_servers: - raise ValueError( - "Declared <m> and later <noMailServers\>, this CID entry is not valid." - ) - self.has_servers = False - elif tag == 'ep': - if attr.has_key('testing') and attr.getValue('testing') == 'true': - # A CID with 'testing' found: - # From the MS-specs: - # "Documents in which such attribute is present with a true - # value SHOULD be entirely ignored (one should act as if the - # document were absent)" - # From the SPF-specs: - # "Neutral (?): The SPF client MUST proceed as if a domain did - # not publish SPF data." - # So we set SPF action to "neutral": - self.action = '?all' - elif tag == 'mx': - # The empty MX-tag, same as SPF's MX-mechanism - self.spf.append('mx') - self.tag = tag - - def characters(self,text): - tag = self.tag - # Remove starting and trailing spaces from text: - text = text.strip() - - if tag == 'a' or tag == 'r': - # The A and R tags from MS-CID are both handled by the - # ipv4/6-mechanisms from SPF: - if text.find(':') < 0: - mechanism = 'ip4' - else: - mechanism = 'ip6' - self.spf.append(mechanism + ':' + text) - elif tag == 'indirect': - # MS-CID's indirect is "sort of" the include from SPF: - # Not really true, because the <indirect> tag from MS-CID also - # provides a fallback in case the included domain doesn't provide - # _ep-records: The inbound MX-servers of the included domains - # are added to the list of allowed outgoing mailservers for the - # domain that declared the _ep-record with the <indirect> tag. - # In SPF you would use the 'mx:domain' to handle this, but this - # wouldn't depend on referred domain having or not SPF-records. - cid_xml = self.cid_txt(text) - if cid_xml: - p = CIDParser() - xml.sax.parseString(cid_xml,p) - if p.has_servers != False: - self.spf += p.spf - else: - self.spf.append('mx:' + text) - - def cid_txt(self,domain): - q = self.spf_query - domain='_ep.' + domain - a = q.dns_txt(domain) - if not a: return None - if a[0].lower().startswith('<ep ') and a[-1].lower().endswith('</ep>'): - return ''.join(a) - return None - - def endElement(self,tag): - if tag == 'ep': - # This is the end... assemble what we've got - spf_entry = ['v=spf1'] - if self.has_servers != False: - spf_entry += self.spf - spf_entry.append(self.action) - self.spf_entry = ' '.join(spf_entry) - - def spf_txt(self,cid_xml): - if not cid_xml.startswith('<'): - cid_xml = self.cid_txt(cid_xml) - if not cid_xml: return None - # Parse the beast. Any XML-problem will be reported by xlm.sax - self.spf_entry = None - xml.sax.parseString(cid_xml,self) - return self.spf_entry - -if __name__ == '__main__': - import sys - if len(sys.argv) < 2: - print >>sys.stderr, \ - """Usage: %s "<ep xmlns='http://ms.net/1'>...</ep>" """ % sys.argv[0] - sys.exit(1) - - cid_xml = sys.argv[1] - - p = CIDParser() - print p.spf_txt(cid_xml) diff --git a/milter.spec b/milter.spec index c2b921762e5f6013b71fddf9a8b23fcc1f7a151b..a41f9b7601be6e52f8607a47c167d33b82f2d331 100644 --- a/milter.spec +++ b/milter.spec @@ -1,23 +1,20 @@ %define name milter -%define version 0.8.6 -%define release 2.RH7 +%define version 0.8.7 +%define release 1 # what version of RH are we building for? -%define redhat9 0 -%define redhat7 1 -%define redhat6 0 +%define redhat7 0 # Options for Redhat version 6.x: -# rpm -ba|--rebuild --define "rh6 1" -%{?rh6:%define redhat7 0} -%{?rh6:%define redhat6 1} +# rpm -ba|--rebuild --define "rh7 1" +%{?rh7:%define redhat7 1} # some systems dont have initrddir defined %{?_initrddir:%define _initrddir /etc/rc.d/init.d} -%if %{redhat9} -%define sysvinit milter.rc -%else # Redhat 7.x and earlier (multiple ps lines per thread) +%if %{redhat7} # Redhat 7.x and earlier (multiple ps lines per thread) %define sysvinit milter.rc7 +%else +%define sysvinit milter.rc %endif # RH9, other systems (single ps line per process) %ifos Linux @@ -43,22 +40,23 @@ Requires: %{python} >= 2.4, sendmail >= 8.13 %ifos Linux Requires: chkconfig %endif -BuildRequires: %{python}-devel , sendmail-devel >= 8.13 +BuildRequires: %{python}-devel >= 2.4, sendmail-devel >= 8.13 %description This is a python extension module to enable python scripts to attach to sendmail's libmilter functionality. Additional python -modules provide for navigating and modifying MIME parts. +modules provide for navigating and modifying MIME parts, sending +DSNs, and doing CBV. %prep %setup #patch -p0 -b .bms %build -%if %{redhat9} - LDFLAGS="-g" -%else +%if %{redhat7} LDFLAGS="-s" +%else # Redhat builds debug packages after 7.3 + LDFLAGS="-g" %endif env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build @@ -176,6 +174,10 @@ rm -rf $RPM_BUILD_ROOT /usr/share/sendmail-cf/hack/rhsbl.m4 %changelog +* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1 +- Prevent PTR cache poisoning +- More lame bounce heuristics +- SPF moved to pyspf RPM * Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2 - Support CBV timeout - Support fail template, headers in templates diff --git a/quarantine.txt b/quarantine.txt index b060d75cd5b3d7aeb9027f26f32b2528e96d7fd7..cdf3f7354c2b5d83e76123ba7c987fa2d2619984 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 2c5716bf59ba3c970ba10586dbcf9e6cc992b32d..ffd12170251e0385eb9da078a9e9b89d0385c53e 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 18b96431456ae84935dca6f696be7afd9c90a2f9..62c9834fc3441584bbf3ce2ed2cd4bce6a36cd76 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 3fb15db402573accf1e6cbb5126eb12194cbdaec..0000000000000000000000000000000000000000 --- a/spf.py +++ /dev/null @@ -1,1514 +0,0 @@ -#!/usr/bin/env python -"""SPF (Sender Policy Framework) implementation. - -Copyright (c) 2003, Terence Way -Portions Copyright (c) 2004,2005,2006 Stuart Gathman <stuart@bmsi.com> -Portions Copyright (c) 2005,2006 Scott Kitterman <scott@kitterman.com> -This module is free software, and you may redistribute it and/or modify -it under the same terms as Python itself, so long as this copyright message -and disclaimer are retained in their original form. - -IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, -SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF -THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. - -THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, -AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, -SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -For more information about SPF, a tool against email forgery, see - http://www.openspf.org/ - -For news, bugfixes, etc. visit the home page for this implementation at - http://www.wayforward.net/spf/ - http://sourceforge.net/projects/pymilter/ -""" - -# Changes: -# 9-dec-2003, v1.1, Meng Weng Wong added PTR code, THANK YOU -# 11-dec-2003, v1.2, ttw added macro expansion, exp=, and redirect= -# 13-dec-2003, v1.3, ttw added %{o} original domain macro, -# print spf result on command line, support default=, -# support localhost, follow DNS CNAMEs, cache DNS results -# during query, support Python 2.2 for Mac OS X -# 16-dec-2003, v1.4, ttw fixed include handling (include is a mechanism, -# complete with status results, so -include: should work. -# Expand macros AFTER looking for status characters ?-+ -# so altavista.com SPF records work. -# 17-dec-2003, v1.5, ttw use socket.inet_aton() instead of DNS.addr2bin, so -# n, n.n, and n.n.n forms for IPv4 addresses work, and to -# ditch the annoying Python 2.4 FutureWarning -# 18-dec-2003, v1.6, Failures on Intel hardware: endianness. Use ! on -# struct.pack(), struct.unpack(). -# -# Development taken over by Stuart Gathman <stuart@bmsi.com>. -# -# $Log$ -# Revision 1.107 2006/11/04 21:58:12 customdesigned -# Prevent cache poisoning by bogus additional RRs in PTR DNS response. -# -# See spf_changelog.txt for earlier changes. - -__author__ = "Terence Way" -__email__ = "terry@wayforward.net" -__version__ = "1.7: July 22, 2005" -MODULE = 'spf' - -USAGE = """To check an incoming mail request: - % python spf.py {ip} {sender} {helo} - % python spf.py 69.55.226.139 tway@optsw.com mx1.wayforward.net - -To test an SPF record: - % python spf.py "v=spf1..." {ip} {sender} {helo} - % python spf.py "v=spf1 +mx +ip4:10.0.0.1 -all" 10.0.0.1 tway@foo.com a - -To fetch an SPF record: - % python spf.py {domain} - % python spf.py wayforward.net - -To test this script (and to output this usage message): - % python spf.py -""" - -import re -import socket # for inet_ntoa() and inet_aton() -import struct # for pack() and unpack() -import time # for time() -import urllib # for quote() - -import DNS # http://pydns.sourceforge.net -if not hasattr(DNS.Type, 'SPF'): - # patch in type99 support - DNS.Type.SPF = 99 - DNS.Type.typemap[99] = 'SPF' - DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata - -def DNSLookup(name, qtype, strict=True): - try: - req = DNS.DnsRequest(name, qtype=qtype) - resp = req.req() - #resp.show() - # key k: ('wayforward.net', 'A'), value v - # FIXME: pydns returns AAAA RR as 16 byte binary string, but - # A RR as dotted quad. For consistency, this driver should - # return both as binary string. - return [((a['name'], a['typename']), a['data']) for a in resp.answers] - except IOError, x: - raise TempError, 'DNS ' + str(x) - except DNS.DNSError, x: - raise TempError, 'DNS ' + str(x) - -RE_SPF = re.compile(r'^v=spf1$|^v=spf1 ',re.IGNORECASE) - -# Regular expression to look for modifiers -RE_MODIFIER = re.compile(r'^([a-z][a-z0-9_\-\.]*)=', re.IGNORECASE) - -# Regular expression to find macro expansions -PAT_CHAR = r'%(%|_|-|(\{[^\}]*\}))' -RE_CHAR = re.compile(PAT_CHAR) - -# Regular expression to break up a macro expansion -RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)') - -RE_DUAL_CIDR = re.compile(r'//(0|[1-9]\d*)$') -RE_CIDR = re.compile(r'/(0|[1-9]\d*)$') - -PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4) -RE_IP4 = re.compile(PAT_IP4+'$') - -RE_TOPLAB = re.compile( - r'\.(?:[0-9a-z]*[a-z][0-9a-z]*|[0-9a-z]+-[0-9a-z-]*[0-9a-z])\.?$|%s' - % PAT_CHAR, re.IGNORECASE) - -RE_IP6 = re.compile( '(?:%(hex4)s:){6}%(ls32)s$' - '|::(?:%(hex4)s:){5}%(ls32)s$' - '|(?:%(hex4)s)?::(?:%(hex4)s:){4}%(ls32)s$' - '|(?:(?:%(hex4)s:){0,1}%(hex4)s)?::(?:%(hex4)s:){3}%(ls32)s$' - '|(?:(?:%(hex4)s:){0,2}%(hex4)s)?::(?:%(hex4)s:){2}%(ls32)s$' - '|(?:(?:%(hex4)s:){0,3}%(hex4)s)?::%(hex4)s:%(ls32)s$' - '|(?:(?:%(hex4)s:){0,4}%(hex4)s)?::%(ls32)s$' - '|(?:(?:%(hex4)s:){0,5}%(hex4)s)?::%(hex4)s$' - '|(?:(?:%(hex4)s:){0,6}%(hex4)s)?::$' - % { - 'ls32': r'(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|%s)'%PAT_IP4, - 'hex4': r'[0-9a-f]{1,4}' - }, re.IGNORECASE) - -# Local parts and senders have their delimiters replaced with '.' during -# macro expansion -# -JOINERS = {'l': '.', 's': '.'} - -RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail', - 'pass': 'pass', 'fail': 'fail', 'permerror': 'permerror', - 'error': 'error', 'neutral': 'neutral', 'softfail': 'softfail', - 'none': 'none', 'local': 'local', 'trusted': 'trusted', - 'ambiguous': 'ambiguous'} - -EXPLANATIONS = {'pass': 'sender SPF authorized', - 'fail': 'SPF fail - not authorized', - 'permerror': 'permanent error in processing', - 'temperror': 'temporary DNS error in processing', - 'softfail': 'domain owner discourages use of this host', - 'neutral': 'access neither permitted nor denied', - 'none': '', - #Note: The following are not formally SPF results - 'local': 'No SPF result due to local policy', - 'trusted': 'No SPF check - trusted-forwarder.org', - #Ambiguous only used in harsh mode for SPF validation - 'ambiguous': 'No error, but results may vary' - } - -# support pre 2.2.1.... -try: - bool, True, False = bool, True, False -except NameError: - False, True = 0, 1 - def bool(x): return not not x -# ...pre 2.2.1 - -DELEGATE = None - -# standard default SPF record for best_guess -DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr' - -#Whitelisted forwarders here. Additional locally trusted forwarders can be -#added to this record. -TRUSTED_FORWARDERS = 'v=spf1 ?include:spf.trusted-forwarder.org -all' - -# maximum DNS lookups allowed -MAX_LOOKUP = 10 #RFC 4408 Para 10.1 -MAX_MX = 10 #RFC 4408 Para 10.1 -MAX_PTR = 10 #RFC 4408 Para 10.1 -MAX_CNAME = 10 # analogous interpretation to MAX_PTR -MAX_RECURSION = 20 - -ALL_MECHANISMS = ('a', 'mx', 'ptr', 'exists', 'include', 'ip4', 'ip6', 'all') -COMMON_MISTAKES = { - 'prt': 'ptr', 'ip': 'ip4', 'ipv4': 'ip4', 'ipv6': 'ip6', 'all.': 'all' -} - -#If harsh processing, for the validator, is invoked, warn if results -#likely deviate from the publishers intention. -class AmbiguityWarning(Exception): - "SPF Warning - ambiguous results" - def __init__(self, msg, mech=None, ext=None): - Exception.__init__(self, msg, mech) - self.msg = msg - self.mech = mech - self.ext = ext - def __str__(self): - if self.mech: - return '%s: %s' %(self.msg, self.mech) - return self.msg - -class TempError(Exception): - "Temporary SPF error" - def __init__(self, msg, mech=None, ext=None): - Exception.__init__(self, msg, mech) - self.msg = msg - self.mech = mech - self.ext = ext - def __str__(self): - if self.mech: - return '%s: %s '%(self.msg, self.mech) - return self.msg - -class PermError(Exception): - "Permanent SPF error" - def __init__(self, msg, mech=None, ext=None): - Exception.__init__(self, msg, mech) - self.msg = msg - self.mech = mech - self.ext = ext - def __str__(self): - if self.mech: - return '%s: %s'%(self.msg, self.mech) - return self.msg - -def check2(i, s, h, local=None, receiver=None): - """Test an incoming MAIL FROM:<s>, from a client with ip address i. - h is the HELO/EHLO domain name. This is the RFC4408 compliant pySPF2.0 - interface. The interface returns an SPF result and explanation only. - SMTP response codes are not returned since RFC 4408 does not specify - receiver policy. Applications updated for RFC 4408 should use this - interface. - - Returns (result, explanation) where result in - ['pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', 'neutral' ]. - - Example: - #>>> check2(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com') - - """ - res,_,exp = query(i=i, s=s, h=h, local=local, receiver=receiver).check() - return res,exp - -def check(i, s, h, local=None, receiver=None): - """Test an incoming MAIL FROM:<s>, from a client with ip address i. - h is the HELO/EHLO domain name. This is the pre-RFC SPF Classic interface. - Applications written for pySPF 1.6/1.7 can use this interface to allow - pySPF2 to be a drop in replacement for older versions. With the exception - of result codes, performance in RFC 4408 compliant. - - Returns (result, code, explanation) where result in - ['pass', 'unknown', 'fail', 'error', 'softfail', 'none', 'neutral' ]. - - Example: - #>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com') - - """ - res,code,exp = query(i=i, s=s, h=h, local=local, receiver=receiver).check() - if res == 'permerror': - res = 'unknown' - elif res == 'tempfail': - res =='error' - return res, code, exp - -class query(object): - """A query object keeps the relevant information about a single SPF - query: - - i: ip address of SMTP client in dotted notation - s: sender declared in MAIL FROM:<> - l: local part of sender s - d: current domain, initially domain part of sender s - h: EHLO/HELO domain - v: 'in-addr' for IPv4 clients and 'ip6' for IPv6 clients - t: current timestamp - p: SMTP client domain name - o: domain part of sender s - r: receiver - c: pretty ip address (different from i for IPv6) - - This is also, by design, the same variables used in SPF macro - expansion. - - Also keeps cache: DNS cache. - """ - def __init__(self, i, s, h, local=None, receiver=None, strict=True): - self.s, self.h = s, h - if not s and h: - self.s = 'postmaster@' + h - self.l, self.o = split_email(s, h) - self.t = str(int(time.time())) - self.d = self.o - self.p = None # lazy evaluation - if receiver: - self.r = receiver - else: - self.r = 'unknown' - # Since the cache does not track Time To Live, it is created - # fresh for each query. It is important for efficiently using - # multiple results provided in DNS answers. - self.cache = {} - self.defexps = dict(EXPLANATIONS) - self.exps = dict(EXPLANATIONS) - self.libspf_local = local # local policy - self.lookups = 0 - # strict can be False, True, or 2 (numeric) for harsh - self.strict = strict - if i: - self.set_ip(i) - - def set_ip(self, i): - "Set connect ip, and ip6 or ip4 mode." - if RE_IP4.match(i): - self.ip = addr2bin(i) - ip6 = False - else: - self.ip = bin2long6(inet_pton(i)) - if (self.ip >> 32) == 0xFFFF: # IP4 mapped address - self.ip = self.ip & 0xFFFFFFFFL - ip6 = False - else: - ip6 = True - # NOTE: self.A is not lowercase, so isn't a macro. See query.expand() - if ip6: - self.c = inet_ntop( - struct.pack("!QQ", self.ip>>64, self.ip&0xFFFFFFFFFFFFFFFFL)) - self.i = '.'.join(list('%032X'%self.ip)) - self.A = 'AAAA' - self.v = 'ip6' - self.cidrmax = 128 - else: - self.c = socket.inet_ntoa(struct.pack("!L", self.ip)) - self.i = self.c - self.A = 'A' - self.v = 'in-addr' - self.cidrmax = 32 - - def set_default_explanation(self, exp): - exps = self.exps - defexps = self.defexps - for i in 'softfail', 'fail', 'permerror': - exps[i] = exp - defexps[i] = exp - - def set_explanation(self, exp): - exps = self.exps - for i in 'softfail', 'fail', 'permerror': - exps[i] = exp - - # Compute p macro only if needed - def getp(self): - if not self.p: - p = self.validated_ptrs() - if not p: - self.p = "unknown" - elif self.d in p: - self.p = self.d - else: - sfx = '.' + self.d - for d in p: - if d.endswith(sfx): - self.p = d - break - else: - self.p = p[0] - return self.p - - def best_guess(self, spf=DEFAULT_SPF): - """Return a best guess based on a default SPF record""" - return self.check(spf) - - - def check(self, spf=None): - """ - Returns (result, mta-status-code, explanation) where result - in ['fail', 'softfail', 'neutral' 'permerror', 'pass', 'temperror', 'none'] - - Examples: - >>> q = query(s='strong-bad@email.example.com', - ... h='mx.example.org', i='192.0.2.3') - >>> q.check(spf='v=spf1 ?all') - ('neutral', 250, 'access neither permitted nor denied') - - >>> q.check(spf='v=spf1 redirect=controlledmail.com exp=_exp.controlledmail.com') - ('fail', 550, 'SPF fail - not authorized') - - >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ?all moo') - ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo') - - >>> q.check(spf='v=spf1 =a ?all moo') - ('permerror', 550, 'SPF Permanent Error: Unknown qualifier, RFC 4408 para 4.6.1, found in: =a') - - >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ~all') - ('pass', 250, 'sender SPF authorized') - - >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo=') - ('pass', 250, 'sender SPF authorized') - - >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all match.sub-domains_9=yes') - ('pass', 250, 'sender SPF authorized') - - >>> q.strict = False - >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo') - ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo') - >>> q.perm_error.ext - ('pass', 250, 'sender SPF authorized') - - >>> q.strict = True - >>> q.check(spf='v=spf1 ip4:192.1.0.0/16 moo -all') - ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo') - - >>> q.check(spf='v=spf1 ip4:192.1.0.0/16 ~all') - ('softfail', 250, 'domain owner discourages use of this host') - - >>> q.check(spf='v=spf1 -ip4:192.1.0.0/6 ~all') - ('fail', 550, 'SPF fail - not authorized') - - # Assumes DNS available - >>> q.check() - ('none', 250, '') - - >>> q.check(spf='v=spf1 ip4:1.2.3.4 -a:example.net -all') - ('fail', 550, 'SPF fail - not authorized') - >>> q.libspf_local='ip4:192.0.2.3 a:example.org' - >>> q.check(spf='v=spf1 ip4:1.2.3.4 -a:example.net -all') - ('pass', 250, 'sender SPF authorized') - - >>> q.check(spf='v=spf1 ip4:1.2.3.4 -all exp=_exp.controlledmail.com') - ('fail', 550, 'Controlledmail.com does not send mail from itself.') - - >>> q.check(spf='v=spf1 ip4:1.2.3.4 ?all exp=_exp.controlledmail.com') - ('neutral', 250, 'access neither permitted nor denied') - """ - self.mech = [] # unknown mechanisms - # If not strict, certain PermErrors (mispelled - # mechanisms, strict processing limits exceeded) - # will continue processing. However, the exception - # that strict processing would raise is saved here - self.perm_error = None - - try: - self.lookups = 0 - if not spf: - spf = self.dns_spf(self.d) - if self.libspf_local and spf: - spf = insert_libspf_local_policy( - spf, self.libspf_local) - rc = self.check1(spf, self.d, 0) - if self.perm_error: - # lax processing encountered a permerror, but continued - self.perm_error.ext = rc - raise self.perm_error - return rc - - except TempError, x: - self.prob = x.msg - if x.mech: - self.mech.append(x.mech) - return ('temperror', 451, 'SPF Temporary Error: ' + str(x)) - except PermError, x: - if not self.perm_error: - self.perm_error = x - self.prob = x.msg - if x.mech: - self.mech.append(x.mech) - # Pre-Lentczner draft treats this as an unknown result - # and equivalent to no SPF record. - return ('permerror', 550, 'SPF Permanent Error: ' + str(x)) - - def check1(self, spf, domain, recursion): - # spf rfc: 3.7 Processing Limits - # - if recursion > MAX_RECURSION: - # This should never happen in strict mode - # because of the other limits we check, - # so if it does, there is something wrong with - # our code. It is not a PermError because there is not - # necessarily anything wrong with the SPF record. - if self.strict: - raise AssertionError('Too many levels of recursion') - # As an extended result, however, it should be - # a PermError. - raise PermError('Too many levels of recursion') - try: - try: - tmp, self.d = self.d, domain - return self.check0(spf, recursion) - finally: - self.d = tmp - except AmbiguityWarning,x: - self.prob = x.msg - if x.mech: - self.mech.append(x.mech) - return ('ambiguous', 000, 'SPF Ambiguity Warning: %s' % x) - - def note_error(self, *msg): - if self.strict: - raise PermError(*msg) - # if lax mode, note error and continue - if not self.perm_error: - try: - raise PermError(*msg) - except PermError, x: - # FIXME: keep a list of errors for even friendlier diagnostics. - self.perm_error = x - return self.perm_error - - def validate_mechanism(self, mech): - """Parse and validate a mechanism. - Returns mech,m,arg,cidrlength,result - - Examples: - >>> q = query(s='strong-bad@email.example.com.', - ... h='mx.example.org', i='192.0.2.3') - >>> q.validate_mechanism('A') - ('A', 'a', 'email.example.com', 32, 'pass') - - >>> q = query(s='strong-bad@email.example.com', - ... h='mx.example.org', i='192.0.2.3') - >>> q.validate_mechanism('A') - ('A', 'a', 'email.example.com', 32, 'pass') - - >>> q.validate_mechanism('?mx:%{d}/27') - ('?mx:%{d}/27', 'mx', 'email.example.com', 27, 'neutral') - - >>> try: q.validate_mechanism('ip4:1.2.3.4/247') - ... except PermError,x: print x - Invalid IP4 CIDR length: ip4:1.2.3.4/247 - - >>> try: q.validate_mechanism('ip4:1.2.3.4/33') - ... except PermError,x: print x - Invalid IP4 CIDR length: ip4:1.2.3.4/33 - - >>> try: q.validate_mechanism('a:example.com:8080') - ... except PermError,x: print x - Invalid domain found (use FQDN): example.com:8080 - - >>> try: q.validate_mechanism('ip4:1.2.3.444/24') - ... except PermError,x: print x - Invalid IP4 address: ip4:1.2.3.444/24 - - >>> try: q.validate_mechanism('ip4:1.2.03.4/24') - ... except PermError,x: print x - Invalid IP4 address: ip4:1.2.03.4/24 - - >>> try: q.validate_mechanism('-all:3030') - ... except PermError,x: print x - Invalid all mechanism format - only qualifier allowed with all: -all:3030 - - >>> q.validate_mechanism('-mx:%%%_/.Clara.de/27') - ('-mx:%%%_/.Clara.de/27', 'mx', '% /.Clara.de', 27, 'fail') - - >>> q.validate_mechanism('~exists:%{i}.%{s1}.100/86400.rate.%{d}') - ('~exists:%{i}.%{s1}.100/86400.rate.%{d}', 'exists', '192.0.2.3.com.100/86400.rate.email.example.com', 32, 'softfail') - - >>> q.validate_mechanism('a:mail.example.com.') - ('a:mail.example.com.', 'a', 'mail.example.com', 32, 'pass') - """ - # a mechanism - m, arg, cidrlength, cidr6length = parse_mechanism(mech, self.d) - # map '?' '+' or '-' to 'neutral' 'pass' or 'fail' - if m: - result = RESULTS.get(m[0]) - if result: - # eat '?' '+' or '-' - m = m[1:] - else: - # default pass - result = 'pass' - if m in COMMON_MISTAKES: - self.note_error('Unknown mechanism found', mech) - m = COMMON_MISTAKES[m] - - if m == 'a' and RE_IP4.match(arg): - x = self.note_error( - 'Use the ip4 mechanism for ip4 addresses', mech) - m = 'ip4' - - - # validate cidr and dual-cidr - if m in ('a', 'mx'): - if cidrlength is None: - cidrlength = 32; - elif cidrlength > 32: - raise PermError('Invalid IP4 CIDR length', mech) - if cidr6length is None: - cidr6length = 128 - elif cidr6length > 128: - raise PermError('Invalid IP6 CIDR length', mech) - if self.v == 'ip6': - cidrlength = cidr6length - elif m == 'ip4': - if cidr6length is not None: - raise PermError('Dual CIDR not allowed', mech) - if cidrlength is None: - cidrlength = 32; - elif cidrlength > 32: - raise PermError('Invalid IP4 CIDR length', mech) - if not RE_IP4.match(arg): - raise PermError('Invalid IP4 address', mech) - elif m == 'ip6': - if cidr6length is not None: - raise PermError('Dual CIDR not allowed', mech) - if cidrlength is None: - cidrlength = 128 - elif cidrlength > 128: - raise PermError('Invalid IP6 CIDR length', mech) - if not RE_IP6.match(arg): - raise PermError('Invalid IP6 address', mech) - else: - if cidrlength is not None or cidr6length is not None: - raise PermError('CIDR not allowed', mech) - cidrlength = self.cidrmax - - # validate domain-spec - if m in ('a', 'mx', 'ptr', 'exists', 'include'): - # any trailing dot was removed by expand() - if RE_TOPLAB.split(arg)[-1]: - raise PermError('Invalid domain found (use FQDN)', arg) - arg = self.expand(arg) - if m == 'include': - if arg == self.d: - if mech != 'include': - raise PermError('include has trivial recursion', mech) - raise PermError('include mechanism missing domain', mech) - return mech, m, arg, cidrlength, result - - # validate 'all' mechanism per RFC 4408 ABNF - if m == 'all' and mech.count(':'): - # print '|'+ arg + '|', mech, self.d, - self.note_error( - 'Invalid all mechanism format - only qualifier allowed with all' - , mech) - if m in ALL_MECHANISMS: - return mech, m, arg, cidrlength, result - if m[1:] in ALL_MECHANISMS: - x = self.note_error( - 'Unknown qualifier, RFC 4408 para 4.6.1, found in', mech) - else: - x = self.note_error('Unknown mechanism found', mech) - return mech, m, arg, cidrlength, x - - def check0(self, spf, recursion): - """Test this query information against SPF text. - - Returns (result, mta-status-code, explanation) where - result in ['fail', 'unknown', 'pass', 'none'] - """ - - if not spf: - return ('none', 250, EXPLANATIONS['none']) - - # split string by whitespace, drop the 'v=spf1' - spf = spf.split() - # Catch case where SPF record has no spaces. - # Can never happen with conforming dns_spf(), however - # in the future we might want to give warnings - # for common mistakes like IN TXT "v=spf1" "mx" "-all" - # in relaxed mode. - if spf[0].lower() != 'v=spf1': - assert strict > 1 - raise AmbiguityWarning('Invalid SPF record in', self.d) - spf = spf[1:] - - # copy of explanations to be modified by exp= - exps = self.exps - redirect = None - - # no mechanisms at all cause unknown result, unless - # overridden with 'default=' modifier - # - default = 'neutral' - mechs = [] - - # Look for modifiers - # - for mech in spf: - m = RE_MODIFIER.split(mech)[1:] - if len(m) != 2: - mechs.append(self.validate_mechanism(mech)) - continue - - if m[0] == 'exp': - # always fetch explanation to check permerrors - exp = self.get_explanation(m[1]) - if not recursion: - # only set explanation in base recursion level - self.set_explanation(exp) - elif m[0] == 'redirect': - self.check_lookups() - redirect = self.expand(m[1]) - elif m[0] == 'default': - arg = self.expand(m[1]) - # default=- is the same as default=fail - default = RESULTS.get(arg, default) - else: - # spf rfc: 3.6 Unrecognized Mechanisms and Modifiers - self.expand(m[1]) # syntax error on invalid macro - - - # Evaluate mechanisms - # - for mech, m, arg, cidrlength, result in mechs: - - if m == 'include': - self.check_lookups() - res, code, txt = self.check1(self.dns_spf(arg), - arg, recursion + 1) - if res == 'pass': - break - if res == 'none': - self.note_error( - 'No valid SPF record for included domain: %s' %arg, - mech) - res = 'neutral' - continue - elif m == 'all': - break - - elif m == 'exists': - self.check_lookups() - try: - if len(self.dns_a(arg,'A')) > 0: - break - except AmbiguityWarning: - # Exists wants no response sometimes so don't raise - # the warning. - pass - - elif m == 'a': - self.check_lookups() - if self.cidrmatch(self.dns_a(arg,self.A), cidrlength): - break - - elif m == 'mx': - self.check_lookups() - if self.cidrmatch(self.dns_mx(arg), cidrlength): - break - - elif m == 'ip4': - if self.v == 'in-addr': # match own connection type only - try: - if self.cidrmatch([arg], cidrlength): break - except socket.error: - raise PermError('syntax error', mech) - - elif m == 'ip6': - if self.v == 'ip6': # match own connection type only - try: - arg = inet_pton(arg) - if self.cidrmatch([arg], cidrlength): break - except socket.error: - raise PermError('syntax error', mech) - - elif m == 'ptr': - self.check_lookups() - if domainmatch(self.validated_ptrs(), arg): - break - - else: - # no matches - if redirect: - #Catch redirect to a non-existant SPF record. - redirect_record = self.dns_spf(redirect) - if not redirect_record: - raise PermError('redirect domain has no SPF record', - redirect) - self.exps = dict(self.defexps) - return self.check1(redirect_record, redirect, recursion) - else: - result = default - - if result == 'fail': - return (result, 550, exps[result]) - else: - return (result, 250, exps[result]) - - def check_lookups(self): - self.lookups = self.lookups + 1 - if self.lookups > MAX_LOOKUP*4: - raise PermError('More than %d DNS lookups'%MAX_LOOKUP*4) - if self.lookups > MAX_LOOKUP: - self.note_error('Too many DNS lookups') - - def get_explanation(self, spec): - """Expand an explanation.""" - if spec: - txt = ''.join(self.dns_txt(self.expand(spec))) - return self.expand(txt, stripdot=False) - else: - return 'explanation : Required option is missing' - - def expand(self, str, stripdot=True): # macros='slodipvh' - """Do SPF RFC macro expansion. - - Examples: - >>> q = query(s='strong-bad@email.example.com', - ... h='mx.example.org', i='192.0.2.3') - >>> q.p = 'mx.example.org' - >>> q.r = 'example.net' - - >>> q.expand('%{d}') - 'email.example.com' - - >>> q.expand('%{d4}') - 'email.example.com' - - >>> q.expand('%{d3}') - 'email.example.com' - - >>> q.expand('%{d2}') - 'example.com' - - >>> q.expand('%{d1}') - 'com' - - >>> q.expand('%{p}') - 'mx.example.org' - - >>> q.expand('%{p2}') - 'example.org' - - >>> q.expand('%{dr}') - 'com.example.email' - - >>> q.expand('%{d2r}') - 'example.email' - - >>> q.expand('%{l}') - 'strong-bad' - - >>> q.expand('%{l-}') - 'strong.bad' - - >>> q.expand('%{lr}') - 'strong-bad' - - >>> q.expand('%{lr-}') - 'bad.strong' - - >>> q.expand('%{l1r-}') - 'strong' - - >>> q.expand('%{c}',stripdot=False) - '192.0.2.3' - - >>> q.expand('%{r}',stripdot=False) - 'example.net' - - >>> q.expand('%{ir}.%{v}._spf.%{d2}') - '3.2.0.192.in-addr._spf.example.com' - - >>> q.expand('%{lr-}.lp._spf.%{d2}') - 'bad.strong.lp._spf.example.com' - - >>> q.expand('%{lr-}.lp.%{ir}.%{v}._spf.%{d2}') - 'bad.strong.lp.3.2.0.192.in-addr._spf.example.com' - - >>> q.expand('%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}') - '3.2.0.192.in-addr.strong.lp._spf.example.com' - - >>> try: q.expand('%(ir).%{v}.%{l1r-}.lp._spf.%{d2}') - ... except PermError,x: print x - invalid-macro-char : %(ir) - - >>> q.expand('%{p2}.trusted-domains.example.net') - 'example.org.trusted-domains.example.net' - - >>> q.expand('%{p2}.trusted-domains.example.net.') - 'example.org.trusted-domains.example.net' - - >>> q = query(s='@email.example.com', - ... h='mx.example.org', i='192.0.2.3') - >>> q.p = 'mx.example.org' - >>> q.expand('%{l}') - 'postmaster' - - """ - macro_delimiters = ['{', '%', '-', '_'] - end = 0 - result = '' - macro_count = str.count('%') - if macro_count != 0: - labels = str.split('.') - for label in labels: - is_macro = False - if len(label) > 1: - if label[0] == '%': - for delimit in macro_delimiters: - if label[1] == delimit: - is_macro = True - if not is_macro: - raise PermError ('invalid-macro-char ', label) - break - for i in RE_CHAR.finditer(str): - result += str[end:i.start()] - macro = str[i.start():i.end()] - if macro == '%%': - result += '%' - elif macro == '%_': - result += ' ' - elif macro == '%-': - result += '%20' - else: - letter = macro[2].lower() -# print letter - if letter == 'p': - self.getp() - elif letter in 'crt' and stripdot: - raise PermError( - 'c,r,t macros allowed in exp= text only', macro) - expansion = getattr(self, letter, self) - if expansion: - if expansion == self: - raise PermError('Unknown Macro Encountered', macro) - e = expand_one(expansion, macro[3:-1], JOINERS.get(letter)) - if letter != macro[2]: - e = urllib.quote(e) - result += e - - end = i.end() - result += str[end:] - if stripdot and result.endswith('.'): - result = result[:-1] - if result.count('.') != 0: - if len(result) > 253: - result = result[(result.index('.')+1):] - return result - - def dns_spf(self, domain): - """Get the SPF record recorded in DNS for a specific domain - name. Returns None if not found, or if more than one record - is found. - """ - # Per RFC 4.3/1, check for malformed domain. This produces - # no results as a special case. - for label in domain.split('.'): - if not label or len(label) > 63: - return None - # for performance, check for most common case of TXT first - a = [t for t in self.dns_txt(domain) if RE_SPF.match(t)] - if len(a) > 1: - raise PermError('Two or more type TXT spf records found.') - if len(a) == 1 and self.strict < 2: - return a[0] - # check official SPF type first when it becomes more popular - try: - b = [t for t in self.dns_99(domain) if RE_SPF.match(t)] - except TempError,x: - # some braindead DNS servers hang on type 99 query - if self.strict > 1: raise TempError(x) - b = [] - - if len(b) > 1: - raise PermError('Two or more type SPF spf records found.') - if len(b) == 1: - if self.strict > 1 and len(a) == 1 and a[0] != b[0]: - #Changed from permerror to warning based on RFC 4408 Auth 48 change - raise AmbiguityWarning( -'v=spf1 records of both type TXT and SPF (type 99) present, but not identical') - return b[0] - if len(a) == 1: - return a[0] # return TXT if SPF wasn't found - if DELEGATE: # use local record if neither found - a = [t - for t in self.dns_txt(domain+'._spf.'+DELEGATE) - if RE_SPF.match(t) - ] - if len(a) == 1: return a[0] - return None - - def dns_txt(self, domainname): - "Get a list of TXT records for a domain name." - if domainname: - return [''.join(a) for a in self.dns(domainname, 'TXT')] - return [] - def dns_99(self, domainname): - "Get a list of type SPF=99 records for a domain name." - if domainname: - return [''.join(a) for a in self.dns(domainname, 'SPF')] - return [] - - def dns_mx(self, domainname): - """Get a list of IP addresses for all MX exchanges for a - domain name. - """ - # RFC 4408 section 5.4 "mx" - # To prevent DoS attacks, more than 10 MX names MUST NOT be looked up - mxnames = self.dns(domainname, 'MX') - if self.strict: - max = MAX_MX - if self.strict > 1: - if len(mxnames) > MAX_MX: - raise AmbiguityWarning( - 'More than %d MX records returned'%MAX_MX) - if len(mxnames) == 0: - raise AmbiguityWarning( - 'No MX records found for mx mechanism', domainname) - else: - max = MAX_MX * 4 - return [a for mx in mxnames[:max] for a in self.dns_a(mx[1],self.A)] - - def dns_a(self, domainname, A='A'): - """Get a list of IP addresses for a domainname. - """ - if not domainname: return [] - if self.strict > 1: - alist = self.dns(domainname, A) - if len(alist) == 0: - raise AmbiguityWarning( - 'No %s records found for'%A, domainname) - else: - return alist - return self.dns(domainname, A) - - def validated_ptrs(self): - """Figure out the validated PTR domain names for the connect IP.""" -# To prevent DoS attacks, more than 10 PTR names MUST NOT be looked up - if self.strict: - max = MAX_PTR - if self.strict > 1: - #Break out the number of PTR records returned for testing - try: - ptrnames = self.dns_ptr(self.i) - if len(ptrnames) > max: - warning = 'More than %d PTR records returned' % max - raise AmbiguityWarning(warning, i) - else: - if len(ptrnames) == 0: - raise AmbiguityWarning( - 'No PTR records found for ptr mechanism', self.c) - except: - raise AmbiguityWarning( - 'No PTR records found for ptr mechanism', i) - else: - max = MAX_PTR * 4 - cidrlength = self.cidrmax - return [p for p in self.dns_ptr(self.i)[:max] - if self.cidrmatch(self.dns_a(p,self.A),cidrlength)] - - def dns_ptr(self, i): - """Get a list of domain names for an IP address.""" - return self.dns('%s.%s.arpa'%(reverse_dots(i),self.v), 'PTR') - - # We have to be careful which additional DNS RRs we cache. For - # instance, PTR records are controlled by the connecting IP, and they - # could poison our local cache with bogus A and MX records. - - SAFE2CACHE = { - ('MX','A'): None, - ('MX','MX'): None, - ('CNAME','A'): None, - ('CNAME','CNAME'): None, - ('A','A'): None, - ('AAAA','AAAA'): None, - ('PTR','PTR'): None, - ('TXT','TXT'): None, - ('SPF','SPF'): None - } - - def dns(self, name, qtype, cnames=None): - """DNS query. - - If the result is in cache, return that. Otherwise pull the - result from DNS, and cache ALL answers, so additional info - is available for further queries later. - - CNAMEs are followed. - - If there is no data, [] is returned. - - pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF'] - post: isinstance(__return__, types.ListType) - """ - result = self.cache.get( (name, qtype) ) - cname = None - - if not result: - safe2cache = query.SAFE2CACHE - for k, v in DNSLookup(name, qtype, self.strict): - if k == (name, 'CNAME'): - cname = v - if (qtype,k[1]) in safe2cache: - self.cache.setdefault(k, []).append(v) - result = self.cache.get( (name, qtype), []) - if not result and cname: - if not cnames: - cnames = {} - elif len(cnames) >= MAX_CNAME: - #return result # if too many == NX_DOMAIN - raise PermError('Length of CNAME chain exceeds %d' % MAX_CNAME) - cnames[name] = cname - if cname in cnames: - raise PermError, 'CNAME loop' - result = self.dns(cname, qtype, cnames=cnames) - return result - - def cidrmatch(self, ipaddrs, n): - """Match connect IP against a list of other IP addresses.""" - try: - if self.v == 'ip6': - MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL - bin = bin2long6 - else: - MASK = 0xFFFFFFFFL - bin = addr2bin - c = ~(MASK >> n) & MASK & self.ip - for ip in [bin(ip) for ip in ipaddrs]: - if c == ~(MASK >> n) & MASK & ip: return True - except socket.error: pass - return False - - def get_header(self, res, receiver=None): - if not receiver: - receiver = self.r - if res in ('pass', 'fail',' softfail'): - return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % ( - res, receiver, self.get_header_comment(res), self.c, - self.l + '@' + self.o, self.h) - if res == 'permerror': - return '%s (%s: %s)' % (' '.join([res] + self.mech), - receiver,self.get_header_comment(res)) - return '%s (%s: %s)' % (res, receiver, self.get_header_comment(res)) - - def get_header_comment(self, res): - """Return comment for Received-SPF header. - """ - sender = self.o - if res == 'pass': - return \ - "domain of %s designates %s as permitted sender" \ - % (sender, self.c) - elif res == 'softfail': return \ - "transitioning domain of %s does not designate %s as permitted sender" \ - % (sender, self.c) - elif res == 'neutral': return \ - "%s is neither permitted nor denied by domain of %s" \ - % (self.c, sender) - elif res == 'none': return \ - "%s is neither permitted nor denied by domain of %s" \ - % (self.c, sender) - #"%s does not designate permitted sender hosts" % sender - elif res == 'permerror': return \ - "permanent error in processing domain of %s: %s" \ - % (sender, self.prob) - elif res == 'error': return \ - "temporary error in processing during lookup of %s" % sender - elif res == 'fail': return \ - "domain of %s does not designate %s as permitted sender" \ - % (sender, self.c) - raise ValueError("invalid SPF result for header comment: "+res) - -def split_email(s, h): - """Given a sender email s and a HELO domain h, create a valid tuple - (l, d) local-part and domain-part. - - Examples: - >>> split_email('', 'wayforward.net') - ('postmaster', 'wayforward.net') - - >>> split_email('foo.com', 'wayforward.net') - ('postmaster', 'foo.com') - - >>> split_email('terry@wayforward.net', 'optsw.com') - ('terry', 'wayforward.net') - """ - if not s: - return 'postmaster', h - else: - parts = s.split('@', 1) - if parts[0] == '': - parts[0] = 'postmaster' - if len(parts) == 2: - return tuple(parts) - else: - return 'postmaster', s - -def parse_mechanism(str, d): - """Breaks A, MX, IP4, and PTR mechanisms into a (name, domain, - cidr,cidr6) tuple. The domain portion defaults to d if not present, - the cidr defaults to 32 if not present. - - Examples: - >>> parse_mechanism('a', 'foo.com') - ('a', 'foo.com', None, None) - - >>> parse_mechanism('a:bar.com', 'foo.com') - ('a', 'bar.com', None, None) - - >>> parse_mechanism('a/24', 'foo.com') - ('a', 'foo.com', 24, None) - - >>> parse_mechanism('A:foo:bar.com/16', 'foo.com') - ('a', 'foo:bar.com', 16, None) - - >>> parse_mechanism('-exists:%{i}.%{s1}.100/86400.rate.%{d}','foo.com') - ('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', None, None) - - >>> parse_mechanism('mx:%%%_/.Claranet.de/27','foo.com') - ('mx', '%%%_/.Claranet.de', 27, None) - - >>> parse_mechanism('mx:%{d}/27','foo.com') - ('mx', '%{d}', 27, None) - - >>> parse_mechanism('iP4:192.0.0.0/8','foo.com') - ('ip4', '192.0.0.0', 8, None) - """ - - a = RE_DUAL_CIDR.split(str) - if len(a) == 3: - str, cidr6 = a[0], int(a[1]) - else: - cidr6 = None - a = RE_CIDR.split(str) - if len(a) == 3: - str, cidr = a[0], int(a[1]) - else: - cidr = None - - a = str.split(':', 1) - if len(a) < 2: - return str.lower(), d, cidr, cidr6 - return a[0].lower(), a[1], cidr, cidr6 - -def reverse_dots(name): - """Reverse dotted IP addresses or domain names. - - Example: - >>> reverse_dots('192.168.0.145') - '145.0.168.192' - - >>> reverse_dots('email.example.com') - 'com.example.email' - """ - a = name.split('.') - a.reverse() - return '.'.join(a) - -def domainmatch(ptrs, domainsuffix): - """grep for a given domain suffix against a list of validated PTR - domain names. - - Examples: - >>> domainmatch(['FOO.COM'], 'foo.com') - 1 - - >>> domainmatch(['moo.foo.com'], 'FOO.COM') - 1 - - >>> domainmatch(['moo.bar.com'], 'foo.com') - 0 - - """ - domainsuffix = domainsuffix.lower() - for ptr in ptrs: - ptr = ptr.lower() - - if ptr == domainsuffix or ptr.endswith('.' + domainsuffix): - return True - - return False - -def addr2bin(str): - """Convert a string IPv4 address into an unsigned integer. - - Examples:: - >>> addr2bin('127.0.0.1') - 2130706433L - - >>> addr2bin('127.0.0.1') == socket.INADDR_LOOPBACK - 1 - - >>> addr2bin('255.255.255.254') - 4294967294L - - >>> addr2bin('192.168.0.1') - 3232235521L - - Unlike DNS.addr2bin, the n, n.n, and n.n.n forms for IP addresses - are handled as well:: - >>> addr2bin('10.65536') - 167837696L - >>> 10 * (2 ** 24) + 65536 - 167837696 - - >>> addr2bin('10.93.512') - 173867520L - >>> 10 * (2 ** 24) + 93 * (2 ** 16) + 512 - 173867520 - """ - return struct.unpack("!L", socket.inet_aton(str))[0] - -def bin2long6(str): - h, l = struct.unpack("!QQ", str) - return h << 64 | l - -if socket.has_ipv6: - def inet_ntop(s): - return socket.inet_ntop(socket.AF_INET6,s) - def inet_pton(s): - return socket.inet_pton(socket.AF_INET6,s) -else: - def inet_ntop(s): - """Convert ip6 address to standard hex notation. - Examples: - >>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0xFFFF,0x0102,0x0304)) - '::FFFF:1.2.3.4' - >>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0,0,0,0x0102,0x0304)) - '1234:5678::102:304' - >>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0x1234,0x5678,0,0x0102,0x0304)) - '::1234:5678:0:102:304' - >>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0x0102,0x0304,0,0,0)) - '1234:5678:0:102:304::' - >>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0,0,0)) - '::' - """ - # convert to 8 words - a = struct.unpack("!HHHHHHHH",s) - n = (0,0,0,0,0,0,0,0) # null ip6 - if a == n: return '::' - # check for ip4 mapped - if a[:5] == (0,0,0,0,0) and a[5] in (0,0xFFFF): - ip4 = '.'.join([str(i) for i in struct.unpack("!HHHHHHBBBB",s)[6:]]) - if a[5]: - return "::FFFF:" + ip4 - return "::" + ip4 - # find index of longest sequence of 0 - for l in (7,6,5,4,3,2,1): - e = n[:l] - for i in range(9-l): - if a[i:i+l] == e: - if i == 0: - return ':'+':%x'*(8-l) % a[l:] - if i == 8 - l: - return '%x:'*(8-l) % a[:-l] + ':' - return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:] - return "%x:%x:%x:%x:%x:%x:%x:%x" % a - - def inet_pton(p): - """Convert ip6 standard hex notation to ip6 address. - Examples: - >>> struct.unpack('!HHHHHHHH',inet_pton('::')) - (0, 0, 0, 0, 0, 0, 0, 0) - >>> struct.unpack('!HHHHHHHH',inet_pton('::1234')) - (0, 0, 0, 0, 0, 0, 0, 4660) - >>> struct.unpack('!HHHHHHHH',inet_pton('1234::')) - (4660, 0, 0, 0, 0, 0, 0, 0) - >>> struct.unpack('!HHHHHHHH',inet_pton('1234::5678')) - (4660, 0, 0, 0, 0, 0, 0, 22136) - >>> struct.unpack('!HHHHHHHH',inet_pton('::FFFF:1.2.3.4')) - (0, 0, 0, 0, 0, 65535, 258, 772) - >>> struct.unpack('!HHHHHHHH',inet_pton('1.2.3.4')) - (0, 0, 0, 0, 0, 65535, 258, 772) - >>> try: inet_pton('::1.2.3.4.5') - ... except ValueError,x: print x - ::1.2.3.4.5 - """ - if p == '::': - return '\0'*16 - s = p - m = RE_IP4.search(s) - try: - if m: - pos = m.start() - ip4 = [int(i) for i in s[pos:].split('.')] - if not pos: - return struct.pack('!QLBBBB',0,65535,*ip4) - s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4) - a = s.split('::') - if len(a) == 2: - l,r = a - if not l: - r = r.split(':') - return struct.pack('!HHHHHHHH', - *[0]*(8-len(r)) + [int(s,16) for s in r]) - if not r: - l = l.split(':') - return struct.pack('!HHHHHHHH', - *[int(s,16) for s in l] + [0]*(8-len(l))) - l = l.split(':') - r = r.split(':') - return struct.pack('!HHHHHHHH', - *[int(s,16) for s in l] + [0]*(8-len(l)-len(r)) - + [int(s,16) for s in r]) - if len(a) == 1: - return struct.pack('!HHHHHHHH', - *[int(s,16) for s in a[0].split(':')]) - except ValueError: pass - raise ValueError,p - -def expand_one(expansion, str, joiner): - if not str: - return expansion - ln, reverse, delimiters = RE_ARGS.split(str)[1:4] - if not delimiters: - delimiters = '.' - expansion = split(expansion, delimiters, joiner) - if reverse: expansion.reverse() - if ln: expansion = expansion[-int(ln)*2+1:] - return ''.join(expansion) - -def split(str, delimiters, joiner=None): - """Split a string into pieces by a set of delimiter characters. The - resulting list is delimited by joiner, or the original delimiter if - joiner is not specified. - - Examples: - >>> split('192.168.0.45', '.') - ['192', '.', '168', '.', '0', '.', '45'] - - >>> split('terry@wayforward.net', '@.') - ['terry', '@', 'wayforward', '.', 'net'] - - >>> split('terry@wayforward.net', '@.', '.') - ['terry', '.', 'wayforward', '.', 'net'] - """ - result, element = [], '' - for c in str: - if c in delimiters: - result.append(element) - element = '' - if joiner: - result.append(joiner) - else: - result.append(c) - else: - element += c - result.append(element) - return result - -def insert_libspf_local_policy(spftxt, local=None): - """Returns spftxt with local inserted just before last non-fail - mechanism. This is how the libspf{2} libraries handle "local-policy". - - Examples: - >>> insert_libspf_local_policy('v=spf1 -all') - 'v=spf1 -all' - >>> insert_libspf_local_policy('v=spf1 -all','mx') - 'v=spf1 -all' - >>> insert_libspf_local_policy('v=spf1','a mx ptr') - 'v=spf1 a mx ptr' - >>> insert_libspf_local_policy('v=spf1 mx -all','a ptr') - 'v=spf1 mx a ptr -all' - >>> insert_libspf_local_policy('v=spf1 mx -include:foo.co +all','a ptr') - 'v=spf1 mx a ptr -include:foo.co +all' - - # FIXME: is this right? If so, "last non-fail" is a bogus description. - >>> insert_libspf_local_policy('v=spf1 mx ?include:foo.co +all','a ptr') - 'v=spf1 mx a ptr ?include:foo.co +all' - >>> spf='v=spf1 ip4:1.2.3.4 -a:example.net -all' - >>> local='ip4:192.0.2.3 a:example.org' - >>> insert_libspf_local_policy(spf,local) - 'v=spf1 ip4:1.2.3.4 ip4:192.0.2.3 a:example.org -a:example.net -all' - """ - # look to find the all (if any) and then put local - # just after last non-fail mechanism. This is how - # libspf2 handles "local policy", and some people - # apparently find it useful (don't ask me why). - if not local: return spftxt - spf = spftxt.split()[1:] - if spf: - # local policy is SPF mechanisms/modifiers with no - # 'v=spf1' at the start - spf.reverse() #find the last non-fail mechanism - for mech in spf: - # map '?' '+' or '-' to 'neutral' 'pass' - # or 'fail' - if not RESULTS.get(mech[0]): - # actually finds last mech with default result - where = spf.index(mech) - spf[where:where] = [local] - spf.reverse() - local = ' '.join(spf) - break - else: - return spftxt # No local policy adds for v=spf1 -all - # Processing limits not applied to local policy. Suggest - # inserting 'local' mechanism to handle this properly - #MAX_LOOKUP = 100 - return 'v=spf1 '+local - -def _test(): - import doctest, spf - return doctest.testmod(spf) - -DNS.DiscoverNameServers() # Fails on Mac OS X? Add domain to /etc/resolv.conf - -if __name__ == '__main__': - import sys - if len(sys.argv) == 1: - print USAGE - _test() - elif len(sys.argv) == 2: - q = query(i='127.0.0.1', s='localhost', h='unknown', - receiver=socket.gethostname()) - print q.dns_spf(sys.argv[1]) - elif len(sys.argv) == 4: - print check(i=sys.argv[1], s=sys.argv[2], h=sys.argv[3], - receiver=socket.gethostname()) - elif len(sys.argv) == 5: - i, s, h = sys.argv[2:] - q = query(i=i, s=s, h=h, receiver=socket.gethostname(), - strict=False) - print q.check(sys.argv[1]) - if q.perm_error and q.perm_error.ext: - print q.perm_error.ext - else: - print USAGE diff --git a/spfquery.py b/spfquery.py deleted file mode 100755 index 96f813c51a8f331c0e25c8cb56c87b6d7bdab0e8..0000000000000000000000000000000000000000 --- a/spfquery.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/python2.3 - -# Author: Stuart D. Gathman <stuart@bmsi.com> -# Copyright 2004 Business Management Systems, Inc. -# This code is under the GNU General Public License. See COPYING for details. - -# $Log$ -# Revision 1.1.1.1 2005/05/31 18:07:19 customdesigned -# Release 0.6.9 -# -# Revision 2.3 2004/04/19 22:12:11 stuart -# Release 0.6.9 -# -# Revision 2.2 2004/04/18 03:29:35 stuart -# Pass most tests except -local and -rcpt-to -# -# Revision 2.1 2004/04/08 18:41:15 stuart -# Reject numeric hello names -# -# Driver for SPF test system - -import spf -import sys - -from optparse import OptionParser - -class PerlOptionParser(OptionParser): - def _process_args (self, largs, rargs, values): - """_process_args(largs : [string], - rargs : [string], - values : Values) - - Process command-line arguments and populate 'values', consuming - options and arguments from 'rargs'. If 'allow_interspersed_args' is - false, stop at the first non-option argument. If true, accumulate any - interspersed non-option arguments in 'largs'. - """ - while rargs: - arg = rargs[0] - # We handle bare "--" explicitly, and bare "-" is handled by the - # standard arg handler since the short arg case ensures that the - # len of the opt string is greater than 1. - if arg == "--": - del rargs[0] - return - elif arg[0:2] == "--": - # process a single long option (possibly with value(s)) - self._process_long_opt(rargs, values) - elif arg[:1] == "-" and len(arg) > 1: - # process a single perl style long option - rargs[0] = '-' + arg - self._process_long_opt(rargs, values) - elif self.allow_interspersed_args: - largs.append(arg) - del rargs[0] - else: - return - -def format(q): - res,code,txt = q.check() - print res - if res in ('pass','neutral','unknown'): print - else: print txt - print 'spfquery:',q.get_header_comment(res) - print 'Received-SPF:',q.get_header(res,'spfquery') - -def main(argv): - parser = PerlOptionParser() - parser.add_option("--file",dest="file") - parser.add_option("--ip",dest="ip") - parser.add_option("--sender",dest="sender") - parser.add_option("--helo",dest="hello_name") - parser.add_option("--local",dest="local_policy") - parser.add_option("--rcpt-to",dest="rcpt") - parser.add_option("--default-explanation",dest="explanation") - parser.add_option("--sanitize",type="int",dest="sanitize") - parser.add_option("--debug",type="int",dest="debug") - opts,args = parser.parse_args(argv) - if opts.ip: - q = spf.query(opts.ip,opts.sender,opts.hello_name,local=opts.local_policy) - if opts.explanation: - q.set_default_explanation(opts.explanation) - format(q) - if opts.file: - if opts.file == '0': - fp = sys.stdin - else: - fp = open(opts.file,'r') - for ln in fp: - ip,sender,helo,rcpt = ln.split(None,3) - q = spf.query(ip,sender,helo,local=opts.local_policy) - if opts.explanation: - q.set_default_explanation(opts.explanation) - format(q) - fp.close() - -if __name__ == "__main__": - import sys - main(sys.argv[1:])