Module spf
[hide private]
[frames] | no frames]

Module spf

source code

SPF (Sender Policy Framework) implementation.

Copyright (c) 2003, Terence Way
Portions Copyright (c) 2004,2005,2006 Stuart Gathman <stuart@bmsi.com>
Portions Copyright (c) 2005,2006 Scott Kitterman <scott@kitterman.com>
This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form.

IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.

THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

For more information about SPF, a tool against email forgery, see
    http://www.openspf.org/

For news, bugfixes, etc. visit the home page for this implementation at
    http://www.wayforward.net/spf/
    http://sourceforge.net/projects/pymilter/



Classes [hide private]
  bool
bool(x) -> bool
  AmbiguityWarning
SPF Warning - ambiguous results
  TempError
Temporary SPF error
  PermError
Permanent SPF error
  query
A query object keeps the relevant information about a single SPF query:

Functions [hide private]
  DNSLookup(name, qtype, strict=True)
  check2(i, s, h, local=None, receiver=None)
Test an incoming MAIL FROM:<s>, from a client with ip address i.
  check(i, s, h, local=None, receiver=None)
Test an incoming MAIL FROM:<s>, from a client with ip address i.
  split_email(s, h)
Given a sender email s and a HELO domain h, create a valid tuple (l, d) local-part and domain-part.
  quote_value(s)
Quote the value for a key-value pair in Received-SPF header field if needed.
  parse_mechanism(str, d)
Breaks A, MX, IP4, and PTR mechanisms into a (name, domain, cidr,cidr6) tuple.
  reverse_dots(name)
Reverse dotted IP addresses or domain names.
  domainmatch(ptrs, domainsuffix)
grep for a given domain suffix against a list of validated PTR domain names.
  addr2bin(str)
Convert a string IPv4 address into an unsigned integer.
  bin2long6(str)
Convert binary IP6 address into an unsigned Python long integer.
  expand_one(expansion, str, joiner)
  split(str, delimiters, joiner=None)
Split a string into pieces by a set of delimiter characters.
  insert_libspf_local_policy(spftxt, local=None)
Returns spftxt with local inserted just before last non-fail mechanism.
  _test()

Variables [hide private]
  __author__ = 'Terence Way'
  __email__ = 'terry@wayforward.net'
  __version__ = '2.1: January 22, 2007'
  MODULE = 'spf'
  USAGE = 'To check an incoming mail request:\n % python spf.p...
  RE_SPF = <_sre.SRE_Pattern object at 0xb7c9a5a0>
  RE_MODIFIER = <_sre.SRE_Pattern object at 0xb7f13da0>
  PAT_CHAR = '%(%|_|-|(\\{[^\\}]*\\}))'
  RE_CHAR = <_sre.SRE_Pattern object at 0xb7cb5210>
  RE_ARGS = <_sre.SRE_Pattern object at 0xb7f1db60>
  RE_DUAL_CIDR = <_sre.SRE_Pattern object at 0xb7f1dc50>
  RE_CIDR = <_sre.SRE_Pattern object at 0xb7c859c0>
  PAT_IP4 = '(?:\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.(?:\\d...
  RE_IP4 = <_sre.SRE_Pattern object at 0x85f8db0>
  RE_TOPLAB = <_sre.SRE_Pattern object at 0x85f5fd8>
  RE_DOT_ATOM = <_sre.SRE_Pattern object at 0xb7c9e520>
  RE_IP6 = <_sre.SRE_Pattern object at 0x8686e00>
  JOINERS = {'s': '.', 'l': '.'}
  RESULTS = {'permerror': 'permerror', 'none': 'none', 'ambiguou...
  EXPLANATIONS = {'permerror': 'permanent error in processing', 'none...
  DELEGATE = None
  DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
  TRUSTED_FORWARDERS = 'v=spf1 ?include:spf.trusted-forwarder.org -all'
  MAX_LOOKUP = 10
  MAX_MX = 10
  MAX_PTR = 10
  MAX_CNAME = 10
  MAX_RECURSION = 20
  ALL_MECHANISMS = ('a', 'mx', 'ptr', 'exists', 'include', 'ip4', 'ip6'...
  COMMON_MISTAKES = {'all.': 'all', 'prt': 'ptr', 'ipv6': 'ip6', 'ipv4':...
  q = query(i= i, s= s, h= h, receiver= socket.gethostname...
  False = False
  True = True

Function Details [hide private]

DNSLookup(name, qtype, strict=True)

source code 
None

check2(i, s, h, local=None, receiver=None)

source code 

Test an incoming MAIL FROM:<s>, from a client with ip address i. h is the HELO/EHLO domain name. This is the RFC4408 compliant pySPF2.0 interface. The interface returns an SPF result and explanation only. SMTP response codes are not returned since RFC 4408 does not specify receiver policy. Applications updated for RFC 4408 should use this interface.

Returns (result, explanation) where result in ['pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', 'neutral' ].

Example: #>>> check2(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')

check(i, s, h, local=None, receiver=None)

source code 

Test an incoming MAIL FROM:<s>, from a client with ip address i. h is the HELO/EHLO domain name. This is the pre-RFC SPF Classic interface. Applications written for pySPF 1.6/1.7 can use this interface to allow pySPF2 to be a drop in replacement for older versions. With the exception of result codes, performance in RFC 4408 compliant.

Returns (result, code, explanation) where result in ['pass', 'unknown', 'fail', 'error', 'softfail', 'none', 'neutral' ].

Example: #>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')

split_email(s, h)

source code 
Given a sender email s and a HELO domain h, create a valid tuple (l, d) local-part and domain-part.
>>> split_email('', 'wayforward.net')
('postmaster', 'wayforward.net')
>>> split_email('foo.com', 'wayforward.net')
('postmaster', 'foo.com')
>>> split_email('terry@wayforward.net', 'optsw.com')
('terry', 'wayforward.net')

quote_value(s)

source code 
Quote the value for a key-value pair in Received-SPF header field if needed. No quoting needed for a dot-atom value.
>>> quote_value('foo@bar.com')
'"foo@bar.com"'
>>> quote_value('mail.example.com')
'mail.example.com'
>>> quote_value('A:1.2.3.4')
'"A:1.2.3.4"'
>>> quote_value('abc"def')
'"abc\\"def"'
>>> quote_value(r'abc\def')
'"abc\\\\def"'
>>> quote_value('abc..def')
'"abc..def"'
>>> quote_value('')
'""'
>>> quote_value(None)

parse_mechanism(str, d)

source code 

Breaks A, MX, IP4, and PTR mechanisms into a (name, domain, cidr,cidr6) tuple. The domain portion defaults to d if not present, the cidr defaults to 32 if not present.

Examples:
>>> parse_mechanism('a', 'foo.com')
('a', 'foo.com', None, None)
>>> parse_mechanism('exists','foo.com')
('exists', None, None, None)
>>> parse_mechanism('a:bar.com', 'foo.com')
('a', 'bar.com', None, None)
>>> parse_mechanism('a/24', 'foo.com')
('a', 'foo.com', 24, None)
>>> parse_mechanism('A:foo:bar.com/16//48', 'foo.com')
('a', 'foo:bar.com', 16, 48)
>>> parse_mechanism('-exists:%{i}.%{s1}.100/86400.rate.%{d}','foo.com')
('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', None, None)
>>> parse_mechanism('mx:%%%_/.Claranet.de/27','foo.com')
('mx', '%%%_/.Claranet.de', 27, None)
>>> parse_mechanism('mx:%{d}//97','foo.com')
('mx', '%{d}', None, 97)
>>> parse_mechanism('iP4:192.0.0.0/8','foo.com')
('ip4', '192.0.0.0', 8, None)

reverse_dots(name)

source code 

Reverse dotted IP addresses or domain names.

Examples:
>>> reverse_dots('192.168.0.145')
'145.0.168.192'
>>> reverse_dots('email.example.com')
'com.example.email'

domainmatch(ptrs, domainsuffix)

source code 

grep for a given domain suffix against a list of validated PTR domain names.

Examples:
>>> domainmatch(['FOO.COM'], 'foo.com')
1
>>> domainmatch(['moo.foo.com'], 'FOO.COM')
1
>>> domainmatch(['moo.bar.com'], 'foo.com')
0

addr2bin(str)

source code 

Convert a string IPv4 address into an unsigned integer.

Examples:
>>> addr2bin('127.0.0.1')
2130706433L
>>> addr2bin('127.0.0.1') == socket.INADDR_LOOPBACK
1
>>> addr2bin('255.255.255.254')
4294967294L
>>> addr2bin('192.168.0.1')
3232235521L
Unlike DNS.addr2bin, the n, n.n, and n.n.n forms for IP addresses are handled as well:
>>> addr2bin('10.65536')
167837696L
>>> 10 * (2 ** 24) + 65536
167837696
>>> addr2bin('10.93.512')
173867520L
>>> 10 * (2 ** 24) + 93 * (2 ** 16) + 512
173867520

bin2long6(str)

source code 
Convert binary IP6 address into an unsigned Python long integer.

expand_one(expansion, str, joiner)

source code 
None

split(str, delimiters, joiner=None)

source code 

Split a string into pieces by a set of delimiter characters. The resulting list is delimited by joiner, or the original delimiter if joiner is not specified.

Examples:
>>> split('192.168.0.45', '.')
['192', '.', '168', '.', '0', '.', '45']
>>> split('terry@wayforward.net', '@.')
['terry', '@', 'wayforward', '.', 'net']
>>> split('terry@wayforward.net', '@.', '.')
['terry', '.', 'wayforward', '.', 'net']

insert_libspf_local_policy(spftxt, local=None)

source code 

Returns spftxt with local inserted just before last non-fail mechanism. This is how the libspf{2} libraries handle "local-policy".

Examples:
>>> insert_libspf_local_policy('v=spf1 -all')
'v=spf1 -all'
>>> insert_libspf_local_policy('v=spf1 -all','mx')
'v=spf1 -all'
>>> insert_libspf_local_policy('v=spf1','a mx ptr')
'v=spf1 a mx ptr'
>>> insert_libspf_local_policy('v=spf1 mx -all','a ptr')
'v=spf1 mx a ptr -all'
>>> insert_libspf_local_policy('v=spf1 mx -include:foo.co +all','a ptr')
'v=spf1 mx a ptr -include:foo.co +all'
# FIXME: is this right? If so, "last non-fail" is a bogus description. >>> insert_libspf_local_policy('v=spf1 mx ?include:foo.co +all','a ptr') 'v=spf1 mx a ptr ?include:foo.co +all' >>> spf='v=spf1 ip4:1.2.3.4 -a:example.net -all' >>> local='ip4:192.0.2.3 a:example.org' >>> insert_libspf_local_policy(spf,local) 'v=spf1 ip4:1.2.3.4 ip4:192.0.2.3 a:example.org -a:example.net -all'

_test()

source code 
None

Variables Details [hide private]

__author__

None
Value:
'Terence Way'                                                          
      

__email__

None
Value:
'terry@wayforward.net'                                                 
      

__version__

None
Value:
'2.1: January 22, 2007'                                                
      

MODULE

None
Value:
'spf'                                                                  
      

USAGE

None
Value:
'''To check an incoming mail request:
    % python spf.py {ip} {sender} {helo}
    % python spf.py 69.55.226.139 tway@optsw.com mx1.wayforward.net

To test an SPF record:
    % python spf.py "v=spf1..." {ip} {sender} {helo}
    % python spf.py "v=spf1 +mx +ip4:10.0.0.1 -all" 10.0.0.1 tway@foo.
com a    
...                                                                    
      

RE_SPF

None
Value:
^v=spf1$|                                                              
      

RE_MODIFIER

None
Value:
^([a-z][a-z0-9_-\.]*)=                                                 
      

PAT_CHAR

None
Value:
'%(%|_|-|(\\{[^\\}]*\\}))'                                             
      

RE_CHAR

None
Value:
%(%|_|-|(\{[^\}]*\}))                                                  
      

RE_ARGS

None
Value:
([0-9]*)(r?)([^0-9a-zA-Z]*)                                            
      

RE_DUAL_CIDR

None
Value:
//(0|[1-9]\d*)$                                                        
      

RE_CIDR

None
Value:
/(0|[1-9]\d*)$                                                         
      

PAT_IP4

None
Value:
'(?:\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.(?:\\d|[1-9]\\d|1\\d\\d|
2[0-4]\\d|25[0-5])\\.(?:\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.(?:\
\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])'                                
      

RE_IP4

None
Value:
(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25
[0-5])\.(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(?:\d|[1-9]\d|1\d\d|2[0
-4]\d|25[0-5])$                                                        
      

RE_TOPLAB

None
Value:
\.(?:[0-9a-z]*[a-z][0-9a-z]*|[0-9a-z]+-[0-9a-z-]*[0-9a-z])\.?$|%(%|_|-
|(\{[^\}]*\}))                                                         
      

RE_DOT_ATOM

None
Value:
[0-9a-z!#\$%&'\*\+/=\?\^_`\{\}\|~-]+(\.[0-9a-z!#\$%&'\*\+/=\?\^_`\{\}\
|~-]+)*$                                                               
      

RE_IP6

None
Value:
(?:[0-9a-f]{1,4}:){6}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:\d|[1-9]\d|1\d\
d|2[0-4]\d|25[0-5])\.(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(?:\d|[1-9
]\d|1\d\d|2[0-4]\d|25[0-5])\.(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))$|:
:(?:[0-9a-f]{1,4}:){5}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:\d|[1-9]\d|1\d
\d|2[0-4]\d|25[0-5])\.(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(?:\d|[1-
9]\d|1\d\d|2[0-4]\d|25[0-5])\.(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))$|
(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4
}|(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(?:\d|[1-9]\d|1\d\d|2[0-4]\d|
...                                                                    
      

JOINERS

None
Value:
{'s': '.', 'l': '.'}                                                   
      

RESULTS

None
Value:
{'+': 'pass',
 '-': 'fail',
 '?': 'neutral',
 'ambiguous': 'ambiguous',
 'error': 'temperror',
 'fail': 'fail',
 'local': 'local',
 'neutral': 'neutral',
...                                                                    
      

EXPLANATIONS

None
Value:
{'ambiguous': 'No error, but results may vary',
 'fail': 'SPF fail - not authorized',
 'local': 'No SPF result due to local policy',
 'neutral': 'access neither permitted nor denied',
 'none': '',
 'pass': 'sender SPF authorized',
 'permerror': 'permanent error in processing',
 'softfail': 'domain owner discourages use of this host',
...                                                                    
      

DELEGATE

None
Value:
None                                                                  
      

DEFAULT_SPF

None
Value:
'v=spf1 a/24 mx/24 ptr'                                                
      

TRUSTED_FORWARDERS

None
Value:
'v=spf1 ?include:spf.trusted-forwarder.org -all'                       
      

MAX_LOOKUP

None
Value:
10                                                                    
      

MAX_MX

None
Value:
10                                                                    
      

MAX_PTR

None
Value:
10                                                                    
      

MAX_CNAME

None
Value:
10                                                                    
      

MAX_RECURSION

None
Value:
20                                                                    
      

ALL_MECHANISMS

None
Value:
('a', 'mx', 'ptr', 'exists', 'include', 'ip4', 'ip6', 'all')           
      

COMMON_MISTAKES

None
Value:
{'all.': 'all', 'prt': 'ptr', 'ipv6': 'ip6', 'ipv4': 'ip4', 'ip': 'ip4
'}                                                                     
      

q

None
Value:
query(i= i, s= s, h= h, receiver= socket.gethostname(), strict= False) 
      

False

None
Value:
False                                                                  
      

True

None
Value:
True