├── .gitignore ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.md ├── cid2spf.py ├── pep8.dat ├── pyspf.spec ├── pyspf_changelog.txt ├── python-pyspf.spec ├── setup.py ├── spf.py ├── spfquery.py ├── test ├── doctest.yml ├── rfc4408-tests.LICENSE ├── rfc4408-tests.yml ├── rfc7208-tests.CHANGES ├── rfc7208-tests.LICENSE ├── rfc7208-tests.yml ├── test.yml └── testspf.py └── type99.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | pyspf.egg-info/ 3 | __pycache__/ 4 | *.pyc 5 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 2.1.0 2 | * Switch to setuptools because "it's the future" 3 | * Require ipaddress instead of ipaddr for python2 4 | * Fix issue Getting list of ip networks fails if prefix is 32 or more, 5 | GitHub #24 6 | 7 | Version 2.0.14 - January 1, 2020 8 | * Fix doctest for CNAME fixes to work with python and python3 9 | * Fix dnspython integration so that SPF TempError is properly raised when 10 | there are timeout or no nameserver errors 11 | * Add missing use of timeout parameter for dnspython DNSLookup 12 | * Restore DNSLookup API for pydnsv(DNS) for tcp fallback works again 13 | * Update Installation section of README.md 14 | 15 | Version 2.0.13 - September 2, 2019 16 | * Add support for use of dnspython (dns) if installed 17 | * Catch ValueError due to improper IP address in connect IP or in ip4/ip6 18 | mechanisms 19 | * Fix for CNAME processing causing incorrect permerrors 20 | 21 | Version 2.0.12 - August 5, 2015 22 | * Reset void_lookups at top of check() 23 | * Ignore permerror for best_guess() 24 | * Don't crash on null DNS TXT record (ignore): test case null-text 25 | * Trailing spaces are allowed by 4.5/2: test case trailing-space 26 | * Make CNAME loop result in unknown host: test case ptr-cname-loop 27 | * Test case and fix for mixed case CNAME loop, test case ptr-cname-loop 28 | 29 | Version 2.0.11 - December 5, 2014 30 | * Fix another bug in SPF record parsing that caused records with terms 31 | separated by multple spaces as invalid, but they are fine per the ABNF 32 | * Downcase names in additional answers returned by DNS before adding 33 | to cache, since case inconsistency can cause PTR match failures (initial 34 | patch thanks to Joni Fieggen) and other problems. 35 | 36 | Version 2.0.10 - September 2, 2014 37 | * Fix bug in SPF record parsing that caused all 'whitespace' characters to 38 | be considered valid term separators and not just spaces 39 | * Fixed multiple bugs in temperror processing that would lead to tracebacks 40 | instead of correct error processing 41 | * Fix AAAA not flagged as bytes when strict=2 42 | * Include '~' as safe char in url quoted macro expansion 43 | 44 | Version 2.0.9 - April 29, 2014 45 | * Update for new SPF standards track RFC 7208 46 | - Add processing for new void lookups processing limit 47 | - Default SPF process timeout limit to 20 seconds per RFC 7208 4.6.4 48 | - Change default DNS timeout to 20 seconds in DNSLookup to better match 49 | RFC 7208 4.6.4 50 | - Make mx lookups > 10 a permerror per RFC 7208 and mx-limit test 51 | - Add RFC 7208 specific test suite and make allowance for RFC 7208 changes 52 | in RFC 4408 test suite 53 | - Convert YAML tests to TestCases, and have testspf.py return success/fail. 54 | 55 | Version 2.0.8 - July 24, 2013 56 | * Use ipaddr/ipaddres module in place of custom IP processing code 57 | * Numerous python3 compatibility fixes 58 | * Improved unicode error detection in SPF records 59 | * Fixed a bug caused by a null CNAME in cache 60 | 61 | Version 2.0.7 - January 19, 2012 62 | * Allow for timeouts to be global for all DNS lookups instead of per DNS lookup 63 | to allow for MAY processing time limitsin RFC 4408 10.1. See README for 64 | details. 65 | * Use openspf.net for SPF web site instead of openspf.org 66 | * Extend query.get_header to return either Received-SPF (still default) or 67 | RFC 5451 Authentication Results headers (needs authres 0.3 or later) 68 | * Rework query.parse_header: 69 | - Make query.parse_header automatically select Received-DPF or 70 | Authentication Results header types and use them to collect SPF 71 | results from trusted relays 72 | - Add query.parse_header_spf and query.parse_header_ar functions for 73 | header type specific processing 74 | * Finish Python3 port - works with python2.6/2.7/3.2 and 2to3 is no longer 75 | required - will also work with newer py3dns where TXT records are returned 76 | as type bytes and not strings 77 | * Accounts for new py3dns error classes coming in py3dns 3.0.2 (but fully 78 | backward compatible with earlier versions) 79 | * check for 7-bit ascii on TXT and SPF records 80 | * fix CNAME chain duplicating TXT records 81 | 82 | Version 2.0.6 - October 27, 2011 83 | * Refactor code so that 2to3 will provide a working python3 module - Now 84 | requires at least python2.6 85 | * Update spfquery.py, type99.py, and testspf.py to work with either python or 86 | python3 (2to3 not needed for these scripts) 87 | - SPF test suite can now be run from either python or python3 88 | * Ensure Temperror for all DNS rcodes other than 0 and 3 per RFC 4408 89 | * Parse Received-SPF header 90 | * Report CIDR error only for valid mechanism 91 | * Handle invalid SPF record on command line 92 | * Add timeout to check2 93 | 94 | Version 2.0.5 - July 29, 2008 95 | * Add TCP fallback if DNS UDP reply is truncated 96 | - Fixes inconsistent results from trying to use partial UDP replies 97 | * Correct Received-SPF formatting 98 | * Minor updates to reflect RFC 4408 errata 99 | * Added License file for RFC 4408 test suite 100 | * Update RFC 4408 test suite from svn 101 | * Fix Type99 conversion script to work with multi-string TXT records 102 | * Timeout parameter 103 | 104 | Version 2.0.4 - January 24, 2007 105 | * Correct unofficial 'best guess' processing. 106 | * PTR validation processing cleanup 107 | * Improved detection of exp= errors 108 | * Keyword parameters on get_header() 109 | 110 | Version 2.0.3 - January 15, 2007 111 | * IPv6 compatibility test fix to support Python 2.2 112 | * Change DNS queries to only check Type SPF in Harsh mode 113 | * pyspf requires pydns, python-pyspf requires python-pydns 114 | * Record matching mechanism and add to Received-SPF header. 115 | * Test for RFC4408 6.2/4, and fix spf.py to comply. 116 | * Permerror for more than one exp or redirect modifier. 117 | * Parse op= modifier 118 | 119 | Version 2.0.2 - January 4, 2007 120 | * Update openspf URLs 121 | * Update Readme to better describe available pyspf interfaces 122 | * Add basic description of type99.py and spfquery.py scripts 123 | * Add usage instructions for type99.py DNS RR type conversion script 124 | * Add spfquery.py usage instructions 125 | * Incorporate downstream feedback from Debian packager 126 | * Fix key-value quoting in get_header 127 | 128 | Version 2.0.1 - December 08, 2006 129 | * Prevent cache poisoning attack 130 | * Prevent malformed RR attack 131 | * Update license on a few files we missed last time 132 | 133 | Version 2.0 - November 20, 2006 134 | * Completed RFC 4408 compliance 135 | * Added spf.check2 for RFC 4408 compatible result codes 136 | * Full IP6 support 137 | * Fedora Core compatible RPM spec file 138 | * Update README, licenses 139 | 140 | Version 1.8 - July 26, 2006 141 | * YAML test suite syntax 142 | * trailing dot support (RFC4408 8.1) 143 | 144 | Version 1.7 - July 21, 2005 145 | * Strict processing limits per newly official SPF RFC 146 | * Fixed several parsing bugs under RFC 147 | * Support official IANA SPF record (type99) 148 | * Extended SPF processing results beyond strict RFC limits 149 | * Validate spf.py against test suite, and add Received-SPF support to spf.py 150 | * Support best_guess for SPF 151 | * Support SPF delegation 152 | 153 | Version 1.6 - December 18, 2003 154 | * Arik Baratz pointed out endian problems using socket.inet_ntoa() and 155 | socket.inet_aton(). Use struct.pack("!L", struct.unpack("!L") to fix. 156 | 157 | Version 1.5 - December 17, 2003 158 | * Replace DNS.addr2bin() and DNS.bin2addr() with socket.inet_ntoa() and 159 | socket.inet_aton(). New code supports n, n.n, and n.n.n formats for IPv4 160 | addresses, and gets rid of annoying Python 2.4 future warnings 161 | 162 | Version 1.4 - December 16, 2003 163 | * Greg Connor discovered that SPF queries to altavista.com were broken. 164 | This was testing to see if a mechanism needs to be macro expanded _before_ 165 | leading ? + - characters were removed. 166 | * Fixed include handling to be a real mechanism: -include must work. 167 | 168 | Version 1.3.1 - December 14, 2003 169 | * Forgot to include new test file in distribution. 170 | * Forgot CHANGELOG in distribution. 171 | 172 | Version 1.3 - December 13, 2003 173 | * Add %{o} (original sender domain) macro 174 | * The ./spf.py {spf} {ipaddr} {sender} {helo} command line didn't print 175 | out the results. Oops. 176 | * Support default= so Meng's test #6 'v=spf1 default=deny' works 177 | * Any IP address '127.*.*.*' automatically pass, so all Meng's tests work 178 | * Follow DNS CNAMES 179 | * Cache DNS results, including additional info, reducing DNS query load 180 | * Support Python 2.2 (doesn't have bool, True, False: those are 181 | added in Python 2.2.1) 182 | 183 | Version 1.2 - December 11, 2003 184 | * Added exp= (explanation) and redirect= modifiers 185 | * Added macros 186 | 187 | Version 1.1 - December 9, 2003 188 | * Meng Weng Wong added PTR code, THANK YOU 189 | 190 | Version 1.0 - December 9, 2003 191 | * Initial Version 192 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 2 | -------------------------------------------- 3 | 4 | 1. This LICENSE AGREEMENT is between the Authors, Terence Way, 5 | Stuart Gathman, Scott Kitterman ("Authors"), and the Individual or Organization 6 | ("Licensee") accessing and otherwise using this software ("pyspf") in source or 7 | binary form and its associated documentation. 8 | 9 | 2. Subject to the terms and conditions of this License Agreement, the Authors 10 | hereby grant Licensee a nonexclusive, royalty-free, world-wide 11 | license to reproduce, analyze, test, perform and/or display publicly, 12 | prepare derivative works, distribute, and otherwise use pyspf 13 | alone or in any derivative version, provided, however, that the Authors' 14 | License Agreement and the Authors' notice of copyright, i.e., 15 | 16 | Copyright (c) 2003 Terence Way 17 | Portions Copyright(c) 2004,2005,2006,2007,2008,2011,2012 Stuart Gathman 18 | Portions Copyright(c) 2005,2006,2007,2008,2011,2012,2013,2014 Scott Kitterman 19 | Portions Copyright(c) 2013,2014,2020 Stuart Gathman 20 | 21 | are retained in pyspf alone or in any derivative version prepared 22 | by Licensee. 23 | 24 | 3. In the event Licensee prepares a derivative work that is based on 25 | or incorporates pyspf or any part thereof, and wants to make 26 | the derivative work available to others as provided herein, then 27 | Licensee hereby agrees to include in any such work a brief summary of 28 | the changes made to pyspf. 29 | 30 | 4. The Authors are making pyspf available to Licensee on an "AS IS" 31 | basis. THE AUTHORS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 32 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, THE AUTHORS MAKE NO AND 33 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 34 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 35 | INFRINGE ANY THIRD PARTY RIGHTS. 36 | 37 | 5. THE AUTHORS SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 38 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 39 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 40 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 41 | 42 | 6. This License Agreement will automatically terminate upon a material 43 | breach of its terms and conditions. 44 | 45 | 7. Nothing in this License Agreement shall be deemed to create any 46 | relationship of agency, partnership, or joint venture between the Authors and 47 | Licensee. This License Agreement does not grant permission to use the Authors 48 | trademarks or trade name in a trademark sense to endorse or promote 49 | products or services of Licensee, or any third party. 50 | 51 | 8. By copying, installing or otherwise using pyspf, Licensee 52 | agrees to be bound by the terms and conditions of this License 53 | Agreement. 54 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README* 2 | include MANIFEST.in 3 | include NOTES 4 | include CHANGELOG 5 | include LICENSE 6 | include spf.py 7 | include spfquery.py 8 | include type99.py 9 | include setup.py 10 | include setup.cfg 11 | include test/*.yml 12 | include test/*.LICENSE 13 | include test/*.CHANGES 14 | include test/testspf.py 15 | include *.spec 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SPF 2 | === 3 | 4 | Sender-Policy-Framework queries in Python 5 | ----------------------------------------- 6 | 7 | Quick Start 8 | =========== 9 | 10 | Installation 11 | ------------ 12 | This package requires either the dns (dnspython) or DNS (PyDNS/Py3DNS modules 13 | and either the ipaddress module or python3.3 and later. For dnspython, at 14 | least version 1.16.0 is required. The authres module is required to process 15 | and generate RFC 8601 Authentication Results headers. These can all be 16 | installed from pypi via pip. Additionally, they are also available via many 17 | distribution packaging systems. 18 | 19 | The minimum Python version required is python2.6. The spf module in this 20 | version has been tested with python3 versions through python3.8. 21 | 22 | Testing 23 | ------- 24 | After this package is installed, cd into the test directory and 25 | execute testspf.py:: 26 | 27 | % cd test 28 | % python testspf.py 29 | WARN: invalid-domain-long in rfc4408-tests.yml, 8.1/2, 5/10: fail preferred to temperror 30 | WARN: txttimeout in rfc4408-tests.yml, 4.4/1: fail preferred to temperror 31 | WARN: spfoverride in rfc4408-tests.yml, 4.5/5: pass preferred to fail 32 | WARN: multitxt1 in rfc4408-tests.yml, 4.5/5: pass preferred to permerror 33 | WARN: multispf2 in rfc4408-tests.yml, 4.5/6: permerror preferred to pass 34 | .. 35 | ---------------------------------------------------------------------- 36 | Ran 2 tests in 3.036s 37 | 38 | OK 39 | 40 | This runs the SPF council test-suite as of when this package was built. 41 | It does not test the pyDNS installation, but uses an internal driver. 42 | This avoids changing results due to DNS timeouts. 43 | 44 | In addition, spf.py runs an internal self-test every time it is used from the 45 | command line. 46 | 47 | If you're running on Mac OS X, and it looks like DNS.DiscoverNameServers() 48 | is failing, you'll need to edit your /etc/resolv.conf and specify a 49 | domain name. For some reason, OS X writes out resolv.conf with a single 50 | 'domain' line, which isn't good at all. Later versions of py3dns have been 51 | updated to better support Max OS X. 52 | 53 | 54 | Description 55 | =========== 56 | SPF does email sender validation. For more information about SPF, 57 | please see http://www.open-spf.org/ 58 | 59 | One incompatible change was introduced in version 1.7. Prior to version 1.7, 60 | connections from a local IP address (127...) would always return a Pass 61 | result. The special case was eliminated. Programs calling pySPF should not 62 | do SPF checks on locally submitted mail. 63 | 64 | This SPF client is intended to be installed on the border MTA, checking 65 | if incoming SMTP clients are permitted to forward mail. The SPF check 66 | should be done during the MAIL FROM:<...> command. 67 | 68 | There are two ways to use this package. The first is from the command 69 | line:: 70 | 71 | % python spf.py {ip-addr} {mail-from} {helo} 72 | 73 | For instance, during an SMTP exchange from client 69.55.226.139:: 74 | 75 | S: 220 mail.example.com ESMTP Postfix 76 | C: EHLO mx1.wayforward.net 77 | S: 250-mail.example.com 78 | S: ... 79 | S: 250 8BITMIME 80 | C: MAIL FROM: 81 | 82 | Then the following command line would check if this is a valid sender:: 83 | 84 | % ./spf.py 69.55.226.139 terry@wayforward.net mx1.wayforward.net ('pass', 250, 'sender SPF authorized') 85 | 86 | Command line calls return RFC 4408/7208 result codes, i.e. 'pass', 'fail', 87 | 'neutral', 'softfail, 'permerror', or 'temperror'. 88 | 89 | The second way is via the module's APIs. 90 | 91 | The legacy (pySPF 1.6) API: 92 | >>> import spf 93 | >>> spf.check(i='69.55.226.139', 94 | ... s='terry@wayforward.net', 95 | ... h='mx1.wayforward.net') 96 | ('pass', 250, 'sender SPF authorized') 97 | 98 | The first element in the tuple is one of 'pass', 'fail', 'netural', 'softfail', 99 | 'unknown', or 'error'. The second is the SMTP response status code: 550 for 100 | 'fail', 450 for 'error' and 250 for all else. The third is an explanation. 101 | 102 | Note: SPF results alone are never sufficient to decide that a message should be 103 | accepted. Accept, reject, or defer decisions are a function of local reciever 104 | policy. 105 | 106 | The RFC 4408/7208 compliant API:: 107 | 108 | >>> import spf 109 | >>> spf.check2(i='69.55.226.139', 110 | ... s='terry@wayforward.net', 111 | ... h='mx1.wayforward.net') 112 | ('pass', 'sender SPF verified') 113 | 114 | The first element in the tuple is one of 'pass', 'fail', 'neutral', 'softfail, 115 | 'permerror', or 'temperror'. The second is an explanation. 116 | 117 | This package also provides two additional helper scripts; type99.py and 118 | spfquery.py. The type99.py script will convert DNS TXT strings to a binary 119 | equivalent suitable for use in a BIND zone file. The spfquery.py script is a 120 | Python reimplementination of Wayne Schlitt's spfquery command line tool. 121 | 122 | The type99.py script is called from the command line as follows: 123 | 124 | python type99.py "v=spf1 -all" {Note: Use your desired SPF record instead.} 125 | \# 12 0b763d73706631202d616c6c {This is the correct result for "v=spf1 -all"} 126 | 127 | or 128 | 129 | python type99 - {File name} 130 | 131 | The input file format is a standard BIND Zone file. The type99 script will add 132 | a Type99 record for each TXT record found in the file. Use of DNS type 99 133 | (type SPF) was removed from SPF in RFC 7208, so this script should be of 134 | historical interest only. 135 | 136 | The spfquery.py script is called with a number of possible options. Options can 137 | either use standard '-' prefix or be PERL style long options, '--'. Supported 138 | options are: 139 | 140 | "--file" or "-file" {filename}: Read the query (or queries) from the designated 141 | file. If {filename} is '0', then query inputs are read from STDIN. 142 | 143 | "--ip" or "-ip" {address}: Client IP address to use for SPF check. 144 | 145 | 146 | "--sender" or "-sender" {Mail From address}: Envelope sender from which mail was 147 | received. 148 | 149 | "--helo" or "-helo" {client hostname}: HELO/EHLO name used by SMTP client. 150 | 151 | "--local" or "-local" {local policy SPF string}: Additional SPF mechanisms to be 152 | checked on the basis of local policy. Note that local policy matches are 153 | not strictly SPF results. Local policy processing is not defined in RFC 154 | 4408 or RFC 7208. Result may vary among SPF implementations. 155 | 156 | "--rcpt-to" or "rcpt-to" {rcpt-to address - if available}: Receipt to address is 157 | not used for actual SPF processing, but if available it can be useful for 158 | logging, spf-received header construction, and providing useful rejection 159 | messages when messages are rejected due to SPF. 160 | 161 | "--default-explanation" or "-default-explanation" {explanation string}: Default 162 | Fail explanation string to be used. 163 | 164 | "--sanitize" or "-sanitize" and "--debug" or "-debug": These options are no-op 165 | in the Python implementation, but are valid inputs to provide compatibliity 166 | with input files developed to work with the original PERL and C spfquery 167 | implementations. 168 | 169 | Overall per SPF check time limits can be controlled by passing querytime 170 | to the spf.check2 function or when initializing a spf.query object. 171 | It is set to 20 seconds by default based on RFC 7208. If querytime is set to 172 | 0, then the overall time limit is disabled and the per DNS lookup limit is used 173 | instead. This defaults to 20 seconds and can be controlled via 174 | spf.MAX_PER_LOOKUP_TIME. RFC 4408 says that the overall limit MAY be used and 175 | recommends no less than 20 seconds if it is. RFC 7208 is stronger, so a 176 | default limit aligned to the RFC requirements is now used. 177 | 178 | License: Python Software Foundation License 179 | 180 | Author: 181 | Terence Way terry@wayforward.net 182 | http://www.wayforward.net/spf/ 183 | 184 | Maintainers: 185 | Stuart Gathman stuart@gathman.org 186 | Scott Kitterman scott@kitterman.com 187 | https://pypi.org/project/pyspf/ 188 | 189 | Code is currently hosted at https://github.com/sdgathman/pyspf/ 190 | -------------------------------------------------------------------------------- /cid2spf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | 3 | # Convert a MS Caller-ID entry (XML) to a SPF entry 4 | # This should be obsolete now. It is included for historical interest. 5 | # 6 | # (c) 2004 by Ernesto Baschny 7 | # (c) 2004 Python version by Stuart Gathman 8 | # 9 | # Date: 2004-02-25 10 | # Version: 1.0 11 | # 12 | # Usage: 13 | # ./cid2spf.pl "..." 14 | # 15 | # Note that the 'include' directives will also have to be checked and 16 | # "translated". Future versions of this script might be able to get a 17 | # domain name as an argument and "crawl" the DNS for the necessary 18 | # information. 19 | # 20 | # A complete reverse translation (SPF -> CID) might be impossible, since 21 | # there are no way to handle: 22 | # - PTR and EXISTS mechanism 23 | # - MX mechanism with an different domain as argument 24 | # - macros 25 | # 26 | # References: 27 | # http://www.microsoft.com/mscorp/twc/privacy/spam_callerid.mspx 28 | # http://spf.pobox.com/ 29 | # 30 | # Known bugs: 31 | # - Currently it won't handle the exclusions provided in the A and R 32 | # tags (prefix '!'). They will show up "as-is" in the SPF record 33 | # - I really haven't read the MS-CID specs in-depth, so there are probably 34 | # other bugs too :) 35 | # 36 | # Ernesto Baschny 37 | # 38 | 39 | import xml.sax 40 | import spf 41 | 42 | # ------------------------------------------------------------------------- 43 | class CIDParser(xml.sax.ContentHandler): 44 | "Convert a MS Caller-ID entry (XML) to a SPF entry" 45 | 46 | def __init__(self,q=None): 47 | self.spf = [] 48 | self.action = '-all' 49 | self.has_servers = None 50 | self.spf_entry = None 51 | if q: 52 | self.spf_query = q 53 | else: 54 | self.spf_query = spf.query(i='127.0.0.1', s='localhost', h='unknown') 55 | 56 | def startElement(self,tag,attr): 57 | if tag == 'm': 58 | if self.has_servers != None and not self.has_servers: 59 | raise ValueError( 60 | "Declared and later , this CID entry is not valid." 61 | ) 62 | self.has_servers = True 63 | elif tag == 'noMailServers': 64 | if self.has_servers: 65 | raise ValueError( 66 | "Declared and later , this CID entry is not valid." 67 | ) 68 | self.has_servers = False 69 | elif tag == 'ep': 70 | if attr.has_key('testing') and attr.getValue('testing') == 'true': 71 | # A CID with 'testing' found: 72 | # From the MS-specs: 73 | # "Documents in which such attribute is present with a true 74 | # value SHOULD be entirely ignored (one should act as if the 75 | # document were absent)" 76 | # From the SPF-specs: 77 | # "Neutral (?): The SPF client MUST proceed as if a domain did 78 | # not publish SPF data." 79 | # So we set SPF action to "neutral": 80 | self.action = '?all' 81 | elif tag == 'mx': 82 | # The empty MX-tag, same as SPF's MX-mechanism 83 | self.spf.append('mx') 84 | self.tag = tag 85 | 86 | def characters(self,text): 87 | tag = self.tag 88 | # Remove starting and trailing spaces from text: 89 | text = text.strip() 90 | 91 | if tag == 'a' or tag == 'r': 92 | # The A and R tags from MS-CID are both handled by the 93 | # ipv4/6-mechanisms from SPF: 94 | if text.find(':') < 0: 95 | mechanism = 'ip4' 96 | else: 97 | mechanism = 'ip6' 98 | self.spf.append(mechanism + ':' + text) 99 | elif tag == 'indirect': 100 | # MS-CID's indirect is "sort of" the include from SPF: 101 | # Not really true, because the tag from MS-CID also 102 | # provides a fallback in case the included domain doesn't provide 103 | # _ep-records: The inbound MX-servers of the included domains 104 | # are added to the list of allowed outgoing mailservers for the 105 | # domain that declared the _ep-record with the tag. 106 | # In SPF you would use the 'mx:domain' to handle this, but this 107 | # wouldn't depend on referred domain having or not SPF-records. 108 | cid_xml = self.cid_txt(text) 109 | if cid_xml: 110 | p = CIDParser() 111 | xml.sax.parseString(cid_xml,p) 112 | if p.has_servers != False: 113 | self.spf += p.spf 114 | else: 115 | self.spf.append('mx:' + text) 116 | 117 | def cid_txt(self,domain): 118 | q = self.spf_query 119 | domain='_ep.' + domain 120 | a = q.dns_txt(domain) 121 | if not a: return None 122 | if a[0].lower().startswith(''): 123 | return ''.join(a) 124 | return None 125 | 126 | def endElement(self,tag): 127 | if tag == 'ep': 128 | # This is the end... assemble what we've got 129 | spf_entry = ['v=spf1'] 130 | if self.has_servers != False: 131 | spf_entry += self.spf 132 | spf_entry.append(self.action) 133 | self.spf_entry = ' '.join(spf_entry) 134 | 135 | def spf_txt(self,cid_xml): 136 | if not cid_xml.startswith('<'): 137 | cid_xml = self.cid_txt(cid_xml) 138 | if not cid_xml: return None 139 | # Parse the beast. Any XML-problem will be reported by xlm.sax 140 | self.spf_entry = None 141 | xml.sax.parseString(cid_xml,self) 142 | return self.spf_entry 143 | 144 | if __name__ == '__main__': 145 | import sys 146 | if len(sys.argv) < 2: 147 | print >>sys.stderr, \ 148 | """Usage: %s "..." """ % sys.argv[0] 149 | sys.exit(1) 150 | 151 | cid_xml = sys.argv[1] 152 | 153 | p = CIDParser() 154 | print p.spf_txt(cid_xml) 155 | -------------------------------------------------------------------------------- /pep8.dat: -------------------------------------------------------------------------------- 1 | Check Description Justification 2 | E111 req indent 4 Creates more continuation lines 3 | E114 req indent 4 cmnt Same 4 | E231 req space after , makes calls like print() harder to read 5 | E266 no ## Required by Doxygen 6 | W291 trailing spaces in cmnt Needed for space preserving para reformat 7 | -------------------------------------------------------------------------------- /pyspf.spec: -------------------------------------------------------------------------------- 1 | %define __python python2.6 2 | %if "%{dist}" == ".el4" || "%{dist}" == ".el5" 3 | %define pythonbase python26 4 | %else 5 | %define pythonbase python 6 | %endif 7 | %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} 8 | 9 | Name: %{pythonbase}-pyspf 10 | Version: 2.0.14 11 | Release: 1 12 | Summary: Python module and programs for SPF (Sender Policy Framework). 13 | 14 | Group: Development/Languages 15 | License: Python Software Foundation License 16 | URL: http://sourceforge.net/forum/forum.php?forum_id=596908 17 | Source0: pyspf-%{version}.tar.gz 18 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 19 | 20 | BuildArch: noarch 21 | BuildRequires: %{pythonbase}-devel 22 | Requires: %{pythonbase}-pydns, %{pythonbase} >= 2.6 23 | Requires: %{pythonbase}-authres %{pythonbase}-ipaddr >= 2.1.10 24 | 25 | %description 26 | SPF does email sender validation. For more information about SPF, 27 | please see http://open-spf.org 28 | 29 | This SPF client is intended to be installed on the border MTA, checking 30 | if incoming SMTP clients are permitted to send mail. The SPF check 31 | should be done during the MAIL FROM:<...> command. 32 | 33 | %define namewithoutpythonprefix %(echo %{name} | sed 's/^%{pythonbase}-//') 34 | %prep 35 | %setup -q -n %{namewithoutpythonprefix}-%{version} 36 | 37 | %build 38 | %{__python} setup.py build 39 | 40 | %install 41 | rm -rf $RPM_BUILD_ROOT 42 | %{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT 43 | mv $RPM_BUILD_ROOT/usr/bin/type99.py $RPM_BUILD_ROOT/usr/bin/type99 44 | mv $RPM_BUILD_ROOT/usr/bin/spfquery.py $RPM_BUILD_ROOT/usr/bin/spfquery 45 | rm -f $RPM_BUILD_ROOT/usr/bin/*.py{o,c} 46 | 47 | %check 48 | %{__python} spf.py 49 | 50 | %clean 51 | rm -rf $RPM_BUILD_ROOT 52 | 53 | %files 54 | %defattr(-,root,root,-) 55 | %doc CHANGELOG PKG-INFO README test 56 | %{python_sitelib}/spf.py* 57 | /usr/bin/type99 58 | /usr/bin/spfquery 59 | /usr/lib/python2.6/site-packages/pyspf-%{version}-py2.6.egg-info 60 | 61 | %changelog 62 | * Thu Oct 17 2019 Stuart Gathman 2.0.14-1 63 | - Fix doctest for CNAME fixes to work with python and python3 64 | - Fix dnspython integration so that SPF TempError is properly raised when 65 | there are timeout or no nameserver errors 66 | - Restore DNSLookup API for pydnsv(DNS) for tcp fallback works again 67 | 68 | * Mon Jul 23 2018 Stuart Gathman 2.0.13-1 69 | - Add support for use of dnspython (dns) if installed 70 | - Catch ValueError due to improper IP address in connect IP or in ip4/ip6 71 | mechanisms 72 | - Fix for CNAME processing causing incorrect permerrors 73 | 74 | * Wed Aug 5 2015 Stuart Gathman 2.0.12-1 75 | - Reset void_lookups at top of check() to fix bogus permerror on best_guess() 76 | - Ignore permerror for best_guess() 77 | - Don't crash on null DNS TXT record (ignore): test case null-text 78 | - Trailing spaces are allowed by 4.5/2: test case trailing-space 79 | - Make CNAME loop result in unknown host: test case ptr-cname-loop 80 | - Test case and fix for mixed case CNAME loop, test case ptr-cname-loop 81 | 82 | * Fri Dec 5 2014 Stuart Gathman 2.0.11-1 83 | - Fix another bug in SPF record parsing that caused records with terms 84 | separated by multple spaces as invalid, but they are fine per the ABNF 85 | - Downcase names in additional answers returned by DNS before adding 86 | to cache, since case inconsistency can cause PTR match failures (initial 87 | patch thanks to Joni Fieggen) and other problems. 88 | 89 | * Tue Sep 2 2014 Stuart Gathman 2.0.10-1 90 | - Fix AAAA not flagged as bytes when strict=2 91 | - Split mechanisms by space only, not by whitespace 92 | - include '~' as safe char in url quoted macro expansion 93 | - treat AttributeError from pydns as TempError 94 | 95 | * Tue Apr 29 2014 Stuart Gathman 2.0.9-1 96 | - RFC7208 support 97 | - void lookup limit and test cases 98 | - Convert YAML tests to TestCases, and have testspf.py return success/fail. 99 | 100 | * Tue Jul 23 2013 Stuart Gathman 2.0.8-2 101 | - Test case and fix for PermError on non-ascii chars in non-SPF TXT records 102 | - Use ipaddr/ipaddress module in place of custom IP processing code 103 | - Numerous python3 compatibility fixes 104 | - Improved unicode error detection in SPF records 105 | - Fixed a bug caused by a null CNAME in cache 106 | 107 | * Fri Feb 03 2012 Stuart Gathman 2.0.7-1 108 | - fix CNAME chain duplicating TXT records 109 | - local test cases for CNAME chains 110 | - python3 compatibility changes e.g. print a -> print(a) 111 | - check for 7-bit ascii on TXT and SPF records 112 | - Use openspf.net for SPF web site instead of openspf.org 113 | - Support Authentication-Results header field 114 | - Support overall DNS timeout 115 | 116 | * Thu Oct 27 2011 Stuart Gathman 2.0.6-2 117 | - Python3 port (still requires 2to3 on spf.py) 118 | - Ensure Temperror for all DNS rcodes other than 0 and 3 per RFC 4408 119 | - Parse Received-SPF header 120 | - Report CIDR error only for valid mechanism 121 | - Handle invalid SPF record on command line 122 | - Add timeout to check2 123 | - Check for non-ascii policy 124 | 125 | * Wed Mar 03 2011 Stuart Gathman 2.0.6-1 126 | - Python-2.6 127 | - parse_header method 128 | 129 | * Wed Apr 02 2008 Stuart Gathman 2.0.5-1 130 | - Add timeout parameter to query ctor and DNSLookup 131 | - Patch from Scott Kitterman to retry truncated results with TCP unless harsh 132 | - Validate DNS labels 133 | - Reflect decision on empty-exp errata 134 | 135 | * Wed Jul 25 2007 Stuart Gathman 2.0.4-1 136 | - Correct unofficial 'best guess' processing. 137 | - PTR validation processing cleanup 138 | - Improved detection of exp= errors 139 | - Keyword args for get_header, minor fixes 140 | * Mon Jan 15 2007 Stuart Gathman 2.0.3-1 141 | - pyspf requires pydns, python-pyspf requires python-pydns 142 | - Record matching mechanism and add to Received-SPF header. 143 | - Test for RFC4408 6.2/4, and fix spf.py to comply. 144 | - Test for type SPF (type 99) by default in harsh mode only. 145 | - Permerror for more than one exp or redirect modifier. 146 | - Parse op= modifier 147 | * Sat Dec 30 2006 Stuart Gathman 2.0.2-1 148 | - Update openspf URLs 149 | - Update Readme to better describe available pyspf interfaces 150 | - Add basic description of type99.py and spfquery.py scripts 151 | - Add usage instructions for type99.py DNS RR type conversion script 152 | - Add spfquery.py usage instructions 153 | - Incorporate downstream feedback from Debian packager 154 | - Fix key-value quoting in get_header 155 | * Fri Dec 08 2006 Stuart Gathman 2.0.1-1 156 | - Prevent cache poisoning attack 157 | - Prevent malformed RR attack 158 | - Update license on a few files we missed last time 159 | * Mon Nov 20 2006 Stuart Gathman 2.0-1 160 | - Completed RFC 4408 compliance 161 | - Added spf.check2 for RFC 4408 compatible result codes 162 | - Full IP6 support 163 | - Fedora Core compatible RPM spec file 164 | - Update README, licenses 165 | * Tue Sep 26 2006 Stuart Gathman 1.8-1 166 | - YAML test suite syntax 167 | - trailing dot support (RFC4408 8.1) 168 | * Tue Aug 29 2006 Sean Reifschneider 1.7-1 169 | - Initial RPM spec file. 170 | -------------------------------------------------------------------------------- /pyspf_changelog.txt: -------------------------------------------------------------------------------- 1 | # Revision 1.108.2.152 2016/04/26 03:57:04 kitterma 2 | # * Set version and update changelog for 2.0.13 development. 3 | # 4 | # Revision 1.108.2.151 2016/04/26 03:53:04 kitterma 5 | # * Catch ValueError due to improper IP address in connect IP or in ip4/ip6 6 | # mechanisms 7 | # 8 | # Revision 1.108.2.150 2015/08/05 13:49:45 customdesigned 9 | # Forgot tabnanny 10 | # 11 | # Revision 1.108.2.149 2015/08/05 13:07:09 customdesigned 12 | # Release 2.0.12 13 | # 14 | # Revision 1.108.2.148 2015/08/05 04:49:48 customdesigned 15 | # Reset void_lookups at top of check() 16 | # 17 | # Revision 1.108.2.147 2015/08/05 03:36:59 customdesigned 18 | # Ignore permerror for best_guess 19 | # 20 | # Revision 1.108.2.146 2015/06/05 15:58:18 customdesigned 21 | # Don't crash on null TXT record. 22 | # 23 | # Revision 1.108.2.145 2015/01/14 20:27:42 customdesigned 24 | # Fix list feature 25 | # 26 | # Revision 1.108.2.144 2015/01/13 04:40:07 customdesigned 27 | # Trailing spaces *are* allowed by 4.5/2 28 | # 29 | # Revision 1.108.2.143 2015/01/12 22:51:56 customdesigned 30 | # Trailing space is PermError, but strip for extended result in lax mode. 31 | # 32 | # Revision 1.108.2.142 2015/01/06 14:13:50 customdesigned 33 | # Make CNAME loop result in unknown host. 34 | # 35 | # Revision 1.108.2.141 2015/01/02 01:08:18 customdesigned 36 | # Test case and fix for mixed case CNAME loop. 37 | # 38 | # Revision 1.108.2.140 2015/01/02 00:26:08 customdesigned 39 | # Make CNAME loop check case insensitive. 40 | # 41 | # Revision 1.108.2.139 2014/12/19 00:16:12 kitterma 42 | # Missed a spot bumping to 2.0.12. 43 | # 44 | # Revision 1.108.2.138 2014/12/19 00:15:12 kitterma 45 | # Bump versions, etc. to start 2.0.12 development. 46 | # 47 | # Revision 1.108.2.137 2014/12/13 15:39:27 customdesigned 48 | # Require ipaddress/ipaddr backport with Bytes for python2. 49 | # 50 | # Revision 1.108.2.136 2014/12/05 16:20:07 customdesigned 51 | # Release 2.0.11 52 | # 53 | # Revision 1.108.2.135 2014/12/03 01:11:09 customdesigned 54 | # Fold case of domain for all cache entries. 55 | # 56 | # Revision 1.108.2.134 2014/12/03 01:01:24 customdesigned 57 | # PTR case change fix with test case 58 | # 59 | # Revision 1.108.2.133 2014/10/06 11:54:11 kitterma 60 | # *** empty log message *** 61 | # 62 | # Revision 1.108.2.132 2014/10/06 11:51:03 kitterma 63 | # * Downcase IPv6 PTR results since case inconsistency can cause PTR match 64 | # failures (patch thanks to Joni Fieggen) 65 | # 66 | # Revision 1.108.2.131 2014/09/22 17:20:33 customdesigned 67 | # Update comments 68 | # 69 | # Revision 1.108.2.130 2014/09/22 17:13:53 customdesigned 70 | # Cleaner fix for multiple spaces. 71 | # 72 | # Revision 1.108.2.129 2014/09/21 21:11:47 kitterma 73 | # * Reset to start 2.0.11 development 74 | # * Fixed bug where multiple spaces between terms causes pyspf to think they 75 | # were unknown mechanisms 76 | # 77 | # Revision 1.108.2.128 2014/09/02 17:31:53 customdesigned 78 | # 79 | # Release 2.0.10 80 | # 81 | # Revision 1.108.2.127 2014/09/01 21:17:13 kitterma 82 | # Fix TempError handling of errors from the DNS module. 83 | # 84 | # Revision 1.108.2.126 2014/08/02 18:35:50 customdesigned 85 | # '~' is also an unreserved char in rfc7208. 86 | # 87 | # Revision 1.108.2.125 2014/08/02 04:36:48 kitterma 88 | # * Fix bug in SPF record parsing that caused all 'whitespace' characters to 89 | # be considered valid term separators and not just spaces 90 | # 91 | # Revision 1.108.2.124 2014/08/02 04:32:36 kitterma 92 | # Archive previous commit messages for spf.py in pyspf_changelog.txt and bumpi 93 | # version to 2.0.10 for start of follow on work. 94 | # 95 | # Revision 1.108.2.123 2014/07/30 18:41:18 customdesigned 96 | # Fix flagging AAAA records in dns_a. Add --strict option to CLI 97 | # 98 | # Revision 1.108.2.122 2014/04/29 22:56:48 customdesigned 99 | # Release 2.0.9 100 | # 101 | # Revision 1.108.2.121 2014/04/28 21:57:08 customdesigned 102 | # Ignore void lookups for explanation and type 99 lookup. 103 | # 104 | # Revision 1.108.2.120 2014/04/24 23:02:15 kitterma 105 | # Remove redundant check of self.void_lookups. 106 | # 107 | # Revision 1.108.2.119 2014/04/22 23:03:42 kitterma 108 | # Update CHANGELOG to prepare for release. 109 | # 110 | # Revision 1.108.2.118 2014/04/22 22:03:13 kitterma 111 | # Add processing for new void lookups processing limit. 112 | # 113 | # Revision 1.108.2.117 2014/04/22 20:54:42 kitterma 114 | # Adjust documentation of lookup limits to include RFC 7208 115 | # Add constants and variables for new void lookup limit 116 | # 117 | # Revision 1.108.2.116 2014/04/22 17:10:54 kitterma 118 | # Default SPF process timeout limit to 20 seconds per RFC 7208 4.6.4. 119 | # 120 | # Revision 1.108.2.115 2014/04/22 17:02:55 kitterma 121 | # Change default DNS timeout to 20 seconds in DNSLookup to better match RFC 122 | # 7208 4.6.4. 123 | # 124 | # Revision 1.108.2.114 2014/04/22 04:56:38 kitterma 125 | # Add permerror to permitted mx-limit results for rfc4408 to fudge changes for 126 | # RFC 7208. 127 | # 128 | # Revision 1.108.2.113 2014/04/22 04:46:58 kitterma 129 | # Make mx > 10 a permerror per RFC 7208 and mx-limit test. 130 | # 131 | # Revision 1.108.2.112 2014/01/20 22:16:38 customdesigned 132 | # Rename local var hiding str. 133 | # 134 | # Revision 1.108.2.111 2014/01/20 22:03:08 customdesigned 135 | # Test case and fix for more thorough macro syntax error detection. 136 | # 137 | # Revision 1.108.2.110 2013/07/25 21:21:49 kitterma 138 | # Archive previous commit messages for spf.py in pyspf_changelog.txt and bump version to 2.0.9 for start of follow on work. 139 | # 140 | # Revision 1.108.2.109 2013/07/25 01:51:24 customdesigned 141 | # Forgot to convert to bytes in py3dns-3.0.2 workaround. 142 | # 143 | # Revision 1.108.2.108 2013/07/25 01:29:07 customdesigned 144 | # The Final and Ultimate Solution to the String Problem for TXT records. 145 | # 146 | # Revision 1.108.2.107 2013/07/23 18:37:17 customdesigned 147 | # Removed decode from dns_txt again, as it breaks python3, both with py3dns and test framework. 148 | # Need to identify exact situation in which it is needed to put it back. 149 | # 150 | # Revision 1.108.2.106 2013/07/23 06:32:58 kitterma 151 | # Post fix cleanup. 152 | # 153 | # Revision 1.108.2.105 2013/07/23 06:30:13 kitterma 154 | # Fix compatibility with py3dns versions that return type bytes. 155 | # 156 | # Revision 1.108.2.104 2013/07/23 06:20:18 kitterma 157 | # Consolidate code related to UnicodeDecodeError and UnicodeEncodeError into UnicodeError. 158 | # 159 | # Revision 1.108.2.103 2013/07/23 06:07:24 customdesigned 160 | # Test case and fix for allowing non-ascii in non-spf TXT records. 161 | # 162 | # Revision 1.108.2.102 2013/07/23 05:22:54 customdesigned 163 | # Check for non-ascii on explanation. 164 | # 165 | # Revision 1.108.2.101 2013/07/23 04:51:59 customdesigned 166 | # Functional alias for __email__ 167 | # 168 | # Revision 1.108.2.100 2013/07/23 04:07:38 customdesigned 169 | # Sort unofficial keywords for consistent ordering. 170 | # 171 | # Revision 1.108.2.99 2013/07/23 02:40:54 customdesigned 172 | # Update __email__ and __author__ 173 | # 174 | # Revision 1.108.2.98 2013/07/23 02:35:33 customdesigned 175 | # Release 2.0.8 176 | # 177 | # Revision 1.108.2.97 2013/07/23 02:04:59 customdesigned 178 | # Release 2.0.8 179 | # 180 | # Revision 1.108.2.96 2013/07/22 22:59:58 kitterma 181 | # Give another header test it's own variable names. 182 | # 183 | # Revision 1.108.2.95 2013/07/22 19:29:22 kitterma 184 | # Fix dns_txt to work if DNS data is not pure bytes for python3 compatibility. 185 | # 186 | # Revision 1.108.2.94 2013/07/22 02:44:39 kitterma 187 | # Add tests for cirdmatch. 188 | # 189 | # Revision 1.108.2.93 2013/07/21 23:56:51 kitterma 190 | # Fix cidrmatch to work with both ipaddr and the python3.3 ipadrress versions of the module. 191 | # 192 | # Revision 1.108.2.91 2013/07/03 23:38:39 customdesigned 193 | # Removed two more unused functions. 194 | # 195 | # Revision 1.108.2.90 2013/07/03 22:58:26 customdesigned 196 | # Clean up use of ipaddress module. make %{i} upper case to match test suite 197 | # (test suite is incorrect requiring uppercase, but one thing at a time). 198 | # Remove no longer used inet_pton substitute. But what if someone was using it? 199 | # 200 | # Revision 1.108.2.89 2013/05/26 03:32:19 kitterma 201 | # Syntax fix to maintain python2.6 compatibility. 202 | # 203 | # Revision 1.108.2.88 2013/05/26 00:30:12 kitterma 204 | # Bump versions to 2.0.8 and add CHANGELOG entries. 205 | # 206 | # Revision 1.108.2.87 2013/05/26 00:23:52 kitterma 207 | # Move old (pre-2.0.7) spf.py commit messages to pyspf_changelog.txt. 208 | # 209 | # Revision 1.108.2.86 2013/05/25 22:39:19 kitterma 210 | # Use ipaddr/ipaddress instead of custome code. 211 | # 212 | # Revision 1.108.2.85 2013/05/25 00:06:03 kitterma 213 | # Fix return type detection for bytes/string for python3 compatibility in dns_txt. 214 | # 215 | # Revision 1.108.2.84 2013/04/20 20:49:13 customdesigned 216 | # Some dual-cidr doc tests 217 | # 218 | # Revision 1.108.2.83 2013/03/25 22:51:37 customdesigned 219 | # Replace dns_99 method with dns_txt(type='SPF') 220 | # Fix null CNAME in cache bug. 221 | # 222 | # Revision 1.108.2.82 2013/03/14 21:13:06 customdesigned 223 | # Fix Non-ascii exception description. 224 | # 225 | # Revision 1.108.2.81 2013/03/14 21:03:25 customdesigned 226 | # Fix dns_txt and dns_spf - should hopefully still be correct for python3. 227 | # 228 | # Revision 1.108.2.80 2012/06/14 20:09:56 kitterma 229 | # Use the correct exception type to capture unicode in SPF records. 230 | # 231 | # Revision 1.108.2.79 2012/03/10 00:19:44 kitterma 232 | # Add fixes for py3dns DNS return as type bytes - not complete. 233 | # 234 | # Revision 1.108.2.77 2012/02/09 22:13:42 kitterma 235 | # Fix stray character in last commit. 236 | # Start fixing python3 bytes issue - Now works, but fails the non-ASCII exp test. 237 | # 238 | # Revision 1.108.2.76 2012/02/05 05:50:39 kitterma 239 | # Fix a few stray print -> print() changes for python3 compatbility. 240 | # 241 | # Revision 1.108.2.75 2012/02/03 01:44:58 customdesigned 242 | # Fix CNAME duplicating DNS records. 243 | # Fix handling non-ascii chars in TXT/SPF records. 244 | # 245 | # Revision 1.108.2.74 2012/01/19 06:40:24 kitterma 246 | # * Accounts for new py3dns error classes coming in py3dns 3.0.2 (but fully 247 | # backward compatible with earlier versions) 248 | # 249 | # Revision 1.108.2.73 2012/01/19 06:22:35 kitterma 250 | # * Accept TXT and SPF type records back from py(3)dns and deal with them regardless of type (string or bytes. 251 | # * Update README 252 | # 253 | # Revision 1.108.2.72 2012/01/16 15:37:47 kitterma 254 | # Do away with default querytime, make it fully optional and by default completely backwards compatible. 255 | # 256 | # Revision 1.108.2.71 2012/01/16 06:19:31 kitterma 257 | # * Refactor timeout changes to improve backward comaptibility (see CHANGELOG). 258 | # 259 | # Revision 1.108.2.70 2012/01/13 04:21:19 kitterma 260 | # * Change timeouts to be global for all DNS lookups instead of per DNS lookup 261 | # to match processing limits recommendation in RFC 4408 10.1 262 | # - Default is 20 seconds for the global timer instead of 30 seconds per DNS 263 | # lookup 264 | # - This can be adjusted by changing spf.MAX_GLOBAL_TIME 265 | # 266 | # Revision 1.108.2.69 2012/01/10 06:13:18 kitterma 267 | # * Finish Python3 port - works with python2.6/2.7/3.2 and 2to3 is no longer 268 | # required. 269 | # 270 | # Revision 1.108.2.68 2012/01/10 05:56:16 kitterma 271 | # Update copyright years and fix date. 272 | # 273 | # Revision 1.108.2.67 2012/01/10 04:42:03 kitterma 274 | # * Rework query.parse_header: 275 | # - Make query.parse_header automatically select Received-DPF or 276 | # Authentication Results header types and use them to collect SPF 277 | # results from trusted relays 278 | # - Add query.parse_header_spf and query.parse_header_ar functions for 279 | # header type specific processing 280 | # * Add 'Programming Language :: Python3' to setup.py 281 | # * Bump release dates 282 | # 283 | # Revision 1.108.2.66 2012/01/10 00:17:09 kitterma 284 | # Fix authentication results support to provide similar comments as Received-SPF. 285 | # 286 | # Revision 1.108.2.65 2011/11/08 07:38:37 kitterma 287 | # Extend query.get_header to return either Received-SPF (still default) or 288 | # Authentication Results headers 289 | # 290 | # Revision 1.108.2.64 2011/11/08 05:11:56 kitterma 291 | # Add tests for query.get_header. 292 | # 293 | # Revision 1.108.2.63 2011/11/08 04:36:33 kitterma 294 | # Update CHANGELOG, setup.py, spf.py, and move old commit messages to 295 | # pyspf_changelog.txt to start on new version (2.0.7). 296 | # 297 | # Revision 1.108.2.62 2011/11/05 19:07:53 customdesigned 298 | # New website openspf.org -> openspf.net 299 | # 300 | # Revision 1.108.2.61 2011/10/27 16:29:38 customdesigned 301 | # Move python version test to def time. 302 | # 303 | # Revision 1.108.2.60 2011/10/27 16:28:18 kitterma 304 | # Use bytes in to_ascii to work in python and python3. 305 | # 306 | # Revision 1.108.2.59 2011/10/27 14:50:05 customdesigned 307 | # Ensure entire SPF policy is ascii. 308 | # 309 | # Revision 1.108.2.58 2011/10/27 14:29:49 customdesigned 310 | # Catch non-ascii domains. 311 | # 312 | # Revision 1.108.2.57 2011/10/27 10:32:06 kitterma 313 | # Drop version from spf.py shebang. 314 | # 315 | # Revision 1.108.2.56 2011/10/27 04:58:03 kitterma 316 | # Update CHANGELOG, adjust minimum version requirement in setup.py, and update dates for a release. 317 | # 318 | # Revision 1.108.2.55 2011/10/27 03:49:11 kitterma 319 | # Fix doctests to raise ... as ... and print(x) as 2to3 doesn't fix these. 320 | # Doctests all pass in 2.6, 2.7, and 3.2 321 | # 322 | # Revision 1.108.2.53 2011/10/18 02:56:32 kitterma 323 | # Resolve local conflicts in spf.py changelog. 324 | # 325 | # Revision 1.108.2.52 2011/10/04 23:08:18 customdesigned 326 | # verbose option 327 | # 328 | # Revision 1.108.2.51 2011/03/06 03:54:01 kitterma 329 | # Update copyright years. 330 | # 331 | # Revision 1.108.2.50 2011/03/06 03:14:54 kitterma 332 | # Wrangle types around so addr2bin tests pass with python2.4/2.6/3.2(with 2to3). 333 | # 334 | # Revision 1.108.2.49 2011/03/05 23:10:55 kitterma 335 | # Fix one missed instance of reverting to the older doctest with error type. 336 | # 337 | # Revision 1.108.2.48 2011/03/05 18:00:46 kitterma 338 | # Fix typo. 339 | # 340 | # Revision 1.108.2.47 2011/03/05 18:00:15 kitterma 341 | # Try to import both email.message and email.Message for backward compatibility. 342 | # 343 | # Revision 1.108.2.46 2011/03/05 17:37:57 kitterma 344 | # Revert to older doctest construct for python2.4/2.5 compatibility and set minimum version to 2.4. 345 | # 346 | # Revision 1.108.2.45 2011/03/03 04:14:31 kitterma 347 | # * Refactor spf.py to support python3 via 2to3 - Minimum Python version is now python2.6. 348 | # * Update README and CHANGELOG 349 | # 350 | # Revision 1.108.2.44 2011/02/11 18:25:31 kitterma 351 | # Move older spf.py commit messages to pyspf_changelog.txt and update version numbers. 352 | # 353 | # Revision 1.108.2.43 2011/02/11 18:17:47 kitterma 354 | # Ensure an error is raise for all DNS rcodes other than 0 and 3 per RFC 4408. 355 | # 356 | # Revision 1.108.2.42 2011/02/11 18:14:22 kitterma 357 | # Make TCP fallback an AmbiguityWarning in strict mode rather than an 358 | # error in harsh mode so we can retry and validate the TCP based record. 359 | # 360 | # Revision 1.108.2.41 2010/08/19 01:18:08 customdesigned 361 | # Return extra keyword dict from parse_header, parse identity. 362 | # 363 | # Revision 1.108.2.40 2010/04/29 20:23:44 customdesigned 364 | # Return result from parse_header 365 | # 366 | # Revision 1.108.2.39 2010/04/29 18:53:38 customdesigned 367 | # Parse Received-SPF header 368 | # 369 | # Revision 1.108.2.38 2010/04/29 16:36:47 customdesigned 370 | # report CIDR error only for valid mechanism 371 | # 372 | # Revision 1.108.2.37 2008/11/11 18:43:42 customdesigned 373 | # Make doc tests run on 2.5. Heuristic for missing IP4. 374 | # 375 | # Revision 1.108.2.36 2008/09/10 00:46:45 customdesigned 376 | # Test case for handling invalid SPF on command line. 377 | # 378 | # Revision 1.108.2.35 2008/09/10 00:35:03 customdesigned 379 | # Handle invalid SPF record on command line. 380 | # 381 | # Revision 1.108.2.34 2008/08/25 17:58:07 customdesigned 382 | # Add timeout to check2. 383 | # 384 | # Revision 1.108.2.33 2008/04/23 21:00:42 customdesigned 385 | # Quote nulls in Received-SPF. 386 | # 387 | # Revision 1.108.2.32 2008/04/23 20:03:53 customdesigned 388 | # Add timeout keyword to query constructor and DNSLookup. 389 | # 390 | # Revision 1.108.2.31 2008/03/27 01:15:33 customdesigned 391 | # Improve valid DNS name check. 392 | # 393 | # Revision 1.108.2.30 2008/03/27 00:58:15 customdesigned 394 | # Check dns names before DNSLookup 395 | # 396 | # Revision 1.108.2.29 2008/03/26 15:08:20 kitterma 397 | # Fix commit log typo. 398 | # 399 | # Revision 1.108.2.28 2008/03/26 14:45:37 kitterma 400 | # Update built in tests for Python2.5 (addr2bin will now fail slightly with older 401 | # Python versions). SF #1655736 402 | # 403 | # Revision 1.108.2.27 2008/03/26 14:34:35 kitterma 404 | # Change shebangs to #!/usr/bin/python throughout. 405 | # 406 | # Revision 1.108.2.26 2008/03/26 14:31:04 kitterma 407 | # Patch from Debian to avoid crash if command line SPF record request returns 408 | # TempError or PermError. 409 | # 410 | # Revision 1.108.2.25 2008/03/26 14:26:19 kitterma 411 | # Update for new version (working on 2.0.5) and year. 412 | # 413 | # Revision 1.108.2.24 2008/03/24 21:33:22 customdesigned 414 | # Patch from Scott Kitterman to retry truncated results with TCP unless 415 | # in harsh mode. 416 | # 417 | # Revision 1.108.2.23 2007/11/28 19:48:37 customdesigned 418 | # Reflect decision on empty-exp errata. 419 | # 420 | # Revision 1.108.2.22 2007/06/23 20:17:09 customdesigned 421 | # Don't try to include null (None) keyword values. 422 | # 423 | # Revision 1.108.2.21 2007/03/29 19:38:03 customdesigned 424 | # Remove trailing ';' again, fix Received-SPF tests. 425 | # 426 | # Revision 1.108.2.20 2007/03/27 20:54:22 customdesigned 427 | # Correct Received-SPF header format. 428 | # 429 | # Revision 1.108.2.19 2007/03/17 19:07:01 customdesigned 430 | # For default modifier, return ambiguous in harsh mode, ignore in strict mode, 431 | # follow in lax mode. 432 | # 433 | # Revision 1.108.2.18 2007/03/17 18:25:38 customdesigned 434 | # Default modifier is obsolete. Retab (expandtab) spf.py 435 | # 436 | # Revision 1.108.2.17 2007/03/13 20:13:16 customdesigned 437 | # Missing parentheses. 438 | # 439 | # Revision 1.108.2.16 2007/01/25 20:50:13 kitterma 440 | # Update versions to reflect working on 2.0.4 now. 441 | # 442 | # Revision 1.108.2.15 2007/01/19 23:23:50 customdesigned 443 | # Fix validated_ptrs and best_guess 444 | # 445 | # Revision 1.108.2.14 2007/01/17 01:01:00 customdesigned 446 | # Merge latest test suite fixes. 447 | # 448 | # 449 | # Revision 1.108.2.13 2007/01/15 19:14:27 customdesigned 450 | # Permerror for more than one exp= or redirect= 451 | # 452 | # Revision 1.132 2007/01/17 00:47:17 customdesigned 453 | # Test for and fix illegal implicit mechanisms. 454 | # 455 | # Revision 1.131 2007/01/16 23:54:58 customdesigned 456 | # Test and fix for invalid domain-spec. 457 | # 458 | # Revision 1.130 2007/01/15 02:21:10 customdesigned 459 | # Forget op= on redirect. 460 | # 461 | # Revision 1.108.2.12 2007/01/13 18:45:33 customdesigned 462 | # Record matching mechanism. 463 | # 464 | # Revision 1.108.2.11 2007/01/13 18:21:41 customdesigned 465 | # Test for RFC4408 6.2/4, and fix spf.py to comply. 466 | # 467 | # Revision 1.123 2007/01/11 18:49:37 customdesigned 468 | # Add mechanism to Received-SPF header. 469 | # 470 | # Revision 1.122 2007/01/11 18:25:54 customdesigned 471 | # Record matching mechanism. 472 | # 473 | # Revision 1.108.2.10 2007/01/13 00:46:35 kitterma 474 | # Update copyright statements for new year. 475 | # 476 | # Revision 1.108.2.9 2007/01/12 22:14:56 kitterma 477 | # Change DNS queries to only check Type SPF in Harsh mode 478 | # 479 | # Revision 1.108.2.8 2007/01/06 22:58:21 kitterma 480 | # Update changelogs and version to reflect 2.0.2 released and 2.0.3 started. 481 | # 482 | # Revision 1.108.2.7 2007/01/06 21:03:15 customdesigned 483 | # Tested spf.py in python2.2. 484 | # 485 | # Version 2.0.2 released. 486 | # 487 | # Revision 1.108.2.6 2006/12/30 17:12:50 customdesigned 488 | # Merge fixes from CVS HEAD. 489 | # 490 | # Revision 1.108.2.5 2006/12/24 19:10:38 kitterma 491 | # Move spf.py changelog to CHANGELOG. Move spf.py cvs commits from previous 492 | # releases to py_spfchangelog.txt. Update README to describe provided scripts. 493 | # Add to README discussion of spf module interface. 494 | # 495 | # Revision 1.108.2.4 2006/12/23 06:35:37 customdesigned 496 | # Fully quote structured values in Received-SPF. 497 | # 498 | # Revision 1.108.2.3 2006/12/23 04:44:05 customdesigned 499 | # Fix key-value quoting in get_header. 500 | # 501 | # Revision 1.121 2006/12/30 17:01:52 customdesigned 502 | # Missed a spot for new result names. 503 | # 504 | # Revision 1.120 2006/12/28 04:54:21 customdesigned 505 | # Skip optional trailing ";" in Received-SPF 506 | # 507 | # Revision 1.118 2006/12/28 04:04:27 customdesigned 508 | # Optimize get_header to remove useless key-value pairs. 509 | # 510 | # Revision 1.117 2006/12/23 06:31:16 customdesigned 511 | # Fully quote values in key-value pairs. 512 | # 513 | # Revision 1.108.2.2 2006/12/22 20:27:24 customdesigned 514 | # Index error reporting non-mech permerror. 515 | # 516 | # Revision 1.108.2.1 2006/12/22 04:59:40 customdesigned 517 | # Merge comma heuristic. 518 | 519 | # Revision 1.108 2006/11/08 01:27:00 customdesigned 520 | # Return all key-value-pairs in Received-SPF header for all results. 521 | # 522 | # Revision 1.107 2006/11/04 21:58:12 customdesigned 523 | # Prevent cache poisoning by bogus additional RRs in PTR DNS response. 524 | # 525 | # Revision 1.106 2006/10/16 20:48:24 customdesigned 526 | # More DOS limit tests. 527 | # 528 | # Revision 1.105 2006/10/07 22:06:28 kitterma 529 | # Pass strict status to DNSLookup - will be needed for TCP failover. 530 | # 531 | # Revision 1.104 2006/10/07 21:59:37 customdesigned 532 | # long/empty label tests and fix. 533 | # 534 | # Revision 1.103 2006/10/07 18:16:20 customdesigned 535 | # Add tests for and fix RE_TOPLAB. 536 | # 537 | # Revision 1.102 2006/10/05 13:57:15 customdesigned 538 | # Remove isSPF and make missing space after version tag a warning. 539 | # 540 | # Revision 1.101 2006/10/05 13:39:11 customdesigned 541 | # SPF version tag is case insensitive. 542 | # 543 | # Revision 1.100 2006/10/04 02:14:04 customdesigned 544 | # Remove incomplete saving of result. Was messing up bmsmilter. Would 545 | # be useful if done consistently - and disabled when passing spf= to check(). 546 | # 547 | # Revision 1.99 2006/10/03 21:00:26 customdesigned 548 | # Correct fat fingered merge error. 549 | # 550 | # Revision 1.98 2006/10/03 17:35:45 customdesigned 551 | # Provide python inet_ntop and inet_pton when not socket.has_ipv6 552 | # 553 | # Revision 1.97 2006/10/02 17:10:13 customdesigned 554 | # Test and fix for uppercase macros. 555 | # 556 | # Revision 1.96 2006/10/01 01:27:54 customdesigned 557 | # Switch to pymilter lax processing convention: 558 | # Always return strict result, extended result in q.perm_error.ext 559 | # 560 | # Revision 1.95 2006/09/30 22:53:44 customdesigned 561 | # Fix getp to obey SHOULDs in RFC. 562 | # 563 | # Revision 1.94 2006/09/30 22:23:25 customdesigned 564 | # p macro tests and fixes 565 | # 566 | # Revision 1.93 2006/09/30 20:57:06 customdesigned 567 | # Remove generator expression for compatibility with python2.3. 568 | # 569 | # Revision 1.92 2006/09/30 19:52:52 customdesigned 570 | # Removed redundant flag and unneeded global. 571 | # 572 | # Revision 1.91 2006/09/30 19:37:49 customdesigned 573 | # Missing L 574 | # 575 | # Revision 1.90 2006/09/30 19:29:58 customdesigned 576 | # pydns returns AAAA RR as binary string 577 | # 578 | # Revision 1.89 2006/09/29 20:23:11 customdesigned 579 | # Optimize cidrmatch 580 | # 581 | # Revision 1.88 2006/09/29 19:44:10 customdesigned 582 | # Fix ptr with ip6 for harsh mode. 583 | # 584 | # Revision 1.87 2006/09/29 19:26:53 customdesigned 585 | # Add PTR tests and fix ip6 ptr 586 | # 587 | # Revision 1.86 2006/09/29 17:55:22 customdesigned 588 | # Pass ip6 tests 589 | # 590 | # Revision 1.85 2006/09/29 15:58:02 customdesigned 591 | # Pass self test on non IP6 python. 592 | # PTR accepts no cidr. 593 | # 594 | # Revision 1.83 2006/09/27 18:09:40 kitterma 595 | # Converted spf.check to return pre-MARID result codes for drop in 596 | # compatibility with pySPF 1.6/1.7. Added new procedure, spf.check2 to 597 | # return RFC4408 results in a two part answer (result, explanation). 598 | # This is the external API for pySPF 2.0. No longer any need to branch 599 | # for 'classic' and RFC compliant pySPF libraries. 600 | # 601 | # Revision 1.82 2006/09/27 18:02:21 kitterma 602 | # Converted max MX limit to ambiguity warning for validator. 603 | # 604 | # Revision 1.81 2006/09/27 17:38:14 kitterma 605 | # Updated initial comments and moved pre-1.7 changes to spf_changelog. 606 | # 607 | # Revision 1.80 2006/09/27 17:33:53 kitterma 608 | # Fixed indentation error in check0. 609 | # 610 | # Revision 1.79 2006/09/26 18:05:44 kitterma 611 | # Removed unused receiver policy definitions. 612 | # 613 | # Revision 1.78 2006/09/26 16:15:50 kitterma 614 | # added additional IP4 and CIDR validation tests - no code changes. 615 | # 616 | # Revision 1.77 2006/09/25 19:42:32 customdesigned 617 | # Fix unknown macro sentinel 618 | # 619 | # Revision 1.76 2006/09/25 19:10:40 customdesigned 620 | # Fix exp= error and add another failing test. 621 | # 622 | # Revision 1.75 2006/09/25 02:02:30 kitterma 623 | # Fixed redirect-cancels-exp test suite failure. 624 | # 625 | # Revision 1.74 2006/09/24 04:04:08 kitterma 626 | # Implemented check for macro 'c' - Macro unimplimented. 627 | # 628 | # Revision 1.73 2006/09/24 02:08:35 kitterma 629 | # Fixed invalid-macro-char test failure. 630 | # 631 | # Revision 1.72 2006/09/23 05:45:52 kitterma 632 | # Fixed domain-name-truncation test failure 633 | # 634 | # Revision 1.71 2006/09/22 01:02:54 kitterma 635 | # pySPF correction for nolocalpart in rfc4408-tests.yml failed, 4.3/2. 636 | # Added comments to testspf.py on where to get YAML. 637 | # 638 | # Revision 1.70 2006/09/18 02:13:27 kitterma 639 | # Worked through a large number of pylint issues - all 4 spaces, not a mix 640 | # of 4 spaces, 2 spaces, and tabs. Caught a few minor errors in the process. 641 | # All built in tests still pass. 642 | # 643 | # Revision 1.69 2006/09/17 18:44:25 kitterma 644 | # Fixed validation mode only crash bug when rDNS check had no PTR record 645 | # 646 | # Revision 1.68 2006/09/01 23:56:43 customdesigned 647 | # Fix improved RE_IP6 648 | # 649 | # Revision 1.67 2006/09/01 23:27:56 customdesigned 650 | # Improved RE_IP6 651 | # 652 | # Revision 1.66 2006/09/01 22:16:41 customdesigned 653 | # Parse IP6 for RFC conformance. 654 | # 655 | # Revision 1.65 2006/08/31 18:00:18 customdesigned 656 | # Fix dual-cidr-length parsing. 657 | # 658 | # Revision 1.64 2006/08/30 17:54:23 customdesigned 659 | # Fix dual-cidr. 660 | # 661 | # Revision 1.63 2006/07/28 01:53:03 customdesigned 662 | # Localhost shouldn't get automatic pass 663 | # 664 | # Revision 1.62 2006/07/27 03:56:45 customdesigned 665 | # Removed redundant trailing dot check. 666 | # 667 | # Revision 1.61 2006/07/26 21:40:19 customdesigned 668 | # YAML test format. Accept trailing dot on domains. 669 | # 670 | # Revision 1.60 2006/06/28 04:25:38 customdesigned 671 | # Catch unexpected IO errors from pydns. 672 | # 673 | # Revision 1.59 2006/05/19 13:18:23 kitterma 674 | # Fix to disallow ':' except between the mechanism and domain-spec. 675 | # 676 | # Revision 1.58 2006/05/19 02:04:58 kitterma 677 | # Corrected validation bug where 'all' mechanism was not correctly checked, 678 | # updated for RFC 4408 Auth 48 changes - trailing dot now allowed in domain 679 | # name and Type TXT and Type SPF DNS records not identical raises a warning 680 | # instead of a permanent error, and changed internet draft references to refer 681 | # to RFC 4408. 682 | # 683 | # Revision 1.57 2006/05/12 16:38:12 customdesigned 684 | # a:1.2.3.4 -> ip4:1.2.3.4 heuristic. 685 | # 686 | # Revision 1.56 2005/12/29 19:14:11 customdesigned 687 | # Handle NULL MX and other A lookups of DNS root. 688 | # 689 | # Revision 1.55 2005/10/30 00:41:48 customdesigned 690 | # Ignore SPF records missing space after version as required by RFC. 691 | # FIXME: in "relaxed" mode, give permerror when there is exactly one 692 | # such malformed record. 693 | # 694 | # Revision 1.54 2005/08/23 21:50:10 customdesigned 695 | # Missing separator line in insert_libspf_local_policy self test. 696 | # 697 | # Revision 1.53 2005/08/23 20:37:19 customdesigned 698 | # Simplify libspf_local further. FIXME for possible specification error. 699 | # 700 | # Revision 1.52 2005/08/23 20:23:31 customdesigned 701 | # Clean up libspf_local and add inline test cases. 702 | # Repair try..finally in check1() broken when Ambiguity warning added. 703 | # 704 | # Revision 1.51 2005/08/19 19:06:49 customdesigned 705 | # use note_error method for consistent extended processing. 706 | # Return extended result, strict result in self.perm_error 707 | # 708 | # Revision 1.50 2005/08/19 18:13:31 customdesigned 709 | # Still want to do strict tests in even stricter modes. 710 | # 711 | # Revision 1.49 2005/08/12 18:54:34 kitterma 712 | # Consistently treat strict as a numeric for hard processing. 713 | # 714 | # Revision 1.48 2005/08/11 14:30:44 kitterma 715 | # Restore all numeric TLD test from 1.44 that was inadvertently deleted. Ugh. 716 | # 717 | # Revision 1.47 2005/08/10 13:31:34 kitterma 718 | # Completed first part of local policy implementation. Local policy will now be 719 | # added before the last non-fail mechanism as in Libspf2 and Mail::SPF::Query. 720 | # Still ToDo for local policy is: don't do local policy until after redirect=, 721 | # modify explanation to indicate result is based on local policy, and an option 722 | # for RFE [ 1224459 ] local policy API to execute local policy before public 723 | # policy. Will do the RFE after basic compatibility with the reference 724 | # implementations. Restored Unix line endings. Changed Harsh mode check for 725 | # ambiguity to exclude exists: mechanisms. 726 | # 727 | # Revision 1.46 2005/08/08 15:03:28 kitterma 728 | # Added PermError for redirect= to a domain without an SPF record. 729 | # 730 | # Revision 1.45 2005/08/08 03:04:44 kitterma 731 | # Added PermError for multiple SPF records per para 4.5 of schlitt-02 732 | # 733 | # Revision 1.44 2005/08/06 06:31:21 kitterma 734 | # Added RFC 3696 test for all numeric TLD, new PermError. 735 | # 736 | # Revision 1.43 2005/08/02 12:57:02 kitterma 737 | # Removed extraneous debugging print statement. 738 | # 739 | # Revision 1.42 2005/07/28 21:03:24 kitterma 740 | # Added ambiguity check for no A records returned for a mechanism when harsh. 741 | # 742 | # Revision 1.41 2005/07/28 18:26:14 kitterma 743 | # Added AmbiguityWarning error class for harsh processing (validator). 744 | # Added ambiguous result tests for more than 10 MX or PTR returned. 745 | # Added AmbiguityWarning for mx mechanisms that return no MX records. 746 | # Created new result called ambiguous for use with harsh processing. 747 | # 748 | # Revision 1.40 2005/07/28 04:25:45 kitterma 749 | # Clean up modifier RE to match current ABNF. Added test example for this. 750 | # Fixed missing space in one test/example. 751 | # 752 | # Revision 1.39 2005/07/28 03:56:13 kitterma 753 | # Restore three part API (res, code, txt). 754 | # Add dictionary to support local policy checks in future updates. 755 | # Add record for trusted-forwarder.org - support future TFWL checks. 756 | # 757 | # Revision 1.38 2005/07/26 14:11:12 kitterma 758 | # Added check to PermError if SPF record has no spaces 759 | # 760 | # Revision 1.37 2005/07/26 06:12:19 customdesigned 761 | # Use ABNF derived RE for IP4. IP6 RE is way ugly... 762 | # 763 | # Revision 1.36 2005/07/26 05:59:38 customdesigned 764 | # Validate ip4 address format. 765 | # 766 | # Revision 1.35 2005/07/26 05:23:24 customdesigned 767 | # Fix stupid typo in RE_CIDR 768 | # 769 | # Revision 1.34 2005/07/23 17:58:02 customdesigned 770 | # Put new result codes in unit tests. 771 | # 772 | # Revision 1.33 2005/07/22 18:23:28 kitterma 773 | # *** Breaks external API. Only returns SPF result now. Up to the calling 774 | # module to determine the MTA result codes from that. Also, internally support 775 | # the newer PermError/TempError convention. 776 | # 777 | # Revision 1.32 2005/07/22 17:45:20 kitterma 778 | # Converted TempError to look like PermError processing 779 | # 780 | # Revision 1.31 2005/07/22 02:11:50 customdesigned 781 | # Use dictionary to check for CNAME loops. Check limit independently for 782 | # each top level name, just like for PTR. 783 | # 784 | # Revision 1.30 2005/07/21 20:07:31 customdesigned 785 | # Translate DNS error in DNSLookup. This completely isolates DNS 786 | # dependencies to the DNSLookup method. 787 | # 788 | # Revision 1.29 2005/07/21 17:49:39 customdesigned 789 | # My best guess at what RFC intended for limiting CNAME loops. 790 | # 791 | # Revision 1.28 2005/07/21 17:37:08 customdesigned 792 | # Break out external DNSLookup method so that test suite can 793 | # duplicate CNAME loop bug. Test zone data dictionary now 794 | # mirrors structure of real DNS. 795 | # 796 | # Revision 1.27 2005/07/21 15:26:06 customdesigned 797 | # First cut at updating docs. Test suite is obsolete. 798 | # 799 | # Revision 1.26 2005/07/20 03:12:40 customdesigned 800 | # When not in strict mode, don't give PermErr for bad mechanism until 801 | # encountered during evaluation. 802 | # 803 | # Revision 1.25 2005/07/19 23:24:42 customdesigned 804 | # Validate all mechanisms before evaluating. 805 | # 806 | # Revision 1.24 2005/07/19 18:11:52 kitterma 807 | # Fix to change that compares type TXT and type SPF records. Bug in the change 808 | # prevented records from being returned if it was published as TXT, but not SPF. 809 | # 810 | # Revision 1.23 2005/07/19 15:22:50 customdesigned 811 | # MX and PTR limits are MUST NOT check limits, and do not result in PermErr. 812 | # Also, check belongs in mx and ptr specific methods, not in dns() method. 813 | # 814 | # Revision 1.22 2005/07/19 05:02:29 customdesigned 815 | # FQDN test was broken. Added test case. Move FQDN test to after 816 | # macro expansion. 817 | # 818 | # Revision 1.21 2005/07/18 20:46:27 kitterma 819 | # Fixed reference problem in 1.20 820 | # 821 | # Revision 1.20 2005/07/18 20:21:47 kitterma 822 | # Change to dns_spf to go ahead and check for a type 99 (SPF) record even if a 823 | # TXT record is found and make sure if type SPF is present that they are 824 | # identical when using strict processing. 825 | # 826 | # Revision 1.19 2005/07/18 19:36:00 kitterma 827 | # Change to require at least one dot in a domain name. Added PermError 828 | # description to indicate FQDN should be used. This is a common error. 829 | # 830 | # Revision 1.18 2005/07/18 17:13:37 kitterma 831 | # Change macro processing to raise PermError on an unknown macro. 832 | # schlitt-spf-classic-02 para 8.1. Change exp modifier processing to ignore 833 | # exp strings with syntax errors. schlitt-spf-classic-02 para 6.2. 834 | # 835 | # Revision 1.17 2005/07/18 14:35:34 customdesigned 836 | # Remove debugging printf 837 | # 838 | # Revision 1.16 2005/07/18 14:34:14 customdesigned 839 | # Forgot to remove debugging print 840 | # 841 | # Revision 1.15 2005/07/15 21:17:36 customdesigned 842 | # Recursion limit raises AssertionError in strict mode, PermError otherwise. 843 | # 844 | # Revision 1.14 2005/07/15 20:34:11 customdesigned 845 | # Check whether DNS package already supports SPF before patching 846 | # 847 | # Revision 1.13 2005/07/15 20:01:22 customdesigned 848 | # Allow extended results for MX limit 849 | # 850 | # Revision 1.12 2005/07/15 19:12:09 customdesigned 851 | # Official IANA SPF record (type 99) support. 852 | # 853 | # Revision 1.11 2005/07/15 18:03:02 customdesigned 854 | # Fix unknown Received-SPF header broken by result changes 855 | # 856 | # Revision 1.10 2005/07/15 16:17:05 customdesigned 857 | # Start type99 support. 858 | # Make Scott's "/" support in parse_mechanism more elegant as requested. 859 | # Add test case for "/" support. 860 | # 861 | # Revision 1.9 2005/07/15 03:33:14 kitterma 862 | # Fix for bug 1238403 - Crash if non-CIDR / present. Also added 863 | # validation check for valid IPv4 CIDR range. 864 | # 865 | # Revision 1.8 2005/07/14 04:18:01 customdesigned 866 | # Bring explanations and Received-SPF header into line with 867 | # the unknown=PermErr and error=TempErr convention. 868 | # Hope my case-sensitive mech fix doesn't clash with Scotts. 869 | # 870 | # Revision 1.7 2005/07/12 21:43:56 kitterma 871 | # Added processing to clarify some cases of unknown 872 | # qualifier errors (to distinguish between unknown qualifier and 873 | # unknown mechanism). 874 | # Also cleaned up comments from previous updates. 875 | # 876 | # Revision 1.6 2005/06/29 14:46:26 customdesigned 877 | # Distinguish trivial recursion from missing arg for diagnostic purposes. 878 | # 879 | # Revision 1.5 2005/06/28 17:48:56 customdesigned 880 | # Support extended processing results when a PermError should strictly occur. 881 | # 882 | # Revision 1.4 2005/06/22 15:54:54 customdesigned 883 | # Correct spelling. 884 | # 885 | # Revision 1.3 2005/06/22 00:08:24 kitterma 886 | # Changes from draft-mengwong overall DNS lookup and recursion 887 | # depth limits to draft-schlitt-spf-classic-02 DNS lookup, MX lookup, and 888 | # PTR lookup limits. Recursion code is still present and functioning, but 889 | # it should be impossible to trip it. 890 | # 891 | # Revision 1.2 2005/06/21 16:46:09 kitterma 892 | # Updated definition of SPF, added reference to the sourceforge project site, 893 | # and deleted obsolete Microsoft Caller ID for Email XML translation routine. 894 | # 895 | # Revision 1.1.1.1 2005/06/20 19:57:32 customdesigned 896 | # Move Python SPF to its own module. 897 | # 898 | # Revision 1.5 2005/06/14 20:31:26 customdesigned 899 | # fix pychecker nits 900 | # 901 | # Revision 1.4 2005/06/02 04:18:55 customdesigned 902 | # Update copyright notices after reading article on /. 903 | # 904 | # Revision 1.3 2005/06/02 02:08:12 customdesigned 905 | # Reject on PermErr 906 | # 907 | # Revision 1.2 2005/05/31 18:57:59 customdesigned 908 | # Clear unknown mechanism list at proper time. 909 | # 910 | # Revision 1.24 2005/03/16 21:58:39 stuart 911 | # Change Milter module to package. 912 | # 913 | # Revision 1.22 2005/02/09 17:52:59 stuart 914 | # Report DNS errors as PermError rather than unknown. 915 | # 916 | # Revision 1.21 2004/11/20 16:37:03 stuart 917 | # Handle multi-segment TXT records. 918 | # 919 | # Revision 1.20 2004/11/19 06:10:30 stuart 920 | # Use PermError exception instead of reporting unknown. 921 | # 922 | # Revision 1.19 2004/11/09 23:00:18 stuart 923 | # Limit recursion and DNS lookups separately. 924 | # 925 | # 926 | # Revision 1.17 2004/09/10 18:08:26 stuart 927 | # Return unknown for null mechanism 928 | # 929 | # Revision 1.16 2004/09/04 23:27:06 stuart 930 | # More mechanism aliases. 931 | # 932 | # Revision 1.15 2004/08/30 21:19:05 stuart 933 | # Return unknown for invalid ip syntax in mechanism 934 | # 935 | # Revision 1.14 2004/08/23 02:28:24 stuart 936 | # Remove Perl usage message. 937 | # 938 | # Revision 1.13 2004/07/23 19:23:12 stuart 939 | # Always fail to match on ip6, until we support it properly. 940 | # 941 | # Revision 1.12 2004/07/23 18:48:15 stuart 942 | # Fold CID parsing into spf 943 | # 944 | # Revision 1.11 2004/07/21 21:32:01 stuart 945 | # Handle CID records (Microsoft XML format). 946 | # 947 | # Revision 1.10 2004/04/19 22:12:11 stuart 948 | # Release 0.6.9 949 | # 950 | # Revision 1.9 2004/04/18 03:29:35 stuart 951 | # Pass most tests except -local and -rcpt-to 952 | # 953 | # Revision 1.8 2004/04/17 22:17:55 stuart 954 | # Header comment method. 955 | # 956 | # Revision 1.7 2004/04/17 18:22:48 stuart 957 | # Support default explanation. 958 | # 959 | # Revision 1.6 2004/04/06 20:18:02 stuart 960 | # Fix bug in include 961 | # 962 | # Revision 1.5 2004/04/05 22:29:46 stuart 963 | # SPF best_guess 964 | # 965 | # Revision 1.4 2004/03/25 03:27:34 stuart 966 | # Support delegation of SPF records. 967 | # 968 | # Revision 1.3 2004/03/13 12:23:23 stuart 969 | # Expanded result codes. Tolerate common method misspellings. 970 | # 971 | # Development taken over by Stuart Gathman 972 | # 973 | # 18-dec-2003, v1.6, Failures on Intel hardware: endianness. Use ! on 974 | # struct.pack(), struct.unpack(). 975 | # 17-dec-2003, v1.5, ttw use socket.inet_aton() instead of DNS.addr2bin, so 976 | # n, n.n, and n.n.n forms for IPv4 addresses work, and to 977 | # ditch the annoying Python 2.4 FutureWarning 978 | # 13-dec-2003, v1.3, ttw added %{o} original domain macro, 979 | # print spf result on command line, support default=, 980 | # support localhost, follow DNS CNAMEs, cache DNS results 981 | # during query, support Python 2.2 for Mac OS X 982 | # 16-dec-2003, v1.4, ttw fixed include handling (include is a mechanism, 983 | # complete with status results, so -include: should work. 984 | # Expand macros AFTER looking for status characters ?-+ 985 | # so altavista.com SPF records work. 986 | # 11-dec-2003, v1.2, ttw added macro expansion, exp=, and redirect= 987 | # 9-dec-2003, v1.1, Meng Weng Wong added PTR code, THANK YOU 988 | 989 | 990 | 991 | 992 | -------------------------------------------------------------------------------- /python-pyspf.spec: -------------------------------------------------------------------------------- 1 | %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} 2 | 3 | Name: python-pyspf 4 | Version: 2.0.14 5 | Release: 1%{?dist} 6 | Summary: Python module and programs for SPF (Sender Policy Framework). 7 | 8 | Group: Development/Languages 9 | License: Python Software Foundation License 10 | URL: http://sourceforge.net/forum/forum.php?forum_id=596908 11 | Source0: pyspf-%{version}.tar.gz 12 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 13 | 14 | BuildArch: noarch 15 | BuildRequires: python-setuptools python-devel 16 | Requires: python-pydns 17 | Requires: python-pydns 18 | Requires: python >= 2.6 19 | Requires: python-authres python-ipaddr >= 2.1.10 20 | # Provide pyspf *only* if not using pyspf package for non-default python 21 | Provides: pyspf 22 | 23 | %description 24 | SPF does email sender validation. For more information about SPF, 25 | please see http://open-spf.org 26 | 27 | This SPF client is intended to be installed on the border MTA, checking 28 | if incoming SMTP clients are permitted to send mail. The SPF check 29 | should be done during the MAIL FROM:<...> command. 30 | 31 | %define namewithoutpythonprefix %(echo %{name} | sed 's/^python-//') 32 | %prep 33 | %setup -q -n %{namewithoutpythonprefix}-%{version} 34 | 35 | 36 | %build 37 | %{__python} setup.py build 38 | 39 | 40 | %install 41 | rm -rf $RPM_BUILD_ROOT 42 | %{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT 43 | mv $RPM_BUILD_ROOT/usr/bin/type99.py $RPM_BUILD_ROOT/usr/bin/type99 44 | mv $RPM_BUILD_ROOT/usr/bin/spfquery.py $RPM_BUILD_ROOT/usr/bin/spfquery 45 | rm -f $RPM_BUILD_ROOT/usr/bin/*.py{o,c} 46 | 47 | 48 | %clean 49 | rm -rf $RPM_BUILD_ROOT 50 | 51 | 52 | %files 53 | %defattr(-,root,root,-) 54 | %doc CHANGELOG PKG-INFO README test 55 | %{python_sitelib}/spf.py* 56 | /usr/bin/type99 57 | /usr/bin/spfquery 58 | /usr/lib/python2.6/site-packages/pyspf-%{version}-py2.6.egg-info 59 | 60 | %changelog 61 | * Thu Oct 17 2019 Stuart Gathman 2.0.14-1 62 | - Fix doctest for CNAME fixes to work with python and python3 63 | - Fix dnspython integration so that SPF TempError is properly raised when 64 | there are timeout or no nameserver errors 65 | - Restore DNSLookup API for pydnsv(DNS) for tcp fallback works again 66 | 67 | * Mon Jul 23 2018 Stuart Gathman 2.0.13-1 68 | - Add support for use of dnspython (dns) if installed 69 | - Catch ValueError due to improper IP address in connect IP or in ip4/ip6 70 | mechanisms 71 | - Fix for CNAME processing causing incorrect permerrors 72 | 73 | * Wed Aug 5 2015 Stuart Gathman 2.0.12-1 74 | - Reset void_lookups at top of check() to fix bogus permerror on best_guess() 75 | - Ignore permerror for best_guess() 76 | - Don't crash on null DNS TXT record (ignore): test case null-text 77 | - Trailing spaces are allowed by 4.5/2: test case trailing-space 78 | - Make CNAME loop result in unknown host: test case ptr-cname-loop 79 | - Test case and fix for mixed case CNAME loop, test case ptr-cname-loop 80 | 81 | * Fri Dec 5 2014 Stuart Gathman 2.0.11-1 82 | - Fix another bug in SPF record parsing that caused records with terms 83 | separated by multple spaces as invalid, but they are fine per the ABNF 84 | - Downcase names in additional answers returned by DNS before adding 85 | to cache, since case inconsistency can cause PTR match failures (initial 86 | patch thanks to Joni Fieggen) and other problems. 87 | 88 | * Tue Sep 2 2014 Stuart Gathman 2.0.10-1 89 | - Fix AAAA not flagged as bytes when strict=2 90 | - Split mechanisms by space only, not by whitespace 91 | - include '~' as safe char in url quoted macro expansion 92 | - treat AttributeError from pydns as TempError 93 | 94 | * Tue Apr 29 2014 Stuart Gathman 2.0.9-1 95 | - RFC7208 support 96 | - void lookup limit and test cases 97 | - Convert YAML tests to TestCases, and have testspf.py return success/fail. 98 | 99 | * Tue Jul 23 2013 Stuart Gathman 2.0.8-2 100 | - Test case and fix for PermError on non-ascii chars in non-SPF TXT records 101 | - Use ipaddr/ipaddress module in place of custom IP processing code 102 | - Numerous python3 compatibility fixes 103 | - Improved unicode error detection in SPF records 104 | - Fixed a bug caused by a null CNAME in cache 105 | 106 | * Fri Feb 03 2012 Stuart Gathman 2.0.7-1 107 | - fix CNAME chain duplicating TXT records 108 | - local test cases for CNAME chains 109 | - python3 compatibility changes e.g. print a -> print(a) 110 | - check for 7-bit ascii on TXT and SPF records 111 | - Use openspf.net for SPF web site instead of openspf.org 112 | - Support Authentication-Results header field 113 | - Support overall DNS timeout 114 | 115 | * Thu Oct 27 2011 Stuart Gathman 2.0.6-1 116 | - Python3 port (still requires 2to3 on spf.py) 117 | - Ensure Temperror for all DNS rcodes other than 0 and 3 per RFC 4408 118 | - Parse Received-SPF header 119 | - Report CIDR error only for valid mechanism 120 | - Handle invalid SPF record on command line 121 | - Add timeout to check2 122 | - Check for non-ascii policy 123 | - parse_header method 124 | - python2.6 125 | 126 | * Wed Apr 02 2008 Stuart Gathman 2.0.5-1 127 | - Add timeout parameter to query ctor and DNSLookup 128 | - Patch from Scott Kitterman to retry truncated results with TCP unless harsh 129 | - Validate DNS labels 130 | - Reflect decision on empty-exp errata 131 | 132 | * Wed Jul 25 2007 Stuart Gathman 2.0.4-1 133 | - Correct unofficial 'best guess' processing. 134 | - PTR validation processing cleanup 135 | - Improved detection of exp= errors 136 | - Keyword args for get_header, minor fixes 137 | 138 | * Mon Jan 15 2007 Stuart Gathman 2.0.3-1 139 | - pyspf requires pydns, python-pyspf requires python-pydns 140 | - Record matching mechanism and add to Received-SPF header. 141 | - Test for RFC4408 6.2/4, and fix spf.py to comply. 142 | - Test for type SPF (type 99) by default in harsh mode only. 143 | - Permerror for more than one exp or redirect modifier. 144 | - Parse op= modifier 145 | 146 | * Sat Dec 30 2006 Stuart Gathman 2.0.2-1 147 | - Update openspf URLs 148 | - Update Readme to better describe available pyspf interfaces 149 | - Add basic description of type99.py and spfquery.py scripts 150 | - Add usage instructions for type99.py DNS RR type conversion script 151 | - Add spfquery.py usage instructions 152 | - Incorporate downstream feedback from Debian packager 153 | - Fix key-value quoting in get_header 154 | 155 | * Fri Dec 08 2006 Stuart Gathman 2.0.1-1 156 | - Prevent cache poisoning attack 157 | - Prevent malformed RR attack 158 | - Update license on a few files we missed last time 159 | 160 | * Mon Nov 20 2006 Stuart Gathman 2.0-1 161 | - Completed RFC 4408 compliance 162 | - Added spf.check2 for RFC 4408 compatible result codes 163 | - Full IP6 support 164 | - Fedora Core compatible RPM spec file 165 | - Update README, licenses 166 | 167 | * Tue Sep 26 2006 Stuart Gathman 1.8-1 168 | - YAML test suite syntax 169 | - trailing dot support (RFC4408 8.1) 170 | 171 | * Tue Aug 29 2006 Sean Reifschneider 1.7-1 172 | - Initial RPM spec file. 173 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from setuptools import setup 4 | import sys 5 | 6 | DESC = """SPF (Sender Policy Framework) implemented in Python.""" 7 | with open("README.md", "r") as fh: 8 | LONG_DESC = fh.read() 9 | 10 | try: 11 | import dns 12 | from dns import version 13 | # dnspython minimum version is for timeout support 14 | if (version.MAJOR, version.MINOR) >= (1,16): 15 | if sys.version_info[0] == 2: 16 | install_req = ['dnspython>=1.16.0', 'authres', 'ipaddress'] 17 | else: 18 | install_req = ['dnspython>=1.16.0', 'authres'] 19 | # dnspython not present in sufficient version, so require PyDNS 20 | elif sys.version_info[0] == 2: 21 | install_req = ['PyDNS', 'authres', 'ipaddress'] 22 | else: 23 | install_req = ['Py3DNS', 'authres'] 24 | except ImportError: # If dnspython is not installed, require PyDNS 25 | if sys.version_info[0] == 2: 26 | install_req = ['PyDNS', 'authres', 'ipaddress'] 27 | else: 28 | install_req = ['Py3DNS', 'authres'] 29 | 30 | setup(name='pyspf', 31 | version='2.1.0', 32 | description=DESC, 33 | long_description=LONG_DESC, 34 | long_description_content_type="text/markdown", 35 | author='Terence Way', 36 | author_email='terry@wayforward.net', 37 | maintainer="Stuart D. Gathman", 38 | maintainer_email="stuart@gathman.org", 39 | url='https://github.com/sdgathman/pyspf/', 40 | license='Python Software Foundation License', 41 | py_modules=['spf'], 42 | keywords = ['spf','email','forgery'], 43 | scripts = ['type99.py','spfquery.py'], 44 | include_package_data=True, 45 | zip_safe = False, 46 | install_requires=install_req, 47 | classifiers = [ 48 | 'Development Status :: 5 - Production/Stable', 49 | 'Environment :: No Input/Output (Daemon)', 50 | 'Intended Audience :: Developers', 51 | 'License :: OSI Approved :: Python Software Foundation License', 52 | 'Natural Language :: English', 53 | 'Operating System :: OS Independent', 54 | 'Programming Language :: Python', 55 | 'Programming Language :: Python :: 3', 56 | 'Topic :: Communications :: Email :: Mail Transport Agents', 57 | 'Topic :: Communications :: Email :: Filters', 58 | 'Topic :: Internet :: Name Service (DNS)', 59 | 'Topic :: Software Development :: Libraries :: Python Modules' 60 | ] 61 | ) 62 | 63 | if sys.version_info < (2, 6): 64 | raise Exception("pyspf 2.0.6 and later requires at least python2.6.") 65 | -------------------------------------------------------------------------------- /spfquery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Author: Stuart D. Gathman 4 | # Copyright 2004 Business Management Systems, Inc. 5 | 6 | # This module is free software, and you may redistribute it and/or modify 7 | # it under the same terms as Python itself, so long as this copyright message 8 | # and disclaimer are retained in their original form. 9 | 10 | # Emulate the spfquery command line tool used by Wayne Schlitt's SPF test suite 11 | 12 | # $Log$ 13 | # Revision 1.4.2.3 2011/10/27 04:44:58 kitterma 14 | # Update spfquery.py to work with 2.6, 2.7, and 3.2: 15 | # - raise ... as ... 16 | # - print() 17 | # 18 | # Revision 1.4.2.2 2008/03/26 14:34:35 kitterma 19 | # Change shebangs to #!/usr/bin/python throughout. 20 | # 21 | # Revision 1.4.2.1 2006/12/23 05:31:22 kitterma 22 | # Minor updates for packaging lessons learned from Ubuntu 23 | # 24 | # Revision 1.4 2006/11/20 18:39:41 customdesigned 25 | # Change license on spfquery.py. Update README. Move tests to test directory. 26 | # 27 | # Revision 1.3 2005/07/22 02:11:57 customdesigned 28 | # Use dictionary to check for CNAME loops. Check limit independently for 29 | # each top level name, just like for PTR. 30 | # 31 | # Revision 1.2 2005/07/14 04:18:01 customdesigned 32 | # Bring explanations and Received-SPF header into line with 33 | # the unknown=PermErr and error=TempErr convention. 34 | # Hope my case-sensitive mech fix doesn't clash with Scotts. 35 | # 36 | # Revision 1.1.1.1 2005/06/20 19:57:32 customdesigned 37 | # Move Python SPF to its own module. 38 | # 39 | # Revision 1.2 2005/06/02 04:18:55 customdesigned 40 | # Update copyright notices after reading article on /. 41 | # 42 | # Revision 1.1.1.1 2005/05/31 18:07:19 customdesigned 43 | # Release 0.6.9 44 | # 45 | # Revision 2.3 2004/04/19 22:12:11 stuart 46 | # Release 0.6.9 47 | # 48 | # Revision 2.2 2004/04/18 03:29:35 stuart 49 | # Pass most tests except -local and -rcpt-to 50 | # 51 | # Revision 2.1 2004/04/08 18:41:15 stuart 52 | # Reject numeric hello names 53 | # 54 | # Driver for SPF test system 55 | 56 | import spf 57 | import sys 58 | 59 | from optparse import OptionParser 60 | 61 | class PerlOptionParser(OptionParser): 62 | def _process_args (self, largs, rargs, values): 63 | """_process_args(largs : [string], 64 | rargs : [string], 65 | values : Values) 66 | 67 | Process command-line arguments and populate 'values', consuming 68 | options and arguments from 'rargs'. If 'allow_interspersed_args' is 69 | false, stop at the first non-option argument. If true, accumulate any 70 | interspersed non-option arguments in 'largs'. 71 | """ 72 | while rargs: 73 | arg = rargs[0] 74 | # We handle bare "--" explicitly, and bare "-" is handled by the 75 | # standard arg handler since the short arg case ensures that the 76 | # len of the opt string is greater than 1. 77 | if arg == "--": 78 | del rargs[0] 79 | return 80 | elif arg[0:2] == "--": 81 | # process a single long option (possibly with value(s)) 82 | self._process_long_opt(rargs, values) 83 | elif arg[:1] == "-" and len(arg) > 1: 84 | # process a single perl style long option 85 | rargs[0] = '-' + arg 86 | self._process_long_opt(rargs, values) 87 | elif self.allow_interspersed_args: 88 | largs.append(arg) 89 | del rargs[0] 90 | else: 91 | return 92 | 93 | def format(q): 94 | res,code,txt = q.check() 95 | print(res) 96 | if res in ('pass','neutral','unknown'): print() 97 | else: print(txt) 98 | print('spfquery:',q.get_header_comment(res)) 99 | print('Received-SPF:',q.get_header(res,'spfquery')) 100 | 101 | def main(argv): 102 | parser = PerlOptionParser() 103 | parser.add_option("--file",dest="file") 104 | parser.add_option("--ip",dest="ip") 105 | parser.add_option("--sender",dest="sender") 106 | parser.add_option("--helo",dest="hello_name") 107 | parser.add_option("--local",dest="local_policy") 108 | parser.add_option("--rcpt-to",dest="rcpt") 109 | parser.add_option("--default-explanation",dest="explanation") 110 | parser.add_option("--sanitize",type="int",dest="sanitize") 111 | parser.add_option("--debug",type="int",dest="debug") 112 | opts,args = parser.parse_args(argv) 113 | if opts.ip: 114 | q = spf.query(opts.ip,opts.sender,opts.hello_name,local=opts.local_policy) 115 | if opts.explanation: 116 | q.set_default_explanation(opts.explanation) 117 | format(q) 118 | if opts.file: 119 | if opts.file == '0': 120 | fp = sys.stdin 121 | else: 122 | fp = open(opts.file,'r') 123 | for ln in fp: 124 | ip,sender,helo,rcpt = ln.split(None,3) 125 | q = spf.query(ip,sender,helo,local=opts.local_policy) 126 | if opts.explanation: 127 | q.set_default_explanation(opts.explanation) 128 | format(q) 129 | fp.close() 130 | 131 | if __name__ == "__main__": 132 | import sys 133 | main(sys.argv[1:]) 134 | -------------------------------------------------------------------------------- /test/doctest.yml: -------------------------------------------------------------------------------- 1 | # Zonedata for doctests 2 | zonedata: 3 | example.net: 4 | - A: 192.0.32.10 5 | _exp.controlledmail.com: 6 | - TXT: Controlledmail.com does not send mail from itself. 7 | _spf.controlledmail.com: 8 | - TXT: v=spf1 ip4:72.81.252.18 ip4:72.81.252.19 ip4:208.43.65.50 ip6:2607:f0d0:3001:00aa:0000:0000:0000:0002 -all 9 | controlledmail.com: 10 | - TXT: v=spf1 redirect=_spf.controlledmail.com 11 | parallel.kitterman.org: 12 | - TXT: v=spf1 include:long.kitterman.org include:cname.kitterman.org -all 13 | a.example.org: 14 | - TXT: "Another TXT record." 15 | - SPF: "v=spf1 ip4:192.0.2.225 ?include:webmail.pair.com ?include:relay.pair.com -all" 16 | - TXT: "More TXT records." 17 | - TXT: "A third TXT record." 18 | - AAAA: 2001:db8:ff0:300::4 19 | b.example.org: 20 | - CNAME: "a.example.org" 21 | parallel.example.org: 22 | - SPF: "v=spf1 include:a.example.org include:b.example.org -all" 23 | - A: 192.0.2.28 24 | webmail.pair.com: 25 | - TXT: "v=spf1 ip4:66.39.3.0/24 ip4:209.68.6.94/32" 26 | relay.pair.com: 27 | - TXT: "v=spf1 ip4:209.68.5.9/32 ip4:209.68.5.15/32 a -all" 28 | - A: 192.0.2.131 29 | -------------------------------------------------------------------------------- /test/rfc4408-tests.LICENSE: -------------------------------------------------------------------------------- 1 | The RFC 4408 test-suite (rfc4408-tests.yml) is 2 | (C) 2006-2007 Stuart D Gathman 3 | 2007 Julian Mehnle 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 1. Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 3. The names of the authors may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 18 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 22 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /test/rfc4408-tests.yml: -------------------------------------------------------------------------------- 1 | # This is the openspf.org test suite (release 2009.10) based on RFC 4408. 2 | # http://www.openspf.org/Test_Suite 3 | # 4 | # $Id$ 5 | # vim:sw=2 sts=2 et 6 | # 7 | # See rfc4408-tests.CHANGES for a changelog. 8 | # 9 | # Contributors: 10 | # Stuart D Gathman 90% of the tests 11 | # Julian Mehnle some tests, proofread YAML syntax, formal schema 12 | # Frank Ellermann 13 | # Scott Kitterman 14 | # Wayne Schlitt 15 | # Craig Whitmore 16 | # Norman Maurer 17 | # Mark Shewmaker 18 | # Philip Gladstone 19 | # 20 | # While the test suite is designed for all types of implementations, it only 21 | # needs to explicitly concern itself with SPF-only (type 99) and TXT-only 22 | # implementations. This is because while an implementation may support both, 23 | # it must use only one record type for a given query - see 4.5/5. If an 24 | # implementation finds SPF (type 99) records and decides to use them, they 25 | # override TXT, and it must ignore any TXT records. Note that an 26 | # implementation may decide whether to use SPF records on a case by case basis. 27 | # Maybe it looks TXT and SPF up in parallel and goes with the first result to 28 | # come back. Or maybe one is cached already. Or maybe it chooses at random. 29 | # Think of dual SPF/TXT implementations as a quantum superposition of SPF-only 30 | # and TXT-only. It must collapse to one or the other on each observation to be 31 | # compliant. 32 | # 33 | # The "Selecting records" test section is the only one concerned with weeding 34 | # out (incorrect) mixed behaviour and checking for proper response to duplicate 35 | # or conflicting records. Other sections rely on auto-magic duplication 36 | # of SPF to TXT records (by test suite drivers) to test all implementation 37 | # types with one specification. 38 | # 39 | --- 40 | description: Initial processing 41 | tests: 42 | toolonglabel: 43 | description: >- 44 | DNS labels limited to 63 chars. 45 | comment: >- 46 | For initial processing, a long label results in None, not TempError 47 | spec: 4.3/1 48 | helo: mail.example.net 49 | host: 1.2.3.5 50 | mailfrom: lyme.eater@A123456789012345678901234567890123456789012345678901234567890123.example.com 51 | result: none 52 | longlabel: 53 | description: >- 54 | DNS labels limited to 63 chars. 55 | spec: 4.3/1 56 | helo: mail.example.net 57 | host: 1.2.3.5 58 | mailfrom: lyme.eater@A12345678901234567890123456789012345678901234567890123456789012.example.com 59 | result: fail 60 | emptylabel: 61 | spec: 4.3/1 62 | helo: mail.example.net 63 | host: 1.2.3.5 64 | mailfrom: lyme.eater@A...example.com 65 | result: none 66 | helo-not-fqdn: 67 | spec: 4.3/1 68 | helo: A2345678 69 | host: 1.2.3.5 70 | mailfrom: "" 71 | result: none 72 | helo-domain-literal: 73 | spec: 4.3/1 74 | helo: "[1.2.3.5]" 75 | host: 1.2.3.5 76 | mailfrom: "" 77 | result: none 78 | nolocalpart: 79 | spec: 4.3/2 80 | helo: mail.example.net 81 | host: 1.2.3.4 82 | mailfrom: '@example.net' 83 | result: fail 84 | explanation: postmaster 85 | domain-literal: 86 | spec: 4.3/1 87 | helo: OEMCOMPUTER 88 | host: 1.2.3.5 89 | mailfrom: "foo@[1.2.3.5]" 90 | result: none 91 | non-ascii-policy: 92 | description: >- 93 | SPF policies are restricted to 7-bit ascii. 94 | spec: 3.1.1/1 95 | helo: hosed 96 | host: 1.2.3.4 97 | mailfrom: "foobar@hosed.example.com" 98 | result: permerror 99 | non-ascii-mech: 100 | description: >- 101 | SPF policies are restricted to 7-bit ascii. 102 | comment: >- 103 | Checking a possibly different code path for non-ascii chars. 104 | spec: 3.1.1/1 105 | helo: hosed 106 | host: 1.2.3.4 107 | mailfrom: "foobar@hosed2.example.com" 108 | result: permerror 109 | non-ascii-result: 110 | description: >- 111 | SPF policies are restricted to 7-bit ascii. 112 | comment: >- 113 | Checking yet another code path for non-ascii chars. 114 | spec: 3.1.1/1 115 | helo: hosed 116 | host: 1.2.3.4 117 | mailfrom: "foobar@hosed3.example.com" 118 | result: permerror 119 | non-ascii-non-spf: 120 | description: >- 121 | Non-ascii content in non-SPF related records. 122 | comment: >- 123 | Non-SPF related TXT records are none of our business. (But what about SPF records?) 124 | spec: 3.1.1/1 125 | helo: hosed 126 | host: 1.2.3.4 127 | mailfrom: "foobar@nothosed.example.com" 128 | result: fail 129 | explanation: DEFAULT 130 | two-spaces: 131 | description: >- 132 | ABNF for term separation is one or more spaces, not just one. 133 | spec: 4.6.1 134 | helo: hosed 135 | host: 1.2.3.4 136 | mailfrom: "actually@fine.example.com" 137 | result: fail 138 | zonedata: 139 | example.com: 140 | - TIMEOUT 141 | example.net: 142 | - SPF: v=spf1 -all exp=exp.example.net 143 | a.example.net: 144 | - SPF: v=spf1 -all exp=exp.example.net 145 | exp.example.net: 146 | - TXT: '%{l}' 147 | a12345678901234567890123456789012345678901234567890123456789012.example.com: 148 | - SPF: v=spf1 -all 149 | hosed.example.com: 150 | - SPF: "v=spf1 a:\xEF\xBB\xBFgarbage.example.net -all" 151 | hosed2.example.com: 152 | - SPF: "v=spf1 \x80a:example.net -all" 153 | hosed3.example.com: 154 | - SPF: "v=spf1 a:example.net \x96all" 155 | nothosed.example.com: 156 | - SPF: "v=spf1 a:example.net -all" 157 | - SPF: "\x96" 158 | fine.example.com: 159 | - TXT: "v=spf1 a -all" 160 | --- 161 | description: Record lookup 162 | tests: 163 | both: 164 | spec: 4.4/1 165 | helo: mail.example.net 166 | host: 1.2.3.4 167 | mailfrom: foo@both.example.net 168 | result: fail 169 | txtonly: 170 | description: Result is none if checking SPF records only. 171 | spec: 4.4/1 172 | helo: mail.example.net 173 | host: 1.2.3.4 174 | mailfrom: foo@txtonly.example.net 175 | result: [fail, none] 176 | spfonly: 177 | description: Result is none if checking TXT records only. 178 | spec: 4.4/1 179 | helo: mail.example.net 180 | host: 1.2.3.4 181 | mailfrom: foo@spfonly.example.net 182 | result: [fail, none] 183 | spftimeout: 184 | description: >- 185 | TXT record present, but SPF lookup times out. 186 | Result is temperror if checking SPF records only. 187 | comment: >- 188 | This actually happens for a popular braindead DNS server. 189 | spec: 4.4/1 190 | helo: mail.example.net 191 | host: 1.2.3.4 192 | mailfrom: foo@spftimeout.example.net 193 | result: [fail, temperror] 194 | txttimeout: 195 | description: >- 196 | SPF record present, but TXT lookup times out. 197 | If only TXT records are checked, result is temperror. 198 | spec: 4.4/1 199 | helo: mail.example.net 200 | host: 1.2.3.4 201 | mailfrom: foo@txttimeout.example.net 202 | result: [fail, temperror] 203 | nospftxttimeout: 204 | description: >- 205 | No SPF record present, and TXT lookup times out. 206 | If only TXT records are checked, result is temperror. 207 | comment: >- 208 | Because TXT records is where v=spf1 records will likely be, returning 209 | temperror will try again later. A timeout due to a braindead server 210 | is unlikely in the case of TXT, as opposed to the newer SPF RR. 211 | spec: 4.4/1 212 | helo: mail.example.net 213 | host: 1.2.3.4 214 | mailfrom: foo@nospftxttimeout.example.net 215 | result: [temperror, none] 216 | alltimeout: 217 | description: Both TXT and SPF queries time out 218 | spec: 4.4/2 219 | helo: mail.example.net 220 | host: 1.2.3.4 221 | mailfrom: foo@alltimeout.example.net 222 | result: temperror 223 | zonedata: 224 | both.example.net: 225 | - TXT: v=spf1 -all 226 | - SPF: v=spf1 -all 227 | txtonly.example.net: 228 | - TXT: v=spf1 -all 229 | spfonly.example.net: 230 | - SPF: v=spf1 -all 231 | - TXT: NONE 232 | spftimeout.example.net: 233 | - TXT: v=spf1 -all 234 | - TIMEOUT 235 | txttimeout.example.net: 236 | - SPF: v=spf1 -all 237 | - TXT: NONE 238 | - TIMEOUT 239 | nospftxttimeout.example.net: 240 | - SPF: "v=spf3 !a:yahoo.com -all" 241 | - TXT: NONE 242 | - TIMEOUT 243 | alltimeout.example.net: 244 | - TIMEOUT 245 | --- 246 | description: Selecting records 247 | tests: 248 | nospace1: 249 | description: >- 250 | Version must be terminated by space or end of record. TXT pieces 251 | are joined without intervening spaces. 252 | spec: 4.5/4 253 | helo: mail.example1.com 254 | host: 1.2.3.4 255 | mailfrom: foo@example2.com 256 | result: none 257 | empty: 258 | description: Empty SPF record. 259 | spec: 4.5/4 260 | helo: mail1.example1.com 261 | host: 1.2.3.4 262 | mailfrom: foo@example1.com 263 | result: neutral 264 | nospace2: 265 | spec: 4.5/4 266 | helo: mail.example1.com 267 | host: 1.2.3.4 268 | mailfrom: foo@example3.com 269 | result: pass 270 | spfoverride: 271 | description: >- 272 | SPF records override TXT records. Older implementation may 273 | check TXT records only. 274 | spec: 4.5/5 275 | helo: mail.example1.com 276 | host: 1.2.3.4 277 | mailfrom: foo@example4.com 278 | result: [pass, fail] 279 | multitxt1: 280 | description: >- 281 | Older implementations will give permerror/unknown because of 282 | the conflicting TXT records. However, RFC 4408 says the SPF 283 | records overrides them. 284 | spec: 4.5/5 285 | helo: mail.example1.com 286 | host: 1.2.3.4 287 | mailfrom: foo@example5.com 288 | result: [pass, permerror] 289 | multitxt2: 290 | description: >- 291 | Multiple records is a permerror, v=spf1 is case insensitive 292 | spec: 4.5/6 293 | helo: mail.example1.com 294 | host: 1.2.3.4 295 | mailfrom: foo@example6.com 296 | result: permerror 297 | multispf1: 298 | description: >- 299 | Multiple records is a permerror, even when they are identical. 300 | However, this situation cannot be reliably reproduced with live 301 | DNS since cache and resolvers are allowed to combine identical 302 | records. 303 | spec: 4.5/6 304 | helo: mail.example1.com 305 | host: 1.2.3.4 306 | mailfrom: foo@example7.com 307 | result: [permerror, fail] 308 | multispf2: 309 | description: >- 310 | Older implementations ignoring SPF-type records will give pass because 311 | there is a (single) TXT record. But RFC 4408 requires permerror because 312 | the SPF records override and there are more than one. 313 | spec: 4.5/6 314 | helo: mail.example1.com 315 | host: 1.2.3.4 316 | mailfrom: foo@example8.com 317 | result: [permerror, pass] 318 | nospf: 319 | spec: 4.5/7 320 | helo: mail.example1.com 321 | host: 1.2.3.4 322 | mailfrom: foo@mail.example1.com 323 | result: none 324 | case-insensitive: 325 | description: >- 326 | v=spf1 is case insensitive 327 | spec: 4.5/6 328 | helo: mail.example1.com 329 | host: 1.2.3.4 330 | mailfrom: foo@example9.com 331 | result: softfail 332 | zonedata: 333 | example3.com: 334 | - SPF: v=spf10 335 | - SPF: v=spf1 mx 336 | - MX: [0, mail.example1.com] 337 | example1.com: 338 | - SPF: v=spf1 339 | example2.com: 340 | - SPF: ['v=spf1', 'mx'] 341 | mail.example1.com: 342 | - A: 1.2.3.4 343 | example4.com: 344 | - SPF: v=spf1 +all 345 | - TXT: v=spf1 -all 346 | example5.com: 347 | - SPF: v=spf1 +all 348 | - TXT: v=spf1 -all 349 | - TXT: v=spf1 +all 350 | example6.com: 351 | - SPF: v=spf1 -all 352 | - SPF: V=sPf1 +all 353 | example7.com: 354 | - SPF: v=spf1 -all 355 | - SPF: v=spf1 -all 356 | example8.com: 357 | - SPF: V=spf1 -all 358 | - SPF: v=spf1 -all 359 | - TXT: v=spf1 +all 360 | example9.com: 361 | - SPF: v=SpF1 ~all 362 | --- 363 | description: Record evaluation 364 | tests: 365 | detect-errors-anywhere: 366 | description: Any syntax errors anywhere in the record MUST be detected. 367 | spec: 4.6 368 | helo: mail.example.com 369 | host: 1.2.3.4 370 | mailfrom: foo@t1.example.com 371 | result: permerror 372 | modifier-charset-good: 373 | description: name = ALPHA *( ALPHA / DIGIT / "-" / "_" / "." ) 374 | spec: 4.6.1/2 375 | helo: mail.example.com 376 | host: 1.2.3.4 377 | mailfrom: foo@t2.example.com 378 | result: pass 379 | modifier-charset-bad1: 380 | description: >- 381 | '=' character immediately after the name and before any ":" or "/" 382 | spec: 4.6.1/4 383 | helo: mail.example.com 384 | host: 1.2.3.4 385 | mailfrom: foo@t3.example.com 386 | result: permerror 387 | modifier-charset-bad2: 388 | description: >- 389 | '=' character immediately after the name and before any ":" or "/" 390 | spec: 4.6.1/4 391 | helo: mail.example.com 392 | host: 1.2.3.4 393 | mailfrom: foo@t4.example.com 394 | result: permerror 395 | redirect-after-mechanisms1: 396 | description: >- 397 | The "redirect" modifier has an effect after all the mechanisms. 398 | comment: >- 399 | The redirect in this example would violate processing limits, except 400 | that it is never used because of the all mechanism. 401 | spec: 4.6.3 402 | helo: mail.example.com 403 | host: 1.2.3.4 404 | mailfrom: foo@t5.example.com 405 | result: softfail 406 | redirect-after-mechanisms2: 407 | description: >- 408 | The "redirect" modifier has an effect after all the mechanisms. 409 | spec: 4.6.3 410 | helo: mail.example.com 411 | host: 1.2.3.5 412 | mailfrom: foo@t6.example.com 413 | result: fail 414 | default-result: 415 | description: Default result is neutral. 416 | spec: 4.7/1 417 | helo: mail.example.com 418 | host: 1.2.3.5 419 | mailfrom: foo@t7.example.com 420 | result: neutral 421 | redirect-is-modifier: 422 | description: |- 423 | Invalid mechanism. Redirect is a modifier. 424 | spec: 4.6.1/4 425 | helo: mail.example.com 426 | host: 1.2.3.4 427 | mailfrom: foo@t8.example.com 428 | result: permerror 429 | invalid-domain: 430 | description: >- 431 | Domain-spec must end in macro-expand or valid toplabel. 432 | spec: 8.1/2 433 | helo: mail.example.com 434 | host: 1.2.3.4 435 | mailfrom: foo@t9.example.com 436 | result: permerror 437 | invalid-domain-empty-label: 438 | description: >- 439 | target-name that is a valid domain-spec per RFC 4408 but an invalid 440 | domain name per RFC 1035 (empty label) must be treated as non-existent. 441 | comment: >- 442 | An empty domain label, i.e. two successive dots, in a mechanism 443 | target-name is valid domain-spec syntax, even though a DNS query cannot 444 | be composed from it. The spec being unclear about it, this could either 445 | be considered a syntax error, or, by analogy to 4.3/1 and 5/10/3, the 446 | mechanism chould be treated as a no-match. 447 | spec: [4.3/1, 5/10/3] 448 | helo: mail.example.com 449 | host: 1.2.3.4 450 | mailfrom: foo@t10.example.com 451 | result: [permerror, fail] 452 | invalid-domain-long: 453 | description: >- 454 | target-name that is a valid domain-spec per RFC 4408 but an invalid 455 | domain name per RFC 1035 (long label) must be treated as non-existent. 456 | comment: >- 457 | A domain label longer than 63 characters in a mechanism target-name is 458 | valid domain-spec syntax, even though a DNS query cannot be composed 459 | from it. The spec being unclear about it, this could either be 460 | considered a syntax error, or, by analogy to 4.3/1 and 5/10/3, the 461 | mechanism chould be treated as a no-match. 462 | spec: [4.3/1, 5/10/3] 463 | helo: mail.example.com 464 | host: 1.2.3.4 465 | mailfrom: foo@t11.example.com 466 | result: [permerror,fail] 467 | invalid-domain-long-via-macro: 468 | description: >- 469 | target-name that is a valid domain-spec per RFC 4408 but an invalid 470 | domain name per RFC 1035 (long label) must be treated as non-existent. 471 | comment: >- 472 | A domain label longer than 63 characters that results from macro 473 | expansion in a mechanism target-name is valid domain-spec syntax (and is 474 | not even subject to syntax checking after macro expansion), even though 475 | a DNS query cannot be composed from it. The spec being unclear about 476 | it, this could either be considered a syntax error, or, by analogy to 477 | 4.3/1 and 5/10/3, the mechanism chould be treated as a no-match. 478 | spec: [4.3/1, 5/10/3] 479 | helo: "%%%%%%%%%%%%%%%%%%%%%%" 480 | host: 1.2.3.4 481 | mailfrom: foo@t12.example.com 482 | result: [permerror,fail] 483 | zonedata: 484 | mail.example.com: 485 | - A: 1.2.3.4 486 | t1.example.com: 487 | - SPF: v=spf1 ip4:1.2.3.4 -all moo 488 | t2.example.com: 489 | - SPF: v=spf1 moo.cow-far_out=man:dog/cat ip4:1.2.3.4 -all 490 | t3.example.com: 491 | - SPF: v=spf1 moo.cow/far_out=man:dog/cat ip4:1.2.3.4 -all 492 | t4.example.com: 493 | - SPF: v=spf1 moo.cow:far_out=man:dog/cat ip4:1.2.3.4 -all 494 | t5.example.com: 495 | - SPF: v=spf1 redirect=t5.example.com ~all 496 | t6.example.com: 497 | - SPF: v=spf1 ip4:1.2.3.4 redirect=t2.example.com 498 | t7.example.com: 499 | - SPF: v=spf1 ip4:1.2.3.4 500 | t8.example.com: 501 | - SPF: v=spf1 ip4:1.2.3.4 redirect:t2.example.com 502 | t9.example.com: 503 | - SPF: v=spf1 a:foo-bar -all 504 | t10.example.com: 505 | - SPF: v=spf1 a:mail.example...com -all 506 | t11.example.com: 507 | - SPF: v=spf1 a:a123456789012345678901234567890123456789012345678901234567890123.example.com -all 508 | t12.example.com: 509 | - SPF: v=spf1 a:%{H}.bar -all 510 | --- 511 | description: ALL mechanism syntax 512 | tests: 513 | all-dot: 514 | description: | 515 | all = "all" 516 | comment: |- 517 | At least one implementation got this wrong 518 | spec: 5.1/1 519 | helo: mail.example.com 520 | host: 1.2.3.4 521 | mailfrom: foo@e1.example.com 522 | result: permerror 523 | all-arg: 524 | description: | 525 | all = "all" 526 | comment: |- 527 | At least one implementation got this wrong 528 | spec: 5.1/1 529 | helo: mail.example.com 530 | host: 1.2.3.4 531 | mailfrom: foo@e2.example.com 532 | result: permerror 533 | all-cidr: 534 | description: | 535 | all = "all" 536 | spec: 5.1/1 537 | helo: mail.example.com 538 | host: 1.2.3.4 539 | mailfrom: foo@e3.example.com 540 | result: permerror 541 | all-neutral: 542 | description: | 543 | all = "all" 544 | spec: 5.1/1 545 | helo: mail.example.com 546 | host: 1.2.3.4 547 | mailfrom: foo@e4.example.com 548 | result: neutral 549 | all-double: 550 | description: | 551 | all = "all" 552 | spec: 5.1/1 553 | helo: mail.example.com 554 | host: 1.2.3.4 555 | mailfrom: foo@e5.example.com 556 | result: pass 557 | zonedata: 558 | mail.example.com: 559 | - A: 1.2.3.4 560 | e1.example.com: 561 | - SPF: v=spf1 -all. 562 | e2.example.com: 563 | - SPF: v=spf1 -all:foobar 564 | e3.example.com: 565 | - SPF: v=spf1 -all/8 566 | e4.example.com: 567 | - SPF: v=spf1 ?all 568 | e5.example.com: 569 | - SPF: v=spf1 all -all 570 | --- 571 | description: PTR mechanism syntax 572 | tests: 573 | ptr-cidr: 574 | description: |- 575 | PTR = "ptr" [ ":" domain-spec ] 576 | spec: 5.5/2 577 | helo: mail.example.com 578 | host: 1.2.3.4 579 | mailfrom: foo@e1.example.com 580 | result: permerror 581 | ptr-match-target: 582 | description: >- 583 | Check all validated domain names to see if they end in the 584 | domain. 585 | spec: 5.5/5 586 | helo: mail.example.com 587 | host: 1.2.3.4 588 | mailfrom: foo@e2.example.com 589 | result: pass 590 | ptr-match-implicit: 591 | description: >- 592 | Check all validated domain names to see if they end in the 593 | domain. 594 | spec: 5.5/5 595 | helo: mail.example.com 596 | host: 1.2.3.4 597 | mailfrom: foo@e3.example.com 598 | result: pass 599 | ptr-nomatch-invalid: 600 | description: >- 601 | Check all validated domain names to see if they end in the 602 | domain. 603 | comment: >- 604 | This PTR record does not validate 605 | spec: 5.5/5 606 | helo: mail.example.com 607 | host: 1.2.3.4 608 | mailfrom: foo@e4.example.com 609 | result: fail 610 | ptr-match-ip6: 611 | description: >- 612 | Check all validated domain names to see if they end in the 613 | domain. 614 | spec: 5.5/5 615 | helo: mail.example.com 616 | host: CAFE:BABE::1 617 | mailfrom: foo@e3.example.com 618 | result: pass 619 | ptr-empty-domain: 620 | description: >- 621 | domain-spec cannot be empty. 622 | spec: 5.5/2 623 | helo: mail.example.com 624 | host: 1.2.3.4 625 | mailfrom: foo@e5.example.com 626 | result: permerror 627 | zonedata: 628 | mail.example.com: 629 | - A: 1.2.3.4 630 | e1.example.com: 631 | - SPF: v=spf1 ptr/0 -all 632 | e2.example.com: 633 | - SPF: v=spf1 ptr:example.com -all 634 | 4.3.2.1.in-addr.arpa: 635 | - PTR: e3.example.com 636 | - PTR: e4.example.com 637 | - PTR: mail.example.com 638 | 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa: 639 | - PTR: e3.example.com 640 | e3.example.com: 641 | - SPF: v=spf1 ptr -all 642 | - A: 1.2.3.4 643 | - AAAA: CAFE:BABE::1 644 | e4.example.com: 645 | - SPF: v=spf1 ptr -all 646 | e5.example.com: 647 | - SPF: "v=spf1 ptr:" 648 | --- 649 | description: A mechanism syntax 650 | tests: 651 | a-cidr6: 652 | description: | 653 | A = "a" [ ":" domain-spec ] [ dual-cidr-length ] 654 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 655 | spec: 5.3/2 656 | helo: mail.example.com 657 | host: 1.2.3.4 658 | mailfrom: foo@e6.example.com 659 | result: fail 660 | a-bad-cidr4: 661 | description: | 662 | A = "a" [ ":" domain-spec ] [ dual-cidr-length ] 663 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 664 | spec: 5.3/2 665 | helo: mail.example.com 666 | host: 1.2.3.4 667 | mailfrom: foo@e6a.example.com 668 | result: permerror 669 | a-bad-cidr6: 670 | description: | 671 | A = "a" [ ":" domain-spec ] [ dual-cidr-length ] 672 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 673 | spec: 5.3/2 674 | helo: mail.example.com 675 | host: 1.2.3.4 676 | mailfrom: foo@e7.example.com 677 | result: permerror 678 | a-dual-cidr-ip4-match: 679 | description: | 680 | A = "a" [ ":" domain-spec ] [ dual-cidr-length ] 681 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 682 | spec: 5.3/2 683 | helo: mail.example.com 684 | host: 1.2.3.4 685 | mailfrom: foo@e8.example.com 686 | result: pass 687 | a-dual-cidr-ip4-err: 688 | description: | 689 | A = "a" [ ":" domain-spec ] [ dual-cidr-length ] 690 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 691 | spec: 5.3/2 692 | helo: mail.example.com 693 | host: 1.2.3.4 694 | mailfrom: foo@e8e.example.com 695 | result: permerror 696 | a-dual-cidr-ip6-match: 697 | description: | 698 | A = "a" [ ":" domain-spec ] [ dual-cidr-length ] 699 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 700 | spec: 5.3/2 701 | helo: mail.example.com 702 | host: 2001:db8:1234::cafe:babe 703 | mailfrom: foo@e8.example.com 704 | result: pass 705 | a-dual-cidr-ip4-default: 706 | description: | 707 | A = "a" [ ":" domain-spec ] [ dual-cidr-length ] 708 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 709 | spec: 5.3/2 710 | helo: mail.example.com 711 | host: 1.2.3.4 712 | mailfrom: foo@e8b.example.com 713 | result: fail 714 | a-dual-cidr-ip6-default: 715 | description: | 716 | A = "a" [ ":" domain-spec ] [ dual-cidr-length ] 717 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 718 | spec: 5.3/2 719 | helo: mail.example.com 720 | host: 2001:db8:1234::cafe:babe 721 | mailfrom: foo@e8a.example.com 722 | result: fail 723 | a-multi-ip1: 724 | description: >- 725 | A matches any returned IP. 726 | spec: 5.3/3 727 | helo: mail.example.com 728 | host: 1.2.3.4 729 | mailfrom: foo@e10.example.com 730 | result: pass 731 | a-multi-ip2: 732 | description: >- 733 | A matches any returned IP. 734 | spec: 5.3/3 735 | helo: mail.example.com 736 | host: 1.2.3.4 737 | mailfrom: foo@e10.example.com 738 | result: pass 739 | a-bad-domain: 740 | description: >- 741 | domain-spec must pass basic syntax checks; 742 | a ':' may appear in domain-spec, but not in top-label 743 | spec: 8.1/2 744 | helo: mail.example.com 745 | host: 1.2.3.4 746 | mailfrom: foo@e9.example.com 747 | result: permerror 748 | a-nxdomain: 749 | description: >- 750 | If no ips are returned, A mechanism does not match, even with /0. 751 | spec: 5.3/3 752 | helo: mail.example.com 753 | host: 1.2.3.4 754 | mailfrom: foo@e1.example.com 755 | result: fail 756 | a-cidr4-0: 757 | description: >- 758 | Matches if any A records are present in DNS. 759 | spec: 5.3/3 760 | helo: mail.example.com 761 | host: 1.2.3.4 762 | mailfrom: foo@e2.example.com 763 | result: pass 764 | a-cidr4-0-ip6: 765 | description: >- 766 | Matches if any A records are present in DNS. 767 | spec: 5.3/3 768 | helo: mail.example.com 769 | host: 1234::1 770 | mailfrom: foo@e2.example.com 771 | result: fail 772 | a-cidr6-0-ip4: 773 | description: >- 774 | Would match if any AAAA records are present in DNS, 775 | but not for an IP4 connection. 776 | spec: 5.3/3 777 | helo: mail.example.com 778 | host: 1.2.3.4 779 | mailfrom: foo@e2a.example.com 780 | result: fail 781 | a-cidr6-0-ip4mapped: 782 | description: >- 783 | Would match if any AAAA records are present in DNS, 784 | but not for an IP4 connection. 785 | spec: 5.3/3 786 | helo: mail.example.com 787 | host: ::FFFF:1.2.3.4 788 | mailfrom: foo@e2a.example.com 789 | result: fail 790 | a-cidr6-0-ip6: 791 | description: >- 792 | Matches if any AAAA records are present in DNS. 793 | spec: 5.3/3 794 | helo: mail.example.com 795 | host: 1234::1 796 | mailfrom: foo@e2a.example.com 797 | result: pass 798 | a-ip6-dualstack: 799 | description: >- 800 | Simple IP6 Address match with dual stack. 801 | spec: 5.3/3 802 | helo: mail.example.com 803 | host: 1234::1 804 | mailfrom: foo@ipv6.example.com 805 | result: pass 806 | a-cidr6-0-nxdomain: 807 | description: >- 808 | No match if no AAAA records are present in DNS. 809 | spec: 5.3/3 810 | helo: mail.example.com 811 | host: 1234::1 812 | mailfrom: foo@e2b.example.com 813 | result: fail 814 | a-null: 815 | description: >- 816 | Null octets not allowed in toplabel 817 | spec: 8.1/2 818 | helo: mail.example.com 819 | host: 1.2.3.5 820 | mailfrom: foo@e3.example.com 821 | result: permerror 822 | a-numeric: 823 | description: >- 824 | toplabel may not be all numeric 825 | comment: >- 826 | A common publishing mistake is using ip4 addresses with A mechanism. 827 | This should receive special diagnostic attention in the permerror. 828 | spec: 8.1/2 829 | helo: mail.example.com 830 | host: 1.2.3.4 831 | mailfrom: foo@e4.example.com 832 | result: permerror 833 | a-numeric-toplabel: 834 | description: >- 835 | toplabel may not be all numeric 836 | spec: 8.1/2 837 | helo: mail.example.com 838 | host: 1.2.3.4 839 | mailfrom: foo@e5.example.com 840 | result: permerror 841 | a-dash-in-toplabel: 842 | description: >- 843 | toplabel may contain dashes 844 | comment: >- 845 | Going from the "toplabel" grammar definition, an implementation using 846 | regular expressions in incrementally parsing SPF records might 847 | erroneously try to match a TLD such as ".xn--zckzah" (cf. IDN TLDs!) to 848 | '( *alphanum ALPHA *alphanum )' first before trying the alternative 849 | '( 1*alphanum "-" *( alphanum / "-" ) alphanum )', essentially causing 850 | a non-greedy, and thus, incomplete match. Make sure a greedy match is 851 | performed! 852 | spec: 8.1/2 853 | helo: mail.example.com 854 | host: 1.2.3.4 855 | mailfrom: foo@e14.example.com 856 | result: pass 857 | a-bad-toplabel: 858 | description: >- 859 | toplabel may not begin with a dash 860 | spec: 8.1/2 861 | helo: mail.example.com 862 | host: 1.2.3.4 863 | mailfrom: foo@e12.example.com 864 | result: permerror 865 | a-only-toplabel: 866 | description: >- 867 | domain-spec may not consist of only a toplabel. 868 | spec: 8.1/2 869 | helo: mail.example.com 870 | host: 1.2.3.4 871 | mailfrom: foo@e5a.example.com 872 | result: permerror 873 | a-only-toplabel-trailing-dot: 874 | description: >- 875 | domain-spec may not consist of only a toplabel. 876 | comment: >- 877 | "A trailing dot doesn't help." 878 | spec: 8.1/2 879 | helo: mail.example.com 880 | host: 1.2.3.4 881 | mailfrom: foo@e5b.example.com 882 | result: permerror 883 | a-colon-domain: 884 | description: >- 885 | domain-spec may contain any visible char except % 886 | spec: 8.1/2 887 | helo: mail.example.com 888 | host: 1.2.3.4 889 | mailfrom: foo@e11.example.com 890 | result: pass 891 | a-colon-domain-ip4mapped: 892 | description: >- 893 | domain-spec may contain any visible char except % 894 | spec: 8.1/2 895 | helo: mail.example.com 896 | host: ::FFFF:1.2.3.4 897 | mailfrom: foo@e11.example.com 898 | result: pass 899 | a-empty-domain: 900 | description: >- 901 | domain-spec cannot be empty. 902 | spec: 5.3/2 903 | helo: mail.example.com 904 | host: 1.2.3.4 905 | mailfrom: foo@e13.example.com 906 | result: permerror 907 | zonedata: 908 | mail.example.com: 909 | - A: 1.2.3.4 910 | e1.example.com: 911 | - SPF: v=spf1 a/0 -all 912 | e2.example.com: 913 | - A: 1.1.1.1 914 | - AAAA: 1234::2 915 | - SPF: v=spf1 a/0 -all 916 | e2a.example.com: 917 | - AAAA: 1234::1 918 | - SPF: v=spf1 a//0 -all 919 | e2b.example.com: 920 | - A: 1.1.1.1 921 | - SPF: v=spf1 a//0 -all 922 | ipv6.example.com: 923 | - AAAA: 1234::1 924 | - A: 1.1.1.1 925 | - SPF: v=spf1 a -all 926 | e3.example.com: 927 | - SPF: "v=spf1 a:foo.example.com\0" 928 | e4.example.com: 929 | - SPF: v=spf1 a:111.222.33.44 930 | e5.example.com: 931 | - SPF: v=spf1 a:abc.123 932 | e5a.example.com: 933 | - SPF: v=spf1 a:museum 934 | e5b.example.com: 935 | - SPF: v=spf1 a:museum. 936 | e6.example.com: 937 | - SPF: v=spf1 a//33 -all 938 | e6a.example.com: 939 | - SPF: v=spf1 a/33 -all 940 | e7.example.com: 941 | - SPF: v=spf1 a//129 -all 942 | e8.example.com: 943 | - A: 1.2.3.5 944 | - AAAA: 2001:db8:1234::dead:beef 945 | - SPF: v=spf1 a/24//64 -all 946 | e8e.example.com: 947 | - A: 1.2.3.5 948 | - AAAA: 2001:db8:1234::dead:beef 949 | - SPF: v=spf1 a/24/64 -all 950 | e8a.example.com: 951 | - A: 1.2.3.5 952 | - AAAA: 2001:db8:1234::dead:beef 953 | - SPF: v=spf1 a/24 -all 954 | e8b.example.com: 955 | - A: 1.2.3.5 956 | - AAAA: 2001:db8:1234::dead:beef 957 | - SPF: v=spf1 a//64 -all 958 | e9.example.com: 959 | - SPF: v=spf1 a:example.com:8080 960 | e10.example.com: 961 | - SPF: v=spf1 a:foo.example.com/24 962 | foo.example.com: 963 | - A: 1.1.1.1 964 | - A: 1.2.3.5 965 | e11.example.com: 966 | - SPF: v=spf1 a:foo:bar/baz.example.com 967 | foo:bar/baz.example.com: 968 | - A: 1.2.3.4 969 | e12.example.com: 970 | - SPF: v=spf1 a:example.-com 971 | e13.example.com: 972 | - SPF: "v=spf1 a:" 973 | e14.example.com: 974 | - SPF: "v=spf1 a:foo.example.xn--zckzah -all" 975 | foo.example.xn--zckzah: 976 | - A: 1.2.3.4 977 | --- 978 | description: Include mechanism semantics and syntax 979 | tests: 980 | include-fail: 981 | description: >- 982 | recursive check_host() result of fail causes include to not match. 983 | spec: 5.2/9 984 | helo: mail.example.com 985 | host: 1.2.3.4 986 | mailfrom: foo@e1.example.com 987 | result: softfail 988 | include-softfail: 989 | description: >- 990 | recursive check_host() result of softfail causes include to not match. 991 | spec: 5.2/9 992 | helo: mail.example.com 993 | host: 1.2.3.4 994 | mailfrom: foo@e2.example.com 995 | result: pass 996 | include-neutral: 997 | description: >- 998 | recursive check_host() result of neutral causes include to not match. 999 | spec: 5.2/9 1000 | helo: mail.example.com 1001 | host: 1.2.3.4 1002 | mailfrom: foo@e3.example.com 1003 | result: fail 1004 | include-temperror: 1005 | description: >- 1006 | recursive check_host() result of temperror causes include to temperror 1007 | spec: 5.2/9 1008 | helo: mail.example.com 1009 | host: 1.2.3.4 1010 | mailfrom: foo@e4.example.com 1011 | result: temperror 1012 | include-permerror: 1013 | description: >- 1014 | recursive check_host() result of permerror causes include to permerror 1015 | spec: 5.2/9 1016 | helo: mail.example.com 1017 | host: 1.2.3.4 1018 | mailfrom: foo@e5.example.com 1019 | result: permerror 1020 | include-syntax-error: 1021 | description: >- 1022 | include = "include" ":" domain-spec 1023 | spec: 5.2/1 1024 | helo: mail.example.com 1025 | host: 1.2.3.4 1026 | mailfrom: foo@e6.example.com 1027 | result: permerror 1028 | include-cidr: 1029 | description: >- 1030 | include = "include" ":" domain-spec 1031 | spec: 5.2/1 1032 | helo: mail.example.com 1033 | host: 1.2.3.4 1034 | mailfrom: foo@e9.example.com 1035 | result: permerror 1036 | include-none: 1037 | description: >- 1038 | recursive check_host() result of none causes include to permerror 1039 | spec: 5.2/9 1040 | helo: mail.example.com 1041 | host: 1.2.3.4 1042 | mailfrom: foo@e7.example.com 1043 | result: permerror 1044 | include-empty-domain: 1045 | description: >- 1046 | domain-spec cannot be empty. 1047 | spec: 5.2/1 1048 | helo: mail.example.com 1049 | host: 1.2.3.4 1050 | mailfrom: foo@e8.example.com 1051 | result: permerror 1052 | zonedata: 1053 | mail.example.com: 1054 | - A: 1.2.3.4 1055 | ip5.example.com: 1056 | - SPF: v=spf1 ip4:1.2.3.5 -all 1057 | ip6.example.com: 1058 | - SPF: v=spf1 ip4:1.2.3.6 ~all 1059 | ip7.example.com: 1060 | - SPF: v=spf1 ip4:1.2.3.7 ?all 1061 | ip8.example.com: 1062 | - TIMEOUT 1063 | erehwon.example.com: 1064 | - TXT: v=spfl am not an SPF record 1065 | e1.example.com: 1066 | - SPF: v=spf1 include:ip5.example.com ~all 1067 | e2.example.com: 1068 | - SPF: v=spf1 include:ip6.example.com all 1069 | e3.example.com: 1070 | - SPF: v=spf1 include:ip7.example.com -all 1071 | e4.example.com: 1072 | - SPF: v=spf1 include:ip8.example.com -all 1073 | e5.example.com: 1074 | - SPF: v=spf1 include:e6.example.com -all 1075 | e6.example.com: 1076 | - SPF: v=spf1 include +all 1077 | e7.example.com: 1078 | - SPF: v=spf1 include:erehwon.example.com -all 1079 | e8.example.com: 1080 | - SPF: "v=spf1 include: -all" 1081 | e9.example.com: 1082 | - SPF: "v=spf1 include:ip5.example.com/24 -all" 1083 | --- 1084 | description: MX mechanism syntax 1085 | tests: 1086 | mx-cidr6: 1087 | description: | 1088 | MX = "mx" [ ":" domain-spec ] [ dual-cidr-length ] 1089 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 1090 | spec: 5.4/2 1091 | helo: mail.example.com 1092 | host: 1.2.3.4 1093 | mailfrom: foo@e6.example.com 1094 | result: fail 1095 | mx-bad-cidr4: 1096 | description: | 1097 | MX = "mx" [ ":" domain-spec ] [ dual-cidr-length ] 1098 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 1099 | spec: 5.4/2 1100 | helo: mail.example.com 1101 | host: 1.2.3.4 1102 | mailfrom: foo@e6a.example.com 1103 | result: permerror 1104 | mx-bad-cidr6: 1105 | description: | 1106 | MX = "mx" [ ":" domain-spec ] [ dual-cidr-length ] 1107 | dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ] 1108 | spec: 5.4/2 1109 | helo: mail.example.com 1110 | host: 1.2.3.4 1111 | mailfrom: foo@e7.example.com 1112 | result: permerror 1113 | mx-multi-ip1: 1114 | description: >- 1115 | MX matches any returned IP. 1116 | spec: 5.4/3 1117 | helo: mail.example.com 1118 | host: 1.2.3.4 1119 | mailfrom: foo@e10.example.com 1120 | result: pass 1121 | mx-multi-ip2: 1122 | description: >- 1123 | MX matches any returned IP. 1124 | spec: 5.4/3 1125 | helo: mail.example.com 1126 | host: 1.2.3.4 1127 | mailfrom: foo@e10.example.com 1128 | result: pass 1129 | mx-bad-domain: 1130 | description: >- 1131 | domain-spec must pass basic syntax checks 1132 | comment: >- 1133 | A ':' may appear in domain-spec, but not in top-label. 1134 | spec: 8.1/2 1135 | helo: mail.example.com 1136 | host: 1.2.3.4 1137 | mailfrom: foo@e9.example.com 1138 | result: permerror 1139 | mx-nxdomain: 1140 | description: >- 1141 | If no ips are returned, MX mechanism does not match, even with /0. 1142 | spec: 5.4/3 1143 | helo: mail.example.com 1144 | host: 1.2.3.4 1145 | mailfrom: foo@e1.example.com 1146 | result: fail 1147 | mx-cidr4-0: 1148 | description: >- 1149 | Matches if any A records for any MX records are present in DNS. 1150 | spec: 5.4/3 1151 | helo: mail.example.com 1152 | host: 1.2.3.4 1153 | mailfrom: foo@e2.example.com 1154 | result: pass 1155 | mx-cidr4-0-ip6: 1156 | description: >- 1157 | cidr4 doesn't apply to IP6 connections. 1158 | spec: 5.4/3 1159 | helo: mail.example.com 1160 | host: 1234::1 1161 | mailfrom: foo@e2.example.com 1162 | result: fail 1163 | mx-cidr6-0-ip4: 1164 | description: >- 1165 | Would match if any AAAA records for MX records are present in DNS, 1166 | but not for an IP4 connection. 1167 | spec: 5.4/3 1168 | helo: mail.example.com 1169 | host: 1.2.3.4 1170 | mailfrom: foo@e2a.example.com 1171 | result: fail 1172 | mx-cidr6-0-ip4mapped: 1173 | description: >- 1174 | Would match if any AAAA records for MX records are present in DNS, 1175 | but not for an IP4 connection. 1176 | spec: 5.4/3 1177 | helo: mail.example.com 1178 | host: ::FFFF:1.2.3.4 1179 | mailfrom: foo@e2a.example.com 1180 | result: fail 1181 | mx-cidr6-0-ip6: 1182 | description: >- 1183 | Matches if any AAAA records for any MX records are present in DNS. 1184 | spec: 5.3/3 1185 | helo: mail.example.com 1186 | host: 1234::1 1187 | mailfrom: foo@e2a.example.com 1188 | result: pass 1189 | mx-cidr6-0-nxdomain: 1190 | description: >- 1191 | No match if no AAAA records for any MX records are present in DNS. 1192 | spec: 5.4/3 1193 | helo: mail.example.com 1194 | host: 1234::1 1195 | mailfrom: foo@e2b.example.com 1196 | result: fail 1197 | mx-null: 1198 | description: >- 1199 | Null not allowed in top-label. 1200 | spec: 8.1/2 1201 | helo: mail.example.com 1202 | host: 1.2.3.5 1203 | mailfrom: foo@e3.example.com 1204 | result: permerror 1205 | mx-numeric-top-label: 1206 | description: >- 1207 | Top-label may not be all numeric 1208 | spec: 8.1/2 1209 | helo: mail.example.com 1210 | host: 1.2.3.4 1211 | mailfrom: foo@e5.example.com 1212 | result: permerror 1213 | mx-colon-domain: 1214 | description: >- 1215 | Domain-spec may contain any visible char except % 1216 | spec: 8.1/2 1217 | helo: mail.example.com 1218 | host: 1.2.3.4 1219 | mailfrom: foo@e11.example.com 1220 | result: pass 1221 | mx-colon-domain-ip4mapped: 1222 | description: >- 1223 | Domain-spec may contain any visible char except % 1224 | spec: 8.1/2 1225 | helo: mail.example.com 1226 | host: ::FFFF:1.2.3.4 1227 | mailfrom: foo@e11.example.com 1228 | result: pass 1229 | mx-bad-toplab: 1230 | description: >- 1231 | Toplabel may not begin with - 1232 | spec: 8.1/2 1233 | helo: mail.example.com 1234 | host: 1.2.3.4 1235 | mailfrom: foo@e12.example.com 1236 | result: permerror 1237 | mx-empty: 1238 | description: >- 1239 | test null MX 1240 | comment: >- 1241 | Some implementations have had trouble with null MX 1242 | spec: 5.4/3 1243 | helo: mail.example.com 1244 | host: 1.2.3.4 1245 | mailfrom: "" 1246 | result: neutral 1247 | mx-implicit: 1248 | description: >- 1249 | If the target name has no MX records, check_host() MUST NOT pretend the 1250 | target is its single MX, and MUST NOT default to an A lookup on the 1251 | target-name directly. 1252 | spec: 5.4/4 1253 | helo: mail.example.com 1254 | host: 1.2.3.4 1255 | mailfrom: foo@e4.example.com 1256 | result: neutral 1257 | mx-empty-domain: 1258 | description: >- 1259 | domain-spec cannot be empty. 1260 | spec: 5.2/1 1261 | helo: mail.example.com 1262 | host: 1.2.3.4 1263 | mailfrom: foo@e13.example.com 1264 | result: permerror 1265 | zonedata: 1266 | mail.example.com: 1267 | - A: 1.2.3.4 1268 | - MX: [0, ""] 1269 | - SPF: v=spf1 mx 1270 | e1.example.com: 1271 | - SPF: v=spf1 mx/0 -all 1272 | - MX: [0, e1.example.com] 1273 | e2.example.com: 1274 | - A: 1.1.1.1 1275 | - AAAA: 1234::2 1276 | - MX: [0, e2.example.com] 1277 | - SPF: v=spf1 mx/0 -all 1278 | e2a.example.com: 1279 | - AAAA: 1234::1 1280 | - MX: [0, e2a.example.com] 1281 | - SPF: v=spf1 mx//0 -all 1282 | e2b.example.com: 1283 | - A: 1.1.1.1 1284 | - MX: [0, e2b.example.com] 1285 | - SPF: v=spf1 mx//0 -all 1286 | e3.example.com: 1287 | - SPF: "v=spf1 mx:foo.example.com\0" 1288 | e4.example.com: 1289 | - SPF: v=spf1 mx 1290 | - A: 1.2.3.4 1291 | e5.example.com: 1292 | - SPF: v=spf1 mx:abc.123 1293 | e6.example.com: 1294 | - SPF: v=spf1 mx//33 -all 1295 | e6a.example.com: 1296 | - SPF: v=spf1 mx/33 -all 1297 | e7.example.com: 1298 | - SPF: v=spf1 mx//129 -all 1299 | e9.example.com: 1300 | - SPF: v=spf1 mx:example.com:8080 1301 | e10.example.com: 1302 | - SPF: v=spf1 mx:foo.example.com/24 1303 | foo.example.com: 1304 | - MX: [0, foo1.example.com] 1305 | foo1.example.com: 1306 | - A: 1.1.1.1 1307 | - A: 1.2.3.5 1308 | e11.example.com: 1309 | - SPF: v=spf1 mx:foo:bar/baz.example.com 1310 | foo:bar/baz.example.com: 1311 | - MX: [0, "foo:bar/baz.example.com"] 1312 | - A: 1.2.3.4 1313 | e12.example.com: 1314 | - SPF: v=spf1 mx:example.-com 1315 | e13.example.com: 1316 | - SPF: "v=spf1 mx: -all" 1317 | --- 1318 | description: EXISTS mechanism syntax 1319 | tests: 1320 | exists-empty-domain: 1321 | description: >- 1322 | domain-spec cannot be empty. 1323 | spec: 5.7/2 1324 | helo: mail.example.com 1325 | host: 1.2.3.4 1326 | mailfrom: foo@e1.example.com 1327 | result: permerror 1328 | exists-implicit: 1329 | description: >- 1330 | exists = "exists" ":" domain-spec 1331 | spec: 5.7/2 1332 | helo: mail.example.com 1333 | host: 1.2.3.4 1334 | mailfrom: foo@e2.example.com 1335 | result: permerror 1336 | exists-cidr: 1337 | description: >- 1338 | exists = "exists" ":" domain-spec 1339 | spec: 5.7/2 1340 | helo: mail.example.com 1341 | host: 1.2.3.4 1342 | mailfrom: foo@e3.example.com 1343 | result: permerror 1344 | exists-ip4: 1345 | description: >- 1346 | mechanism matches if any DNS A RR exists 1347 | spec: 5.7/3 1348 | helo: mail.example.com 1349 | host: 1.2.3.4 1350 | mailfrom: foo@e4.example.com 1351 | result: pass 1352 | exists-ip6: 1353 | description: >- 1354 | The lookup type is A even when the connection is ip6 1355 | spec: 5.7/3 1356 | helo: mail.example.com 1357 | host: CAFE:BABE::3 1358 | mailfrom: foo@e4.example.com 1359 | result: pass 1360 | exists-ip6only: 1361 | description: >- 1362 | The lookup type is A even when the connection is ip6 1363 | spec: 5.7/3 1364 | helo: mail.example.com 1365 | host: CAFE:BABE::3 1366 | mailfrom: foo@e5.example.com 1367 | result: fail 1368 | exists-dnserr: 1369 | description: >- 1370 | Result for DNS error is being clarified in spfbis 1371 | spec: 5.7/3 1372 | helo: mail.example.com 1373 | host: CAFE:BABE::3 1374 | mailfrom: foo@e6.example.com 1375 | result: [fail, temperror] 1376 | zonedata: 1377 | mail.example.com: 1378 | - A: 1.2.3.4 1379 | mail6.example.com: 1380 | - AAAA: CAFE:BABE::4 1381 | err.example.com: 1382 | - TIMEOUT 1383 | e1.example.com: 1384 | - SPF: "v=spf1 exists:" 1385 | e2.example.com: 1386 | - SPF: "v=spf1 exists" 1387 | e3.example.com: 1388 | - SPF: "v=spf1 exists:mail.example.com/24" 1389 | e4.example.com: 1390 | - SPF: "v=spf1 exists:mail.example.com" 1391 | e5.example.com: 1392 | - SPF: "v=spf1 exists:mail6.example.com -all" 1393 | e6.example.com: 1394 | - SPF: "v=spf1 exists:err.example.com -all" 1395 | --- 1396 | description: IP4 mechanism syntax 1397 | tests: 1398 | cidr4-0: 1399 | description: >- 1400 | ip4-cidr-length = "/" 1*DIGIT 1401 | spec: 5.6/2 1402 | helo: mail.example.com 1403 | host: 1.2.3.4 1404 | mailfrom: foo@e1.example.com 1405 | result: pass 1406 | cidr4-32: 1407 | description: >- 1408 | ip4-cidr-length = "/" 1*DIGIT 1409 | spec: 5.6/2 1410 | helo: mail.example.com 1411 | host: 1.2.3.4 1412 | mailfrom: foo@e2.example.com 1413 | result: pass 1414 | cidr4-33: 1415 | description: >- 1416 | Invalid CIDR should get permerror. 1417 | comment: >- 1418 | The RFC is silent on ip4 CIDR > 32 or ip6 CIDR > 128. However, 1419 | since there is no reasonable interpretation (except a noop), we have 1420 | read between the lines to see a prohibition on invalid CIDR. 1421 | spec: 5.6/2 1422 | helo: mail.example.com 1423 | host: 1.2.3.4 1424 | mailfrom: foo@e3.example.com 1425 | result: permerror 1426 | cidr4-032: 1427 | description: >- 1428 | Invalid CIDR should get permerror. 1429 | comment: >- 1430 | Leading zeros are not explicitly prohibited by the RFC. However, 1431 | since the RFC explicity prohibits leading zeros in ip4-network, 1432 | our interpretation is that CIDR should be also. 1433 | spec: 5.6/2 1434 | helo: mail.example.com 1435 | host: 1.2.3.4 1436 | mailfrom: foo@e4.example.com 1437 | result: permerror 1438 | bare-ip4: 1439 | description: >- 1440 | IP4 = "ip4" ":" ip4-network [ ip4-cidr-length ] 1441 | spec: 5.6/2 1442 | helo: mail.example.com 1443 | host: 1.2.3.4 1444 | mailfrom: foo@e5.example.com 1445 | result: permerror 1446 | bad-ip4-port: 1447 | description: >- 1448 | IP4 = "ip4" ":" ip4-network [ ip4-cidr-length ] 1449 | comment: >- 1450 | This has actually been published in SPF records. 1451 | spec: 5.6/2 1452 | helo: mail.example.com 1453 | host: 1.2.3.4 1454 | mailfrom: foo@e8.example.com 1455 | result: permerror 1456 | bad-ip4-short: 1457 | description: >- 1458 | It is not permitted to omit parts of the IP address instead of 1459 | using CIDR notations. 1460 | spec: 5.6/4 1461 | helo: mail.example.com 1462 | host: 1.2.3.4 1463 | mailfrom: foo@e9.example.com 1464 | result: permerror 1465 | ip4-dual-cidr: 1466 | description: >- 1467 | dual-cidr-length not permitted on ip4 1468 | spec: 5.6/2 1469 | helo: mail.example.com 1470 | host: 1.2.3.4 1471 | mailfrom: foo@e6.example.com 1472 | result: permerror 1473 | ip4-mapped-ip6: 1474 | description: >- 1475 | IP4 mapped IP6 connections MUST be treated as IP4 1476 | spec: 5/9/2 1477 | helo: mail.example.com 1478 | host: ::FFFF:1.2.3.4 1479 | mailfrom: foo@e7.example.com 1480 | result: fail 1481 | zonedata: 1482 | mail.example.com: 1483 | - A: 1.2.3.4 1484 | e1.example.com: 1485 | - SPF: v=spf1 ip4:1.1.1.1/0 -all 1486 | e2.example.com: 1487 | - SPF: v=spf1 ip4:1.2.3.4/32 -all 1488 | e3.example.com: 1489 | - SPF: v=spf1 ip4:1.2.3.4/33 -all 1490 | e4.example.com: 1491 | - SPF: v=spf1 ip4:1.2.3.4/032 -all 1492 | e5.example.com: 1493 | - SPF: v=spf1 ip4 1494 | e6.example.com: 1495 | - SPF: v=spf1 ip4:1.2.3.4//32 1496 | e7.example.com: 1497 | - SPF: v=spf1 -ip4:1.2.3.4 ip6:::FFFF:1.2.3.4 1498 | e8.example.com: 1499 | - SPF: v=spf1 ip4:1.2.3.4:8080 1500 | e9.example.com: 1501 | - SPF: v=spf1 ip4:1.2.3 1502 | --- 1503 | description: IP6 mechanism syntax 1504 | comment: >- 1505 | IP4 only implementations may skip tests where host is not IP4 1506 | tests: 1507 | bare-ip6: 1508 | description: >- 1509 | IP6 = "ip6" ":" ip6-network [ ip6-cidr-length ] 1510 | spec: 5.6/2 1511 | helo: mail.example.com 1512 | host: 1.2.3.4 1513 | mailfrom: foo@e1.example.com 1514 | result: permerror 1515 | cidr6-0-ip4: 1516 | description: >- 1517 | IP4 connections do not match ip6. 1518 | comment: >- 1519 | There is controversy over ip4 mapped connections. RFC4408 clearly 1520 | requires such connections to be considered as ip4. However, 1521 | some interpret the RFC to mean that such connections should *also* 1522 | match appropriate ip6 mechanisms (but not, inexplicably, A or MX 1523 | mechanisms). Until there is consensus, both 1524 | results are acceptable. 1525 | spec: 5/9/2 1526 | helo: mail.example.com 1527 | host: 1.2.3.4 1528 | mailfrom: foo@e2.example.com 1529 | result: [neutral, pass] 1530 | cidr6-ip4: 1531 | description: >- 1532 | Even if the SMTP connection is via IPv6, an IPv4-mapped IPv6 IP address 1533 | (see RFC 3513, Section 2.5.5) MUST still be considered an IPv4 address. 1534 | comment: >- 1535 | There is controversy over ip4 mapped connections. RFC4408 clearly 1536 | requires such connections to be considered as ip4. However, 1537 | some interpret the RFC to mean that such connections should *also* 1538 | match appropriate ip6 mechanisms (but not, inexplicably, A or MX 1539 | mechanisms). Until there is consensus, both 1540 | results are acceptable. 1541 | spec: 5/9/2 1542 | helo: mail.example.com 1543 | host: ::FFFF:1.2.3.4 1544 | mailfrom: foo@e2.example.com 1545 | result: [neutral, pass] 1546 | cidr6-0: 1547 | description: >- 1548 | Match any IP6 1549 | spec: 5/8 1550 | helo: mail.example.com 1551 | host: DEAF:BABE::CAB:FEE 1552 | mailfrom: foo@e2.example.com 1553 | result: pass 1554 | cidr6-129: 1555 | description: >- 1556 | Invalid CIDR 1557 | comment: >- 1558 | IP4 only implementations MUST fully syntax check all mechanisms, 1559 | even if they otherwise ignore them. 1560 | spec: 5.6/2 1561 | helo: mail.example.com 1562 | host: 1.2.3.4 1563 | mailfrom: foo@e3.example.com 1564 | result: permerror 1565 | cidr6-bad: 1566 | description: >- 1567 | dual-cidr syntax not used for ip6 1568 | comment: >- 1569 | IP4 only implementations MUST fully syntax check all mechanisms, 1570 | even if they otherwise ignore them. 1571 | spec: 5.6/2 1572 | helo: mail.example.com 1573 | host: 1.2.3.4 1574 | mailfrom: foo@e4.example.com 1575 | result: permerror 1576 | cidr6-33: 1577 | description: >- 1578 | make sure ip4 cidr restriction are not used for ip6 1579 | spec: 5.6/2 1580 | helo: mail.example.com 1581 | host: "CAFE:BABE:8000::" 1582 | mailfrom: foo@e5.example.com 1583 | result: pass 1584 | cidr6-33-ip4: 1585 | description: >- 1586 | make sure ip4 cidr restriction are not used for ip6 1587 | spec: 5.6/2 1588 | helo: mail.example.com 1589 | host: 1.2.3.4 1590 | mailfrom: foo@e5.example.com 1591 | result: neutral 1592 | ip6-bad1: 1593 | description: >- 1594 | spec: 5.6/2 1595 | helo: mail.example.com 1596 | host: 1.2.3.4 1597 | mailfrom: foo@e6.example.com 1598 | result: permerror 1599 | zonedata: 1600 | mail.example.com: 1601 | - A: 1.2.3.4 1602 | e1.example.com: 1603 | - SPF: v=spf1 -all ip6 1604 | e2.example.com: 1605 | - SPF: v=spf1 ip6:::1.1.1.1/0 1606 | e3.example.com: 1607 | - SPF: v=spf1 ip6:::1.1.1.1/129 1608 | e4.example.com: 1609 | - SPF: v=spf1 ip6:::1.1.1.1//33 1610 | e5.example.com: 1611 | - SPF: v=spf1 ip6:CAFE:BABE:8000::/33 1612 | e6.example.com: 1613 | - SPF: v=spf1 ip6::CAFE::BABE 1614 | --- 1615 | description: Semantics of exp and other modifiers 1616 | comment: >- 1617 | Implementing exp= is optional. If not implemented, the test driver should 1618 | not check the explanation field. 1619 | tests: 1620 | redirect-none: 1621 | description: >- 1622 | If no SPF record is found, or if the target-name is malformed, the result 1623 | is a "PermError" rather than "None". 1624 | spec: 6.1/4 1625 | helo: mail.example.com 1626 | host: 1.2.3.4 1627 | mailfrom: foo@e10.example.com 1628 | result: permerror 1629 | redirect-cancels-exp: 1630 | description: >- 1631 | when executing "redirect", exp= from the original domain MUST NOT be used. 1632 | spec: 6.2/13 1633 | helo: mail.example.com 1634 | host: 1.2.3.4 1635 | mailfrom: foo@e1.example.com 1636 | result: fail 1637 | explanation: DEFAULT 1638 | redirect-syntax-error: 1639 | description: | 1640 | redirect = "redirect" "=" domain-spec 1641 | comment: >- 1642 | A literal application of the grammar causes modifier syntax 1643 | errors (except for macro syntax) to become unknown-modifier. 1644 | 1645 | modifier = explanation | redirect | unknown-modifier 1646 | 1647 | However, it is generally agreed, with precedent in other RFCs, 1648 | that unknown-modifier should not be "greedy", and should not 1649 | match known modifier names. There should have been explicit 1650 | prose to this effect, and some has been proposed as an erratum. 1651 | spec: 6.1/2 1652 | helo: mail.example.com 1653 | host: 1.2.3.4 1654 | mailfrom: foo@e17.example.com 1655 | result: permerror 1656 | include-ignores-exp: 1657 | description: >- 1658 | when executing "include", exp= from the target domain MUST NOT be used. 1659 | spec: 6.2/13 1660 | helo: mail.example.com 1661 | host: 1.2.3.4 1662 | mailfrom: foo@e7.example.com 1663 | result: fail 1664 | explanation: Correct! 1665 | redirect-cancels-prior-exp: 1666 | description: >- 1667 | when executing "redirect", exp= from the original domain MUST NOT be used. 1668 | spec: 6.2/13 1669 | helo: mail.example.com 1670 | host: 1.2.3.4 1671 | mailfrom: foo@e3.example.com 1672 | result: fail 1673 | explanation: See me. 1674 | invalid-modifier: 1675 | description: | 1676 | unknown-modifier = name "=" macro-string 1677 | name = ALPHA *( ALPHA / DIGIT / "-" / "_" / "." ) 1678 | comment: >- 1679 | Unknown modifier name must begin with alpha. 1680 | spec: A/3 1681 | helo: mail.example.com 1682 | host: 1.2.3.4 1683 | mailfrom: foo@e5.example.com 1684 | result: permerror 1685 | empty-modifier-name: 1686 | description: | 1687 | name = ALPHA *( ALPHA / DIGIT / "-" / "_" / "." ) 1688 | comment: >- 1689 | Unknown modifier name must not be empty. 1690 | spec: A/3 1691 | helo: mail.example.com 1692 | host: 1.2.3.4 1693 | mailfrom: foo@e6.example.com 1694 | result: permerror 1695 | dorky-sentinel: 1696 | description: >- 1697 | An implementation that uses a legal expansion as a sentinel. We 1698 | cannot check them all, but we can check this one. 1699 | comment: >- 1700 | Spaces are allowed in local-part. 1701 | spec: 8.1/6 1702 | helo: mail.example.com 1703 | host: 1.2.3.4 1704 | mailfrom: "Macro Error@e8.example.com" 1705 | result: fail 1706 | explanation: Macro Error in implementation 1707 | exp-multiple-txt: 1708 | description: | 1709 | Ignore exp if multiple TXT records. 1710 | comment: >- 1711 | If domain-spec is empty, or there are any DNS processing errors (any 1712 | RCODE other than 0), or if no records are returned, or if more than one 1713 | record is returned, or if there are syntax errors in the explanation 1714 | string, then proceed as if no exp modifier was given. 1715 | spec: 6.2/4 1716 | helo: mail.example.com 1717 | host: 1.2.3.4 1718 | mailfrom: foo@e11.example.com 1719 | result: fail 1720 | explanation: DEFAULT 1721 | exp-no-txt: 1722 | description: | 1723 | Ignore exp if no TXT records. 1724 | comment: >- 1725 | If domain-spec is empty, or there are any DNS processing errors (any 1726 | RCODE other than 0), or if no records are returned, or if more than one 1727 | record is returned, or if there are syntax errors in the explanation 1728 | string, then proceed as if no exp modifier was given. 1729 | spec: 6.2/4 1730 | helo: mail.example.com 1731 | host: 1.2.3.4 1732 | mailfrom: foo@e22.example.com 1733 | result: fail 1734 | explanation: DEFAULT 1735 | exp-dns-error: 1736 | description: | 1737 | Ignore exp if DNS error. 1738 | comment: >- 1739 | If domain-spec is empty, or there are any DNS processing errors (any 1740 | RCODE other than 0), or if no records are returned, or if more than one 1741 | record is returned, or if there are syntax errors in the explanation 1742 | string, then proceed as if no exp modifier was given. 1743 | spec: 6.2/4 1744 | helo: mail.example.com 1745 | host: 1.2.3.4 1746 | mailfrom: foo@e21.example.com 1747 | result: fail 1748 | explanation: DEFAULT 1749 | exp-empty-domain: 1750 | description: | 1751 | PermError if exp= domain-spec is empty. 1752 | comment: >- 1753 | Section 6.2/4 says, "If domain-spec is empty, or there are any DNS 1754 | processing errors (any RCODE other than 0), or if no records are 1755 | returned, or if more than one record is returned, or if there are syntax 1756 | errors in the explanation string, then proceed as if no exp modifier was 1757 | given." However, "if domain-spec is empty" conflicts with the grammar 1758 | given for the exp modifier. This was reported as an erratum, and the 1759 | solution chosen was to report explicit "exp=" as PermError, but ignore 1760 | problems due to macro expansion, DNS, or invalid explanation string. 1761 | spec: 6.2/4 1762 | helo: mail.example.com 1763 | host: 1.2.3.4 1764 | mailfrom: foo@e12.example.com 1765 | result: permerror 1766 | explanation-syntax-error: 1767 | description: | 1768 | Ignore exp if the explanation string has a syntax error. 1769 | comment: >- 1770 | If domain-spec is empty, or there are any DNS processing errors (any 1771 | RCODE other than 0), or if no records are returned, or if more than one 1772 | record is returned, or if there are syntax errors in the explanation 1773 | string, then proceed as if no exp modifier was given. 1774 | spec: 6.2/4 1775 | helo: mail.example.com 1776 | host: 1.2.3.4 1777 | mailfrom: foo@e13.example.com 1778 | result: fail 1779 | explanation: DEFAULT 1780 | exp-syntax-error: 1781 | description: | 1782 | explanation = "exp" "=" domain-spec 1783 | comment: >- 1784 | A literal application of the grammar causes modifier syntax 1785 | errors (except for macro syntax) to become unknown-modifier. 1786 | 1787 | modifier = explanation | redirect | unknown-modifier 1788 | 1789 | However, it is generally agreed, with precedent in other RFCs, 1790 | that unknown-modifier should not be "greedy", and should not 1791 | match known modifier names. There should have been explicit 1792 | prose to this effect, and some has been proposed as an erratum. 1793 | spec: 6.2/1 1794 | helo: mail.example.com 1795 | host: 1.2.3.4 1796 | mailfrom: foo@e16.example.com 1797 | result: permerror 1798 | exp-twice: 1799 | description: | 1800 | exp= appears twice. 1801 | comment: >- 1802 | These two modifiers (exp,redirect) MUST NOT appear in a record more than 1803 | once each. If they do, then check_host() exits with a result of 1804 | "PermError". 1805 | spec: 6/2 1806 | helo: mail.example.com 1807 | host: 1.2.3.4 1808 | mailfrom: foo@e14.example.com 1809 | result: permerror 1810 | redirect-empty-domain: 1811 | description: | 1812 | redirect = "redirect" "=" domain-spec 1813 | comment: >- 1814 | Unlike for exp, there is no instruction to override the permerror 1815 | for an empty domain-spec (which is invalid syntax). 1816 | spec: 6.2/4 1817 | helo: mail.example.com 1818 | host: 1.2.3.4 1819 | mailfrom: foo@e18.example.com 1820 | result: permerror 1821 | redirect-twice: 1822 | description: | 1823 | redirect= appears twice. 1824 | comment: >- 1825 | These two modifiers (exp,redirect) MUST NOT appear in a record more than 1826 | once each. If they do, then check_host() exits with a result of 1827 | "PermError". 1828 | spec: 6/2 1829 | helo: mail.example.com 1830 | host: 1.2.3.4 1831 | mailfrom: foo@e15.example.com 1832 | result: permerror 1833 | unknown-modifier-syntax: 1834 | description: | 1835 | unknown-modifier = name "=" macro-string 1836 | comment: >- 1837 | Unknown modifiers must have valid macro syntax. 1838 | spec: A/3 1839 | helo: mail.example.com 1840 | host: 1.2.3.4 1841 | mailfrom: foo@e9.example.com 1842 | result: permerror 1843 | default-modifier-obsolete: 1844 | description: | 1845 | Unknown modifiers do not modify the RFC SPF result. 1846 | comment: >- 1847 | Some implementations may have a leftover default= modifier from 1848 | earlier drafts. 1849 | spec: 6/3 1850 | helo: mail.example.com 1851 | host: 1.2.3.4 1852 | mailfrom: foo@e19.example.com 1853 | result: neutral 1854 | default-modifier-obsolete2: 1855 | description: | 1856 | Unknown modifiers do not modify the RFC SPF result. 1857 | comment: >- 1858 | Some implementations may have a leftover default= modifier from 1859 | earlier drafts. 1860 | spec: 6/3 1861 | helo: mail.example.com 1862 | host: 1.2.3.4 1863 | mailfrom: foo@e20.example.com 1864 | result: neutral 1865 | non-ascii-exp: 1866 | description: >- 1867 | SPF explanation text is restricted to 7-bit ascii. 1868 | comment: >- 1869 | Checking a possibly different code path for non-ascii chars. 1870 | spec: 6.2/5 1871 | helo: hosed 1872 | host: 1.2.3.4 1873 | mailfrom: "foobar@nonascii.example.com" 1874 | result: fail 1875 | explanation: DEFAULT 1876 | two-exp-records: 1877 | description: >- 1878 | Must ignore exp= if DNS returns more than one TXT record. 1879 | spec: 6.2/4 1880 | helo: hosed 1881 | host: 1.2.3.4 1882 | mailfrom: "foobar@tworecs.example.com" 1883 | result: fail 1884 | explanation: DEFAULT 1885 | zonedata: 1886 | mail.example.com: 1887 | - A: 1.2.3.4 1888 | e1.example.com: 1889 | - SPF: v=spf1 exp=exp1.example.com redirect=e2.example.com 1890 | e2.example.com: 1891 | - SPF: v=spf1 -all 1892 | e3.example.com: 1893 | - SPF: v=spf1 exp=exp1.example.com redirect=e4.example.com 1894 | e4.example.com: 1895 | - SPF: v=spf1 -all exp=exp2.example.com 1896 | exp1.example.com: 1897 | - TXT: No-see-um 1898 | exp2.example.com: 1899 | - TXT: See me. 1900 | exp3.example.com: 1901 | - TXT: Correct! 1902 | exp4.example.com: 1903 | - TXT: "%{l} in implementation" 1904 | e5.example.com: 1905 | - SPF: v=spf1 1up=foo 1906 | e6.example.com: 1907 | - SPF: v=spf1 =all 1908 | e7.example.com: 1909 | - SPF: v=spf1 include:e3.example.com -all exp=exp3.example.com 1910 | e8.example.com: 1911 | - SPF: v=spf1 -all exp=exp4.example.com 1912 | e9.example.com: 1913 | - SPF: v=spf1 -all foo=%abc 1914 | e10.example.com: 1915 | - SPF: v=spf1 redirect=erehwon.example.com 1916 | e11.example.com: 1917 | - SPF: v=spf1 -all exp=e11msg.example.com 1918 | e11msg.example.com: 1919 | - TXT: Answer a fool according to his folly. 1920 | - TXT: Do not answer a fool according to his folly. 1921 | e12.example.com: 1922 | - SPF: v=spf1 exp= -all 1923 | e13.example.com: 1924 | - SPF: v=spf1 exp=e13msg.example.com -all 1925 | e13msg.example.com: 1926 | - TXT: The %{x}-files. 1927 | e14.example.com: 1928 | - SPF: v=spf1 exp=e13msg.example.com -all exp=e11msg.example.com 1929 | e15.example.com: 1930 | - SPF: v=spf1 redirect=e12.example.com -all redirect=e12.example.com 1931 | e16.example.com: 1932 | - SPF: v=spf1 exp=-all 1933 | e17.example.com: 1934 | - SPF: v=spf1 redirect=-all ?all 1935 | e18.example.com: 1936 | - SPF: v=spf1 ?all redirect= 1937 | e19.example.com: 1938 | - SPF: v=spf1 default=pass 1939 | e20.example.com: 1940 | - SPF: "v=spf1 default=+" 1941 | e21.example.com: 1942 | - SPF: v=spf1 exp=e21msg.example.com -all 1943 | e21msg.example.com: 1944 | - TIMEOUT 1945 | e22.example.com: 1946 | - SPF: v=spf1 exp=mail.example.com -all 1947 | nonascii.example.com: 1948 | - SPF: v=spf1 exp=badexp.example.com -all 1949 | badexp.example.com: 1950 | - TXT: "\xEF\xBB\xBFExplanation" 1951 | tworecs.example.com: 1952 | - SPF: v=spf1 exp=twoexp.example.com -all 1953 | twoexp.example.com: 1954 | - TXT: "one" 1955 | - TXT: "two" 1956 | --- 1957 | description: Macro expansion rules 1958 | tests: 1959 | trailing-dot-domain: 1960 | spec: 8.1/16 1961 | description: >- 1962 | trailing dot is ignored for domains 1963 | helo: msgbas2x.cos.example.com 1964 | host: 192.168.218.40 1965 | mailfrom: test@example.com 1966 | result: pass 1967 | trailing-dot-exp: 1968 | spec: 8.1 1969 | description: >- 1970 | trailing dot is not removed from explanation 1971 | comment: >- 1972 | A simple way for an implementation to ignore trailing dots on 1973 | domains is to remove it when present. But be careful not to 1974 | remove it for explanation text. 1975 | helo: msgbas2x.cos.example.com 1976 | host: 192.168.218.40 1977 | mailfrom: test@exp.example.com 1978 | result: fail 1979 | explanation: This is a test. 1980 | exp-only-macro-char: 1981 | spec: 8.1/8 1982 | description: >- 1983 | The following macro letters are allowed only in "exp" text: c, r, t 1984 | helo: msgbas2x.cos.example.com 1985 | host: 192.168.218.40 1986 | mailfrom: test@e2.example.com 1987 | result: permerror 1988 | invalid-macro-char: 1989 | spec: 8.1/9 1990 | description: >- 1991 | A '%' character not followed by a '{', '%', '-', or '_' character 1992 | is a syntax error. 1993 | helo: msgbas2x.cos.example.com 1994 | host: 192.168.218.40 1995 | mailfrom: test@e1.example.com 1996 | result: permerror 1997 | invalid-embedded-macro-char: 1998 | spec: 8.1/9 1999 | description: >- 2000 | A '%' character not followed by a '{', '%', '-', or '_' character 2001 | is a syntax error. 2002 | helo: msgbas2x.cos.example.com 2003 | host: 192.168.218.40 2004 | mailfrom: test@e1e.example.com 2005 | result: permerror 2006 | invalid-trailing-macro-char: 2007 | spec: 8.1/9 2008 | description: >- 2009 | A '%' character not followed by a '{', '%', '-', or '_' character 2010 | is a syntax error. 2011 | helo: msgbas2x.cos.example.com 2012 | host: 192.168.218.40 2013 | mailfrom: test@e1t.example.com 2014 | result: permerror 2015 | macro-mania-in-domain: 2016 | description: >- 2017 | macro-encoded percents (%%), spaces (%_), and URL-percent-encoded 2018 | spaces (%-) 2019 | spec: 8.1/3, 8.1/4 2020 | helo: mail.example.com 2021 | host: 1.2.3.4 2022 | mailfrom: test@e1a.example.com 2023 | result: pass 2024 | exp-txt-macro-char: 2025 | spec: 8.1/20 2026 | description: >- 2027 | For IPv4 addresses, both the "i" and "c" macros expand 2028 | to the standard dotted-quad format. 2029 | helo: msgbas2x.cos.example.com 2030 | host: 192.168.218.40 2031 | mailfrom: test@e3.example.com 2032 | result: fail 2033 | explanation: Connections from 192.168.218.40 not authorized. 2034 | domain-name-truncation: 2035 | spec: 8.1/25 2036 | description: >- 2037 | When the result of macro expansion is used in a domain name query, if the 2038 | expanded domain name exceeds 253 characters, the left side is truncated 2039 | to fit, by removing successive domain labels until the total length does 2040 | not exceed 253 characters. 2041 | helo: msgbas2x.cos.example.com 2042 | host: 192.168.218.40 2043 | mailfrom: test@somewhat.long.exp.example.com 2044 | result: fail 2045 | explanation: Congratulations! That was tricky. 2046 | v-macro-ip4: 2047 | spec: 8.1/6 2048 | description: |- 2049 | v = the string "in-addr" if is ipv4, or "ip6" if is ipv6 2050 | helo: msgbas2x.cos.example.com 2051 | host: 192.168.218.40 2052 | mailfrom: test@e4.example.com 2053 | result: fail 2054 | explanation: 192.168.218.40 is queried as 40.218.168.192.in-addr.arpa 2055 | v-macro-ip6: 2056 | spec: 8.1/6 2057 | description: |- 2058 | v = the string "in-addr" if is ipv4, or "ip6" if is ipv6 2059 | helo: msgbas2x.cos.example.com 2060 | host: CAFE:BABE::1 2061 | mailfrom: test@e4.example.com 2062 | result: fail 2063 | explanation: cafe:babe::1 is queried as 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa 2064 | undef-macro: 2065 | spec: 8.1/6 2066 | description: >- 2067 | Allowed macros chars are 'slodipvh' plus 'crt' in explanation. 2068 | helo: msgbas2x.cos.example.com 2069 | host: CAFE:BABE::192.168.218.40 2070 | mailfrom: test@e5.example.com 2071 | result: permerror 2072 | p-macro-ip4-novalid: 2073 | spec: 8.1/22 2074 | description: |- 2075 | p = the validated domain name of 2076 | comment: >- 2077 | The PTR in this example does not validate. 2078 | helo: msgbas2x.cos.example.com 2079 | host: 192.168.218.40 2080 | mailfrom: test@e6.example.com 2081 | result: fail 2082 | explanation: connect from unknown 2083 | p-macro-ip4-valid: 2084 | spec: 8.1/22 2085 | description: |- 2086 | p = the validated domain name of 2087 | comment: >- 2088 | If a subdomain of the is present, it SHOULD be used. 2089 | helo: msgbas2x.cos.example.com 2090 | host: 192.168.218.41 2091 | mailfrom: test@e6.example.com 2092 | result: fail 2093 | explanation: connect from mx.example.com 2094 | p-macro-ip6-novalid: 2095 | spec: 8.1/22 2096 | description: |- 2097 | p = the validated domain name of 2098 | comment: >- 2099 | The PTR in this example does not validate. 2100 | helo: msgbas2x.cos.example.com 2101 | host: CAFE:BABE::1 2102 | mailfrom: test@e6.example.com 2103 | result: fail 2104 | explanation: connect from unknown 2105 | p-macro-ip6-valid: 2106 | spec: 8.1/22 2107 | description: |- 2108 | p = the validated domain name of 2109 | comment: >- 2110 | If a subdomain of the is present, it SHOULD be used. 2111 | helo: msgbas2x.cos.example.com 2112 | host: CAFE:BABE::3 2113 | mailfrom: test@e6.example.com 2114 | result: fail 2115 | explanation: connect from mx.example.com 2116 | p-macro-multiple: 2117 | spec: 8.1/22 2118 | description: |- 2119 | p = the validated domain name of 2120 | comment: >- 2121 | If a subdomain of the is present, it SHOULD be used. 2122 | helo: msgbas2x.cos.example.com 2123 | host: 192.168.218.42 2124 | mailfrom: test@e7.example.com 2125 | result: [pass, softfail] 2126 | upper-macro: 2127 | spec: 8.1/26 2128 | description: >- 2129 | Uppercased macros expand exactly as their lowercased equivalents, 2130 | and are then URL escaped. 2131 | helo: msgbas2x.cos.example.com 2132 | host: 192.168.218.42 2133 | mailfrom: jack&jill=up@e8.example.com 2134 | result: fail 2135 | explanation: http://example.com/why.html?l=jack%26jill%3Dup 2136 | hello-macro: 2137 | spec: 8.1/6 2138 | description: |- 2139 | h = HELO/EHLO domain 2140 | helo: msgbas2x.cos.example.com 2141 | host: 192.168.218.40 2142 | mailfrom: test@e9.example.com 2143 | result: pass 2144 | invalid-hello-macro: 2145 | spec: 8.1/2 2146 | description: |- 2147 | h = HELO/EHLO domain, but HELO is invalid 2148 | comment: >- 2149 | Domain-spec must end in either a macro, or a valid toplabel. 2150 | It is not correct to check syntax after macro expansion. 2151 | helo: "JUMPIN' JUPITER" 2152 | host: 192.168.218.40 2153 | mailfrom: test@e9.example.com 2154 | result: fail 2155 | hello-domain-literal: 2156 | spec: 8.1/2 2157 | description: |- 2158 | h = HELO/EHLO domain, but HELO is a domain literal 2159 | comment: >- 2160 | Domain-spec must end in either a macro, or a valid toplabel. 2161 | It is not correct to check syntax after macro expansion. 2162 | helo: "[192.168.218.40]" 2163 | host: 192.168.218.40 2164 | mailfrom: test@e9.example.com 2165 | result: fail 2166 | require-valid-helo: 2167 | spec: 8.1/6 2168 | description: >- 2169 | Example of requiring valid helo in sender policy. This is a complex 2170 | policy testing several points at once. 2171 | helo: OEMCOMPUTER 2172 | host: 1.2.3.4 2173 | mailfrom: test@e10.example.com 2174 | result: fail 2175 | macro-reverse-split-on-dash: 2176 | spec: [8.1/15, 8.1/16, 8.1/17, 8.1/18] 2177 | description: >- 2178 | Macro value transformation (splitting on arbitrary characters, reversal, 2179 | number of right-hand parts to use) 2180 | helo: mail.example.com 2181 | host: 1.2.3.4 2182 | mailfrom: philip-gladstone-test@e11.example.com 2183 | result: pass 2184 | macro-multiple-delimiters: 2185 | spec: [8.1/15, 8.1/16] 2186 | description: |- 2187 | Multiple delimiters may be specified in a macro expression. 2188 | macro-expand = ( "%{" macro-letter transformers *delimiter "}" ) 2189 | / "%%" / "%_" / "%-" 2190 | helo: mail.example.com 2191 | host: 1.2.3.4 2192 | mailfrom: foo-bar+zip+quux@e12.example.com 2193 | result: pass 2194 | zonedata: 2195 | example.com.d.spf.example.com: 2196 | - SPF: v=spf1 redirect=a.spf.example.com 2197 | a.spf.example.com: 2198 | - SPF: v=spf1 include:o.spf.example.com. ~all 2199 | o.spf.example.com: 2200 | - SPF: v=spf1 ip4:192.168.218.40 2201 | msgbas2x.cos.example.com: 2202 | - A: 192.168.218.40 2203 | example.com: 2204 | - A: 192.168.90.76 2205 | - SPF: v=spf1 redirect=%{d}.d.spf.example.com. 2206 | exp.example.com: 2207 | - SPF: v=spf1 exp=msg.example.com. -all 2208 | msg.example.com: 2209 | - TXT: This is a test. 2210 | e1.example.com: 2211 | - SPF: v=spf1 -exists:%(ir).sbl.example.com ?all 2212 | e1e.example.com: 2213 | - SPF: v=spf1 exists:foo%(ir).sbl.example.com ?all 2214 | e1t.example.com: 2215 | - SPF: v=spf1 exists:foo%.sbl.example.com ?all 2216 | e1a.example.com: 2217 | - SPF: "v=spf1 a:macro%%percent%_%_space%-url-space.example.com -all" 2218 | "macro%percent space%20url-space.example.com": 2219 | - A: 1.2.3.4 2220 | e2.example.com: 2221 | - SPF: v=spf1 -all exp=%{r}.example.com 2222 | e3.example.com: 2223 | - SPF: v=spf1 -all exp=%{ir}.example.com 2224 | 40.218.168.192.example.com: 2225 | - TXT: Connections from %{c} not authorized. 2226 | somewhat.long.exp.example.com: 2227 | - SPF: v=spf1 -all exp=foobar.%{o}.%{o}.%{o}.%{o}.%{o}.%{o}.%{o}.%{o}.example.com 2228 | somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.somewhat.long.exp.example.com.example.com: 2229 | - TXT: Congratulations! That was tricky. 2230 | e4.example.com: 2231 | - SPF: v=spf1 -all exp=e4msg.example.com 2232 | e4msg.example.com: 2233 | - TXT: "%{c} is queried as %{ir}.%{v}.arpa" 2234 | e5.example.com: 2235 | - SPF: v=spf1 a:%{a}.example.com -all 2236 | e6.example.com: 2237 | - SPF: v=spf1 -all exp=e6msg.example.com 2238 | e6msg.example.com: 2239 | - TXT: "connect from %{p}" 2240 | mx.example.com: 2241 | - A: 192.168.218.41 2242 | - A: 192.168.218.42 2243 | - AAAA: CAFE:BABE::2 2244 | - AAAA: CAFE:BABE::3 2245 | 40.218.168.192.in-addr.arpa: 2246 | - PTR: mx.example.com 2247 | 41.218.168.192.in-addr.arpa: 2248 | - PTR: mx.example.com 2249 | 42.218.168.192.in-addr.arpa: 2250 | - PTR: mx.example.com 2251 | - PTR: mx.e7.example.com 2252 | 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa: 2253 | - PTR: mx.example.com 2254 | 3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa: 2255 | - PTR: mx.example.com 2256 | mx.e7.example.com: 2257 | - A: 192.168.218.42 2258 | mx.e7.example.com.should.example.com: 2259 | - A: 127.0.0.2 2260 | mx.example.com.ok.example.com: 2261 | - A: 127.0.0.2 2262 | e7.example.com: 2263 | - SPF: v=spf1 exists:%{p}.should.example.com ~exists:%{p}.ok.example.com 2264 | e8.example.com: 2265 | - SPF: v=spf1 -all exp=msg8.%{D2} 2266 | msg8.example.com: 2267 | - TXT: "http://example.com/why.html?l=%{L}" 2268 | e9.example.com: 2269 | - SPF: v=spf1 a:%{H} -all 2270 | e10.example.com: 2271 | - SPF: v=spf1 -include:_spfh.%{d2} ip4:1.2.3.0/24 -all 2272 | _spfh.example.com: 2273 | - SPF: v=spf1 -a:%{h} +all 2274 | e11.example.com: 2275 | - SPF: v=spf1 exists:%{i}.%{l2r-}.user.%{d2} 2276 | 1.2.3.4.gladstone.philip.user.example.com: 2277 | - A: 127.0.0.2 2278 | e12.example.com: 2279 | - SPF: v=spf1 exists:%{l2r+-}.user.%{d2} 2280 | bar.foo.user.example.com: 2281 | - A: 127.0.0.2 2282 | --- 2283 | description: Processing limits 2284 | tests: 2285 | redirect-loop: 2286 | description: >- 2287 | SPF implementations MUST limit the number of mechanisms and modifiers 2288 | that do DNS lookups to at most 10 per SPF check. 2289 | spec: 10.1/6 2290 | helo: mail.example.com 2291 | host: 1.2.3.4 2292 | mailfrom: foo@e1.example.com 2293 | result: permerror 2294 | include-loop: 2295 | description: >- 2296 | SPF implementations MUST limit the number of mechanisms and modifiers 2297 | that do DNS lookups to at most 10 per SPF check. 2298 | spec: 10.1/6 2299 | helo: mail.example.com 2300 | host: 1.2.3.4 2301 | mailfrom: foo@e2.example.com 2302 | result: permerror 2303 | mx-limit: 2304 | description: >- 2305 | there MUST be a limit of no more than 10 MX looked up and checked. 2306 | comment: >- 2307 | The required result for this test was the subject of much 2308 | controversy. Many felt that the RFC *should* have specified 2309 | permerror, but the consensus was that it failed to actually do so. 2310 | The preferred result reflects evaluating the 10 allowed MX records in the 2311 | order returned by the test data - or sorted via priority. 2312 | If testing with live DNS, the MX order may be random, and a pass 2313 | result would still be compliant. The SPF result is effectively 2314 | random. 2315 | spec: 10.1/7 2316 | helo: mail.example.com 2317 | host: 1.2.3.5 2318 | mailfrom: foo@e4.example.com 2319 | result: [neutral, pass, permerror] 2320 | ptr-limit: 2321 | description: >- 2322 | there MUST be a limit of no more than 10 PTR looked up and checked. 2323 | comment: >- 2324 | The result of this test cannot be permerror not only because the 2325 | RFC does not specify it, but because the sender has no control over 2326 | the PTR records of spammers. 2327 | The preferred result reflects evaluating the 10 allowed PTR records in 2328 | the order returned by the test data. 2329 | If testing with live DNS, the PTR order may be random, and a pass 2330 | result would still be compliant. The SPF result is effectively 2331 | randomized. 2332 | spec: 10.1/7 2333 | helo: mail.example.com 2334 | host: 1.2.3.5 2335 | mailfrom: foo@e5.example.com 2336 | result: [neutral, pass] 2337 | false-a-limit: 2338 | description: >- 2339 | unlike MX, PTR, there is no RR limit for A 2340 | comment: >- 2341 | There seems to be a tendency for developers to want to limit 2342 | A RRs in addition to MX and PTR. These are IPs, not usable for 2343 | 3rd party DoS attacks, and hence need no low limit. 2344 | spec: 10.1/7 2345 | helo: mail.example.com 2346 | host: 1.2.3.12 2347 | mailfrom: foo@e10.example.com 2348 | result: pass 2349 | mech-at-limit: 2350 | description: >- 2351 | SPF implementations MUST limit the number of mechanisms and modifiers 2352 | that do DNS lookups to at most 10 per SPF check. 2353 | spec: 10.1/6 2354 | helo: mail.example.com 2355 | host: 1.2.3.4 2356 | mailfrom: foo@e6.example.com 2357 | result: pass 2358 | mech-over-limit: 2359 | description: >- 2360 | SPF implementations MUST limit the number of mechanisms and modifiers 2361 | that do DNS lookups to at most 10 per SPF check. 2362 | comment: >- 2363 | We do not check whether an implementation counts mechanisms before 2364 | or after evaluation. The RFC is not clear on this. 2365 | spec: 10.1/6 2366 | helo: mail.example.com 2367 | host: 1.2.3.4 2368 | mailfrom: foo@e7.example.com 2369 | result: permerror 2370 | include-at-limit: 2371 | description: >- 2372 | SPF implementations MUST limit the number of mechanisms and modifiers 2373 | that do DNS lookups to at most 10 per SPF check. 2374 | comment: >- 2375 | The part of the RFC that talks about MAY parse the entire record first 2376 | (4.6) is specific to syntax errors. Processing limits is a different, 2377 | non-syntax issue. Processing limits (10.1) specifically talks about 2378 | limits during a check. 2379 | spec: 10.1/6 2380 | helo: mail.example.com 2381 | host: 1.2.3.4 2382 | mailfrom: foo@e8.example.com 2383 | result: pass 2384 | include-over-limit: 2385 | description: >- 2386 | SPF implementations MUST limit the number of mechanisms and modifiers 2387 | that do DNS lookups to at most 10 per SPF check. 2388 | spec: 10.1/6 2389 | helo: mail.example.com 2390 | host: 1.2.3.4 2391 | mailfrom: foo@e9.example.com 2392 | result: permerror 2393 | zonedata: 2394 | mail.example.com: 2395 | - A: 1.2.3.4 2396 | e1.example.com: 2397 | - SPF: v=spf1 ip4:1.1.1.1 redirect=e1.example.com 2398 | - A: 1.2.3.6 2399 | e2.example.com: 2400 | - SPF: v=spf1 include:e3.example.com 2401 | - A: 1.2.3.7 2402 | e3.example.com: 2403 | - SPF: v=spf1 include:e2.example.com 2404 | - A: 1.2.3.8 2405 | e4.example.com: 2406 | - SPF: v=spf1 mx 2407 | - MX: [0, mail.example.com] 2408 | - MX: [1, mail.example.com] 2409 | - MX: [2, mail.example.com] 2410 | - MX: [3, mail.example.com] 2411 | - MX: [4, mail.example.com] 2412 | - MX: [5, mail.example.com] 2413 | - MX: [6, mail.example.com] 2414 | - MX: [7, mail.example.com] 2415 | - MX: [8, mail.example.com] 2416 | - MX: [9, mail.example.com] 2417 | - MX: [10, e4.example.com] 2418 | - A: 1.2.3.5 2419 | e5.example.com: 2420 | - SPF: v=spf1 ptr 2421 | - A: 1.2.3.5 2422 | 5.3.2.1.in-addr.arpa: 2423 | - PTR: e1.example.com. 2424 | - PTR: e2.example.com. 2425 | - PTR: e3.example.com. 2426 | - PTR: e4.example.com. 2427 | - PTR: example.com. 2428 | - PTR: e6.example.com. 2429 | - PTR: e7.example.com. 2430 | - PTR: e8.example.com. 2431 | - PTR: e9.example.com. 2432 | - PTR: e10.example.com. 2433 | - PTR: e5.example.com. 2434 | e6.example.com: 2435 | - SPF: v=spf1 a mx a mx a mx a mx a ptr ip4:1.2.3.4 -all 2436 | - A: 1.2.3.8 2437 | - MX: [10, e6.example.com] 2438 | e7.example.com: 2439 | - SPF: v=spf1 a mx a mx a mx a mx a ptr a ip4:1.2.3.4 -all 2440 | - A: 1.2.3.20 2441 | e8.example.com: 2442 | - SPF: v=spf1 a include:inc.example.com ip4:1.2.3.4 mx -all 2443 | - A: 1.2.3.4 2444 | inc.example.com: 2445 | - SPF: v=spf1 a a a a a a a a 2446 | - A: 1.2.3.10 2447 | e9.example.com: 2448 | - SPF: v=spf1 a include:inc.example.com a ip4:1.2.3.4 -all 2449 | - A: 1.2.3.21 2450 | e10.example.com: 2451 | - SPF: v=spf1 a -all 2452 | - A: 1.2.3.1 2453 | - A: 1.2.3.2 2454 | - A: 1.2.3.3 2455 | - A: 1.2.3.4 2456 | - A: 1.2.3.5 2457 | - A: 1.2.3.6 2458 | - A: 1.2.3.7 2459 | - A: 1.2.3.8 2460 | - A: 1.2.3.9 2461 | - A: 1.2.3.10 2462 | - A: 1.2.3.11 2463 | - A: 1.2.3.12 2464 | -------------------------------------------------------------------------------- /test/rfc7208-tests.CHANGES: -------------------------------------------------------------------------------- 1 | # Legend: 2 | # --- = A new release 3 | # ! = Added a test case or otherwise tightened a requirement, possibly 4 | # causing implementations to become incompliant with the current 5 | # test-suite release 6 | # - = Removed a test case or otherwise relaxed a requirement 7 | # * = Fixed a bug, or made a minor improvement 8 | 9 | --- 2019.08 (UNRELEASED) 10 | ! Added multiple tests for creative syntax errors in SPF records 11 | that were breaking implementations. 12 | 13 | --- 2014.04 (UNRELEASED) 14 | ! Updates for RFC 7208 (4408bis) 15 | ! Updated multiple tests not to consider type SPF records under mixed 16 | conditions - Note: due to the way the test suite is structured, many 17 | records are still labled SPF internally, but for test functions, it 18 | doesn't matter externally. 19 | - Removed "invalid-domain-empty-label", "invalid-domain-long", and 20 | "invalid-domain-long-via-macro". Since RFC 7208 explicitly describes 21 | the results for these conditions as undefined, there's no point in 22 | testing for a particular result. 23 | ! Modified multiple tests to remove ambiguous results for cases that were 24 | ambiguous in RFC 4408, but have been clarified in RFC 7208. 25 | ! Changed "mx-limit" test to produce permerror result per changes in RFC 26 | 7208 27 | ! Added "invalid-trailing-macro-char" and "invalid-embedded-macro-char" 28 | tests from Stuart on pyspf trunk 29 | 30 | --- 2009.10 (2009-10-31 20:00) 31 | 32 | ! Added test case: 33 | ! "macro-multiple-delimiters": 34 | Multiple delimiters in a macro expression must be supported. 35 | * Fixed "multitxt2" test case failing with SPF-type-only implementations. 36 | Tolerate a "None" result to accomodate those. 37 | 38 | --- 2008.08 (2008-08-17 16:00) 39 | 40 | ! "invalid-domain-empty-label", "invalid-domain-long", 41 | "invalid-domain-long-via-macro" test cases: 42 | A that is a valid domain-spec per RFC 4408 but an invalid 43 | domain name per RFC 1035 (two successive dots or labels longer than 63 44 | characters) must be treated either as a "PermError" or as non-existent and 45 | thus a no-match. (In particular, those cases can never cause a TempError 46 | because the error is guaranteed to reoccur given the same input data. 47 | This applies likewise to RFC-1035-invalid s that are the 48 | result of macro expansion.) Refined descriptions and comments to that 49 | end. 50 | The no-match behavior can be inferred by analogy from 4.3/1 and 5/10/3. 51 | The spec reference to 8.1/2 is bogus because the formal grammar does not 52 | preclude such invalid domain names. 53 | ! The "exp= without domain-spec" controversy has been resolved; it must be a 54 | syntax error. Tightened "exp-empty-domain" test case accordingly. 55 | ! Added test cases: 56 | ! "a-dash-in-toplabel": 57 | may contain dashes. Implementations matching 58 | non-greedily may get that wrong. 59 | ! "a-only-toplabel", "a-only-toplabel-trailing-dot": 60 | Both "a:museum" and "a:museum." are invalid syntax. A bare top-label is 61 | insufficient, with or without a trailing dot. 62 | ! "exp-no-txt", "exp-dns-error": 63 | Clearly, "exp=" referring to a non-existent TXT RR, or the look-up 64 | resulting in a DNS error, must cause the "exp=" modifier to be ignored per 65 | 6.2/4. 66 | ! "macro-mania-in-domain": 67 | Test macro-encoded percents (%%), spaces (%_), and URL-percent-encoded 68 | spaces (%20) in . 69 | ! "macro-reverse-split-on-dash": 70 | Test transformation of macro expansion results: splitting on non-dot 71 | separator characters, reversal, number of right-hand parts to use. 72 | - Removed "a-valid-syntax-but-unqueryable" test case. It is redundant to 73 | the "invalid-domain-empty-label" test case. 74 | - Relaxed "multispf1" test case: 75 | If performed via live DNS (yes, some people do that!), this test may be 76 | ineffective as DNS resolvers may combine multiple identical RRs. Thus, 77 | tolerate the test failing in this manner. 78 | * Adjusted "multispf2" test case: 79 | Avoid combination of multiple identical RRs by using different 80 | capitalization in intentionally duplicate RRs. 81 | * Renamed test cases: 82 | a-numeric-top-label -> a-numeric-toplabel 83 | a-bad-toplab -> a-bad-toplabel 84 | 85 | --- 2007.05 (2007-05-30 21:00) 86 | 87 | - "exp-empty-domain" test case is subject to controversy. "exp=" with an 88 | empty domain-spec may be considered a syntax error or not, thus both "Fail" 89 | and "PermError" results are acceptable for now. 90 | * Renamed the old "exp-syntax-error" test case to "explanation-syntax-error" 91 | to indicate that it refers to syntax errors in the explanation string, not 92 | in the "exp=" modifier. 93 | ! Added test cases: 94 | ! "exp-syntax-error", "redirect-syntax-error": Syntax errors in "exp=" and 95 | "redirect=" must be treated as such. 96 | ! "a-empty-domain", "mx-empty-domain", "ptr-empty-domain", 97 | "include-empty-domain", "redirect-empty-domain": "a:", "mx:", "ptr:", 98 | "include:", and "redirect=" with an empty domain-spec are syntax errors. 99 | ! "include-cidr": "include:/" is a syntax error. 100 | ! "helo-not-fqdn", "helo-domain-literal", "domain-literal": A non-FQDN 101 | HELO or MAIL FROM must result in a "None" result. 102 | ! "hello-domain-literal": Macro expansion results must not be checked for 103 | syntax errors, but must rather be treated as non-matches if nonsensical. 104 | ! "false-a-limit": There is no limit for the number of A records resulting 105 | from an "a:"-induced lookup, and no such limit must be imposed. 106 | ! "default-modifier-obsolete(2)": The "default=" modifier used in very old 107 | spec drafts must be ignored by RFC 4408 implementations. 108 | 109 | --- 2007.01 (2007-01-14 05:19) 110 | 111 | ! Added test cases: 112 | ! "nospftxttimeout": If no SPF-type record is present and the TXT lookup 113 | times out, the result must either be "None" (preferred) or "TempError". 114 | ! "exp-multiple-txt", "exp-syntax-error": Multiple explanation string TXT 115 | records and syntax errors in explanation strings must be ignored (i.e., 116 | specifically "PermError" must NOT be returned). 117 | ! "exp-empty-domain": "exp=" with an empty domain-spec is to be tolerated, 118 | i.e., ignored, too. (This is under debate.) 119 | ! "exp-twice", "redirect-twice": Added. Multiple "exp=" or "redirect=" 120 | modifiers are prohibited. 121 | * "Macro expansion rules" scenario: Fixed a bug that caused TXT-only 122 | implementations to fail several tests incorrectly due to a real TXT record 123 | blocking the automatic synthesis of TXT records from the corresponding 124 | SPF-type records. 125 | 126 | --- 2006.11 (initial release) (2006-11-27 21:27) 127 | 128 | # $Id$ 129 | # vim:tw=79 sts=2 sw=2 130 | -------------------------------------------------------------------------------- /test/rfc7208-tests.LICENSE: -------------------------------------------------------------------------------- 1 | The RFC 7208 test-suite (rfc7208-tests.yml) is 2 | (C) 2006-2008 Stuart D Gathman 3 | 2007-2008 Julian Mehnle 4 | 2014 Scott Kitterman 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 1. Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 3. The names of the authors may not be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 19 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 23 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /test/test.yml: -------------------------------------------------------------------------------- 1 | # This is the test suite used during development of the pyspf library. 2 | # It is a collection of ad hoc tests based on bug reports. It is the 3 | # goal of the SPF test project to have an elegant and minimal test suite 4 | # that reflects RFC 4408. However, this should help get things started 5 | # by serving as a example of what tests look like. Also, any implementation 6 | # that flunks this, should flunk the minimal elegant suite as well. 7 | # 8 | # We extended the test attributes with 'receiver' and 'header' to test 9 | # our implementation of the Received-SPF header. This cannot easily 10 | # be part of the RFC test suite because of wide latitude in formatting. 11 | # 12 | --- 13 | comment: | 14 | check basic exists with macros 15 | tests: 16 | exists-pass: 17 | helo: mail.example.net 18 | host: 1.2.3.5 19 | mailfrom: lyme.eater@example.co.uk 20 | result: pass 21 | receiver: receiver.com 22 | header: >- 23 | Pass (receiver.com: domain of example.co.uk designates 1.2.3.5 as 24 | permitted sender) client-ip=1.2.3.5; 25 | envelope-from="lyme.eater@example.co.uk"; helo=mail.example.net; 26 | receiver=receiver.com; mechanism="exists:%{l}.%{d}.%{i}.spf.example.net"; 27 | identity=mailfrom 28 | exists-fail: 29 | helo: mail.example.net 30 | host: 1.2.3.4 31 | mailfrom: lyme.eater@example.co.uk 32 | result: fail 33 | zonedata: 34 | lyme.eater.example.co.uk.1.2.3.5.spf.example.net: 35 | - A: 127.0.0.1 36 | example.co.uk: 37 | - SPF: v=spf1 mx/26 exists:%{l}.%{d}.%{i}.spf.example.net -all 38 | 39 | --- 40 | comment: | 41 | permerror detection 42 | tests: 43 | incloop: 44 | comment: | 45 | include loop 46 | helo: mail.example.com 47 | host: 66.150.186.79 48 | mailfrom: chuckvsr@examplea.com 49 | result: permerror 50 | badall: 51 | helo: mail.example.com 52 | host: 66.150.186.79 53 | mailfrom: chuckvsr@examplec.com 54 | result: permerror 55 | baddomain: 56 | helo: mail.example.com 57 | host: 66.150.186.79 58 | mailfrom: chuckvsr@exampled.com 59 | result: permerror 60 | receiver: receiver.com 61 | header: >- 62 | PermError (receiver.com: permanent error in processing 63 | domain of exampled.com: Invalid domain found (use FQDN)) 64 | client-ip=66.150.186.79; envelope-from="chuckvsr@exampled.com"; 65 | helo=mail.example.com; receiver=receiver.com; 66 | problem="examplea.com:8080"; identity=mailfrom 67 | tworecs: 68 | helo: mail.example.com 69 | host: 66.150.186.79 70 | mailfrom: chuckvsr@examplef.com 71 | result: permerror 72 | receiver: receiver.com 73 | header: >- 74 | PermError (receiver.com: permanent error in processing domain of 75 | examplef.com: Two or more type TXT spf records found.) 76 | client-ip=66.150.186.79; envelope-from="chuckvsr@examplef.com"; 77 | helo=mail.example.com; receiver=receiver.com; identity=mailfrom 78 | badip: 79 | helo: mail.example.com 80 | host: 66.150.186.79 81 | mailfrom: chuckvsr@examplee.com 82 | result: permerror 83 | zonedata: 84 | examplea.com: 85 | - SPF: v=spf1 a mx include:b.com 86 | exampleb.com: 87 | - SPF: v=spf1 a mx include:a.com 88 | examplec.com: 89 | - SPF: v=spf1 -all:foobar 90 | exampled.com: 91 | - SPF: v=spf1 a:examplea.com:8080 92 | examplee.com: 93 | - SPF: v=spf1 ip4:1.2.3.4:8080 94 | examplef.com: 95 | - SPF: v=spf1 -all 96 | - SPF: v=spf1 +all 97 | 98 | --- 99 | tests: 100 | nospace1: 101 | comment: | 102 | test no space 103 | test multi-line comment 104 | helo: mail.example1.com 105 | host: 1.2.3.4 106 | mailfrom: foo@example2.com 107 | result: none 108 | empty: 109 | comment: | 110 | test empty 111 | helo: mail1.example1.com 112 | host: 1.2.3.4 113 | mailfrom: foo@example1.com 114 | result: neutral 115 | nospace2: 116 | helo: mail.example1.com 117 | host: 1.2.3.4 118 | mailfrom: foo@example3.com 119 | result: pass 120 | zonedata: 121 | example3.com: 122 | - SPF: [ 'v=spf1','mx' ] 123 | - SPF: [ 'v=spf1 ', 'mx' ] 124 | - MX: [0, mail.example1.com] 125 | example1.com: 126 | - SPF: v=spf1 127 | example2.com: 128 | - SPF: v=spf1mx 129 | mail.example1.com: 130 | - A: 1.2.3.4 131 | 132 | --- 133 | comment: | 134 | corner cases 135 | tests: 136 | emptyMX: 137 | comment: | 138 | test empty MX 139 | helo: mail.example.com 140 | host: 1.2.3.4 141 | mailfrom: "" 142 | result: neutral 143 | localhost: 144 | helo: mail.example.com 145 | host: 127.0.0.1 146 | mailfrom: root@example.com 147 | result: fail 148 | default-modifier: 149 | comment: | 150 | default modifier implemented in lax mode for compatibility 151 | helo: mail.example.com 152 | host: 1.2.3.4 153 | mailfrom: root@e1.example.com 154 | result: fail 155 | strict: 0 156 | default-modifier-harsh: 157 | comment: | 158 | default modifier implemented in lax mode for compatibility 159 | helo: mail.example.com 160 | host: 1.2.3.4 161 | mailfrom: root@e1.example.com 162 | result: ambiguous 163 | strict: 2 164 | cname-chain: 165 | comment: | 166 | pyspf was duplicating TXT (and other) records while following CNAME 167 | helo: mail.example.com 168 | host: 1.2.3.4 169 | mailfrom: foo@e2.example.com 170 | result: pass 171 | null-cname: 172 | comment: | 173 | pyspf was getting a type error for null CNAMEs 174 | Thanks to Kazuhiro Ogura 175 | helo: mail.example.com 176 | host: 1.2.3.4 177 | mailfrom: bar@e3.example.com 178 | result: softfail 179 | zonedata: 180 | mail.example.com: 181 | - MX: [0, ''] 182 | - SPF: v=spf1 mx 183 | example.com: 184 | - SPF: v=spf1 -all 185 | e1.example.com: 186 | - SPF: v=spf1 default=- 187 | e2.example.com: 188 | - CNAME: c1.example.com. 189 | c1.example.com: 190 | - CNAME: c2.example.com. 191 | c2.example.com: 192 | - SPF: v=spf1 a a:c1.example.com -all 193 | - A: 1.2.3.4 194 | mx1.example.com: 195 | - CNAME: '' 196 | e3.example.com: 197 | - SPF: v=spf1 a:mx1.example.com mx:mx1.example.com ~all 198 | -------------------------------------------------------------------------------- /test/testspf.py: -------------------------------------------------------------------------------- 1 | # Author: Stuart D. Gathman 2 | # Copyright 2006 Business Management Systems, Inc. 3 | 4 | # This module is free software, and you may redistribute it and/or modify 5 | # it under the same terms as Python itself, so long as this copyright message 6 | # and disclaimer are retained in their original form. 7 | 8 | # Run SPF test cases in the YAML format specified by the SPF council. 9 | from __future__ import print_function 10 | import unittest 11 | import socket 12 | import sys 13 | import spf 14 | import re 15 | try: 16 | import yaml 17 | except: 18 | print("yaml can be found at http://pyyaml.org/") 19 | print("Tested with PYYAML 3.04 up to 5.1.2") 20 | raise 21 | 22 | zonedata = {} 23 | RE_IP4 = re.compile(r'\.'.join( 24 | [r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)+'$') 25 | 26 | def DNSLookup(name,qtype,strict=True,timeout=None,level=0): 27 | try: 28 | #print name,qtype 29 | timeout = True 30 | 31 | # emulate pydns-2.3.0 label processing 32 | a = [] 33 | for label in name.split('.'): 34 | if label: 35 | if len(label) > 63: 36 | raise spf.TempError('DNS label too long') 37 | a.append(label) 38 | name = '.'.join(a) 39 | 40 | for i in zonedata[name.lower()]: 41 | if i == 'TIMEOUT': 42 | if timeout: 43 | raise spf.TempError('DNS timeout') 44 | return 45 | t,v,n = i 46 | if t == qtype: 47 | timeout = False 48 | if v == 'TIMEOUT': 49 | if t == qtype: 50 | raise spf.TempError('DNS timeout') 51 | continue 52 | # keep test zonedata human readable, but translate to simulate pydns 53 | if t == 'AAAA': 54 | v = bytes(socket.inet_pton(socket.AF_INET6,v)) 55 | elif t in ('TXT','SPF'): 56 | v = tuple([s.encode('utf-8') for s in v]) 57 | yield ((n,t),v) 58 | # emulate CNAME additional info for 1 level 59 | if not level and t == 'CNAME': 60 | for j in DNSLookup(v,qtype,strict,timeout,1): 61 | yield j 62 | except KeyError: 63 | if name.startswith('error.'): 64 | raise spf.TempError('DNS timeout') 65 | 66 | spf.DNSLookup = DNSLookup 67 | 68 | class SPFTest(object): 69 | def __init__(self,testid,scenario,data={}): 70 | self.id = testid 71 | self.scenario = scenario 72 | self.explanation = None 73 | self.bestguess = None 74 | self.spec = None 75 | self.header = None 76 | self.strict = True 77 | self.receiver = None 78 | self.comment = [] 79 | if 'result' not in data: 80 | print(testid,'missing result') 81 | for k,v in list(data.items()): 82 | setattr(self,k,v) 83 | if type(self.comment) is str: 84 | self.comment = self.comment.splitlines() 85 | 86 | def getrdata(r,name): 87 | "Unpack rdata given as list of maps to list of tuples." 88 | txt = [] # generated TXT records 89 | gen = True 90 | for m in r: 91 | try: 92 | for i in list(m.items()): 93 | t,v = i 94 | if t in ('TXT','SPF') and type(v) == str: 95 | v = (v,) 96 | if t == 'TXT': 97 | gen = False # no generated TXT records 98 | elif t == 'SPF' and gen: 99 | txt.append(('TXT',v,name)) 100 | if v != ('NONE',): 101 | yield (t,v,name) 102 | except: 103 | yield m 104 | if gen: 105 | for i in txt: 106 | yield i 107 | 108 | def loadZone(data): 109 | return dict([ 110 | (d.lower(), list(getrdata(r,d))) for d,r in list(data['zonedata'].items()) 111 | ]) 112 | 113 | class SPFScenario(object): 114 | def __init__(self,filename=None,data={}): 115 | self.id = None 116 | self.filename = filename 117 | self.comment = [] 118 | self.zonedata = {} 119 | self.tests = {} 120 | if data: 121 | self.zonedata= loadZone(data) 122 | #print self.zonedata 123 | for t,v in list(data['tests'].items()): 124 | self.tests[t] = SPFTest(t,self,v) 125 | if 'id' in data: 126 | self.id = data['id'] 127 | if 'comment' in data: 128 | self.comment = data['comment'].splitlines() 129 | 130 | def addDNS(self,name,val): 131 | self.zonedata.setdefault(name,[]).append(val) 132 | 133 | def addTest(self,test): 134 | self.tests[test.id] = test 135 | 136 | def loadYAML(fname): 137 | "Load testcases in YAML format. Return map of SPFTests by name." 138 | fp = open(fname,'rb') 139 | try: 140 | tests = {} 141 | for s in yaml.safe_load_all(fp): 142 | scenario = SPFScenario(fname,data=s) 143 | for k,v in list(scenario.tests.items()): 144 | tests[k] = v 145 | return tests 146 | finally: fp.close() 147 | 148 | oldresults = { 'unknown': 'permerror', 'error': 'temperror' } 149 | 150 | verbose = 0 151 | warnings = [] 152 | 153 | class SPFTestCase(unittest.TestCase): 154 | 155 | def __init__(self,t): 156 | unittest.TestCase.__init__(self) 157 | self._spftest = t 158 | self._testMethodName = 'runTest' 159 | self._testMethodDoc = str(t.spec) 160 | 161 | def id(self): 162 | t = self._spftest 163 | return t.id + ' in ' + t.scenario.filename 164 | 165 | def setUp(self): 166 | global zonedata 167 | self.savezonedata = zonedata 168 | 169 | def tearDown(self): 170 | global zonedata 171 | zonedata = self.savezonedata 172 | 173 | def warn(self,msg): 174 | global warnings 175 | warnings.append(msg) 176 | 177 | def runTest(self): 178 | global zonedata 179 | t = self._spftest 180 | zonedata = t.scenario.zonedata 181 | q = spf.query(i=t.host, s=t.mailfrom, h=t.helo, strict=t.strict) 182 | q.verbose = verbose 183 | q.set_default_explanation('DEFAULT') 184 | res,code,exp = q.check() 185 | #print q.mechanism 186 | if res in oldresults: 187 | res = oldresults[res] 188 | ok = True 189 | msg = '' 190 | if res != t.result and res not in t.result: 191 | if verbose: msg += ' '.join((t.result,'!=',res))+'\n' 192 | ok = False 193 | elif res != t.result and res != t.result[0]: 194 | self.warn("WARN: %s in %s, %s: %s preferred to %s" % ( 195 | t.id,t.scenario.filename,t.spec,t.result[0],res)) 196 | if t.explanation is not None and t.explanation != exp: 197 | if verbose: msg += ' '.join((t.explanation,'!=',exp))+'\n' 198 | ok = False 199 | if t.header: 200 | self.assertEqual(t.header,q.get_header(res,receiver=t.receiver)) 201 | if q.perm_error and t.bestguess is not None \ 202 | and q.perm_error.ext[0] != t.bestguess: 203 | ok = False 204 | if not ok: 205 | print('Session cache:',q.cache) 206 | if verbose and not t.explanation: msg += exp+'\n' 207 | if verbose > 1: msg += t.scenario.zonedata 208 | self.fail(msg+"%s in %s failed, %s" % (t.id,t.scenario.filename,t.spec)) 209 | 210 | class SPFTestCases(unittest.TestCase): 211 | 212 | def testInvalidSPF(self): 213 | i, s, h = '1.2.3.4','sender@domain','helo' 214 | q = spf.query(i=i, s=s, h=h, receiver='localhost', strict=False) 215 | res,code,txt = q.check('v=spf1...') 216 | self.assertEqual('none',res) 217 | q = spf.query(i=i, s=s, h=h, receiver='localhost', strict=2) 218 | res,code,txt = q.check('v=spf1...') 219 | self.assertEqual('ambiguous',res) 220 | 221 | def makeSuite(filename): 222 | suite = unittest.TestSuite() 223 | for t in loadYAML(filename).values(): 224 | suite.addTest(SPFTestCase(t)) 225 | return suite 226 | 227 | def docsuite(): 228 | suite = unittest.defaultTestLoader.loadTestsFromTestCase(SPFTestCases) 229 | try: 230 | import authres 231 | except: 232 | print("no authres module: skipping doctests") 233 | return suite 234 | import doctest 235 | suite.addTest(doctest.DocTestSuite(spf)) 236 | return suite 237 | 238 | def suite(skipdoc=False): 239 | suite = docsuite() 240 | suite.addTest(makeSuite('test.yml')) 241 | suite.addTest(makeSuite('rfc7208-tests.yml')) 242 | suite.addTest(makeSuite('rfc4408-tests.yml')) 243 | return suite 244 | 245 | if __name__ == '__main__': 246 | tc = None 247 | doctest = False 248 | for i in sys.argv[1:]: 249 | if i == '-v': 250 | verbose += 1 251 | continue 252 | if i == '-d': 253 | doctest = True 254 | continue 255 | # a specific test selected by id from YAML files 256 | if not tc: 257 | tc = unittest.TestSuite() 258 | t0 = loadYAML('rfc7208-tests.yml') 259 | t1 = loadYAML('rfc4408-tests.yml') 260 | t2 = loadYAML('test.yml') 261 | if i in t0: 262 | tc.addTest(SPFTestCase(t0[i])) 263 | if i in t1: 264 | tc.addTest(SPFTestCase(t1[i])) 265 | if i in t2: 266 | tc.addTest(SPFTestCase(t2[i])) 267 | if not tc: 268 | # load zonedata for doctests 269 | with open('doctest.yml','rb') as fp: 270 | zonedata = loadZone(next(yaml.safe_load_all(fp))) 271 | if doctest: 272 | tc = docsuite() # doctests only 273 | else: 274 | tc = suite() # all tests, including doctests 275 | runner = unittest.TextTestRunner() 276 | res = runner.run(tc) 277 | for s in warnings: 278 | print(s) 279 | if not res.wasSuccessful(): 280 | sys.exit(1) 281 | -------------------------------------------------------------------------------- /type99.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Type 99 (SPF) DNS conversion script. 3 | 4 | Copyright (c) 2005,2006 Stuart Gathman 5 | Portions Copyright (c) 2007 Scott Kitterman 6 | This module is free software, and you may redistribute it and/or modify 7 | it under the same terms as Python itself, so long as this copyright message 8 | and disclaimer are retained in their original form. 9 | 10 | IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, 11 | SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF 12 | THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 13 | DAMAGE. 14 | 15 | THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 17 | PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, 18 | AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, 19 | SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 20 | 21 | For more information about SPF, a tool against email forgery, see 22 | http://www.open-spf.org/""" 23 | 24 | # Copy Bind zonefiles to stdout, removing TYPE99 RRs and 25 | # adding a TYPE99 RR for each TXT RR encountered. 26 | # This can be used to maintain SPF records as TXT RRs 27 | # in a zonefile until Bind is patched/upgraded to recognize 28 | # the SPF RR. After adding/changing/deleting TXT RRs, 29 | # filtering through this script will refresh the TYPE99 RRs. 30 | # 31 | # $Log$ 32 | # Revision 1.4.4.4 2011/10/27 04:44:58 kitterma 33 | # Update type99.py to work with 2.6, 2.7, and 3.2: 34 | # - raise ... as ... 35 | # - Add filter to stdin processing 36 | # - Modernize output print to use format to get consistent python/python3 output 37 | # 38 | # Revision 1.4.4.3 2008/03/26 19:01:07 kitterma 39 | # Capture Type99.py improvements from trunk. SF #1257140 40 | # 41 | # Revision 1.9 2008/03/26 18:56:42 kitterma 42 | # Update Type99 script to correctly parse multi-string single line TXT records. 43 | # Multi-string/multi-line still fails. 44 | # 45 | # Revision 1.8 2007/01/26 05:06:41 customdesigned 46 | # Tweaks for epydoc. 47 | # Design for test in type99.py, test cases. 48 | # Null byte test case for quote_value. 49 | # 50 | # Revision 1.7 2007/01/25 21:59:29 kitterma 51 | # Update comments to match bug fix. Include copyright statements. Update sheband. 52 | # 53 | # Revision 1.6 2007/01/25 21:51:45 kitterma 54 | # Fix type99 script for multi-line support (Fixes sourceforge #1257140) 55 | # 56 | # Revision 1.5 2006/12/16 20:45:23 customdesigned 57 | # Move dns drivers to package directory. 58 | # 59 | # Revision 1.4 2005/08/26 20:53:38 kitterma 60 | # Fixed typo in type99 script 61 | # 62 | # Revision 1.3 2005/08/19 19:06:49 customdesigned 63 | # use note_error method for consistent extended processing. 64 | # Return extended result, strict result in self.perm_error 65 | # 66 | # Revision 1.2 2005/07/17 02:46:03 customdesigned 67 | # Use of expand not needed. 68 | # 69 | # Revision 1.1 2005/07/17 02:39:42 customdesigned 70 | # Utility to maintain TYPE99 copies of SPF TXT RRs. 71 | # 72 | 73 | import sys 74 | import fileinput 75 | import re 76 | 77 | def dnstxt(txt): 78 | "Convert data into DNS TXT format (sequence of pascal strings)." 79 | r = [] 80 | while txt: 81 | s,txt = txt[:255],txt[255:] 82 | r.append(chr(len(s))+s) 83 | return ''.join(r) 84 | 85 | RE_TXT = re.compile(r'^(?P.*\s)TXT\s"(?Pv=spf1.*)"(?P.*)', 86 | re.DOTALL) 87 | RE_TYPE99 = re.compile(r'\sTYPE99\s') 88 | 89 | def filter(fin): 90 | for line in fin: 91 | if not RE_TYPE99.search(line): 92 | yield line 93 | m = RE_TXT.match(line) 94 | if not m: 95 | left = line.split('(') 96 | try: 97 | right = left[1].split(')') 98 | except IndexError as errmsg: 99 | right = left[0].split(')') 100 | if len(left) == 2: 101 | right = left[1] 102 | else: 103 | left = line.split('(') 104 | right = left[0] 105 | middlelist = right[0].split('"') 106 | middle = '' 107 | for fragment in middlelist: 108 | if fragment != ' ': 109 | middle = middle + fragment 110 | line = left[0] + '"' + middle + '"' 111 | m = RE_TXT.match(line) 112 | if m: 113 | phrase = dnstxt(m.group('str')) 114 | dns_string = '' 115 | list = m.group('str') 116 | for st in list: 117 | dns_string += st 118 | phrase = dnstxt(dns_string) 119 | s = m.group('rr') + 'TYPE99 \# %i '%len(phrase) 120 | yield s+''.join(["%02x"%ord(c) for c in phrase])+m.group('eol') 121 | 122 | USAGE="""Usage:\t%s phrase 123 | %s -