3 f_\7@@sddlmZdZdZdZdZdZddlZddlZddl Z ddl Z ddl Z yddl j ZWnddlZYnXddlZddlmZydd lmZWn ek rdd lmZYnXy ddlZeekrdd lmZWnNek r yddlZdd lmZWnek red YnXYnXdddZdddZyFddlZddlZddl Ze!ej"dsvdej"_#ej"j#ej"j$d<eZ%WnRddl&Z&e!e&j'dsde&j'_#de&j'j(d<e&j)j*j+e&j)j*_,e&j-eZ%YnXej.dej/Z0ej.dej/Z1dZ2ej.e2Z3ej.dZ4ej.dZ5ej.dZ6ej.dZ7dj8dgdZ9ej.e9dZ:ej.d e2ej/Z;ej.d!d"d#iej/Zd*d+d,d-d*d+d.d/d,d-d0d1d2d3d.d4Z?d5d6d7d8d9d:d;dd? Z@dZAd@ZBdAZCdBZDdBZEdBZFdBZGd ZHd ZIdCZJdZKdFdIdIdJdKdLZLGdMdNdNeMZNGdOdPdPeMZOGdQdRdReMZPddeIdSd fdTdUZQddVdWZRGdXdYdYeSZTdZd[ZUd\d]ZVd^d_ZWd`daZXdbdcZYdddeZZddfdgZ[ddhdiZ\ej]ddCkrdjdkZ^ndldkZ^dmdnZ_e`dokrddlaZay&eajaejbdpddqdrdsdtg\ZcZbWnDeajdk rPZez$eeeeeeejfdCWYddZe[eXnXdSZgd ZhxFecD]>\ZiZjeidkrvd ZgeidkrekejZhneidkr`eeq`Welebdkreee_nelebdpkrZy*eTd{d|d}e jmd~ZneenjoebdWnZeOk r(ZpzedepWYddZp[pXn.ePk rTZpzedepWYddZp[pXnXnjelebdkreb\ZqZrZseTeqerese jmegehdZnenjRZtedetenjuetdd0kredenjvenjuenjwrenjwjxredenjwjxenjyrxenjyD]ZzeezqWnelebdkrebdpd\ZqZrZseTeqerese jmdSegdZnenjRebdZtedetenjuetdd0krxedenjvenjuenjwrenjwjxredenjwjxenjyrx"enjyD]ZzeezqWneedS))print_functionz,Terence Way, Stuart Gathman, Scott Kittermanzpyspf@openspf.orgz2.0.14spfaTo check an incoming mail request: % python spf.py [-v] {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] "v=spf1..." {ip} {sender} {helo} % python spf.py "v=spf1 +mx +ip4:10.0.0.1 -all" 10.0.0.1 tway@foo.com a To fetch an SPF record: % python spf.py {domain} % python spf.py wayforward.net To test this script (and to output this usage message): % python spf.py N)reduce)Message)Bytesz;ipaddr module required: http://code.google.com/p/ipaddr-py/TcCsytj|||d}|j}|jddkr|dkr8tdytj||d|d}|j}Wn4tjk r}ztdt|WYdd}~XnX|jd d kr|jd d krtd |jd dt|jd dd|j Ddd|j DSt k r}ztdt|WYdd}~Xnhtk rP}ztdt|WYdd}~Xn6tjk r}ztdt|WYdd}~XnXdS)N)qtypetimeoutZtcTzNDNS: Truncated UDP Reply, SPF records should fit in a UDP packet, retrying TCPZtcp)rZprotocolr zDNS: TCP Fallback error: ZrcoderzError: Zstatusz RCODE: cSs$g|]}|d|df|dfqS)nametypenamedata).0arr/usr/lib/python3.6/spf.py rsz#DNSLookup_pydns..cSs$g|]}|d|df|dfqS)r r rr)rrrrrrtszDNS ) DNSZ DnsRequestreqheaderAmbiguityWarningZDNSError TempErrorstrIOErroranswersZ additionalAttributeError)r rstrictr rZrespxrrrDNSLookup_pydns]s. "$   rcCsVg}ytjj||}x|D]}|dks.|dkrD|j||f|jfq|dkrh|j||f|j|jffq|dkr|j||f|jjdfq|dks|dkr|j||f|j fqWWntjj k rYntjj k rYnntj j k r}ztdt|WYdd}~Xn8tjjk rP}ztdt|WYdd}~XnX|S) NAAAAAMXPTRTTXTSPFzDNS )dnsZresolverqueryappendZaddressZ preferenceZexchangetargetZto_textZstringsZNoAnswerZNXDOMAINZ exceptionZTimeoutrrZ NoNameservers)r rZ tcpfallbackr ZretValrZrdatarrrrDNSLookup_dnspython~s*  "r+r&cs^v=spf1$|^v=spf1 z^([a-z][a-z0-9_\-\.]*)=z%(%|_|-|(\{[^\}]*\}))z(?, from a client with ip address i. h is the HELO/EHLO domain name. This is the RFC4408/7208 compliant pySPF2.0 interface. The interface returns an SPF result and explanation only. SMTP response codes are not returned since neither RFC 4408 nor RFC 7208 does specify receiver policy. Applications updated for RFC 4408 and RFC 7208 should use this interface. The maximum time, in seconds, this function is allowed to run before a TempError is returned is controlled by querytime. When set to 0 the timeout parameter (default 20 seconds) controls the time allowed for each DNS lookup. When set to a non-zero value, it total time for all processing related to the SPF check is limited to querytime (default 20 seconds as recommended in RFC 7208, paragraph 4.6.4). 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') )ir1hr9receiverr verbose querytime)r(check) rYr1rZr9r[r r\r]res_exprrrcheck2/s rbc CsFt||||||dj\}}}|dkr,d}n|dkr<|dk|||fS)a?Test an incoming MAIL FROM:, from a client with ip address i. h is the HELO/EHLO domain name. This is the pre-RFC SPF Classic interface. Applications written for pySPF 1.6/1.7 can use this interface to allow pySPF2 to be a drop in replacement for older versions. With the exception of result codes, performance in RFC 4408 compliant. Returns (result, code, explanation) where result in ['pass', 'unknown', 'fail', 'error', 'softfail', 'none', 'neutral' ]. Example: #>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com') )rYr1rZr9r[r\r6rAZtempfailr@)r(r^) rYr1rZr9r[r\r_coderarrrr^Hs r^c@s:eZdZdZdddeddfddZdd Zd d Zd d ZddZ ddZ e fddZ dHddZ ddZddZddZddZddZd d!Zd"d#ZdId$d%Zd&d'ZdJd)d*Zd+d,ZdKd.d/Zd0d1Zd2d3ZdLddMddNddOddPddQddRddSdiZdTd9d:Zd;d<Zd=d>Zd?d@Z dAdBZ!dUdDdEZ"dFdGZ#dS)Vr(ajA query object keeps the relevant information about a single SPF query: i: ip address of SMTP client in dotted notation s: sender declared in MAIL FROM:<> l: local part of sender s d: current domain, initially domain part of sender s h: EHLO/HELO domain v: 'in-addr' for IPv4 clients and 'ip6' for IPv6 clients t: current timestamp p: SMTP client domain name o: domain part of sender s r: receiver c: pretty ip address (different from i for IPv6) This is also, by design, the same variables used in SPF macro expansion. Also keeps cache: DNS cache. NTFrc Cs|||_|_| r*|r*d||_d|_nd|_t||\|_|_tttj|_ |j|_ d|_ |rn||_ nd|_ i|_ tt|_tt|_||_d|_d|_||_||_| |_| dkr| |_d|_d|_|r|j|d|_||_d|_dS)Nz postmaster@helomailfromrArT)r1rZident split_emailr0orinttimetdprcachedict EXPLANATIONSdefexpsexps libspf_locallookups void_lookupsrr r]Ztimeripaddrset_ipdefault_modifierr\authserv) rRrYr1rZr9r[rr r\r]rrrrNss<     zquery.__init__cCstd|||fdS)Nz %s: %s "%s")print)rRrPrlrrrrlogsz query.logcCsFd|_|jdkrg|_d}n|jdkr6g|_d}ny6ytj||_Wn tk rhtj||_YnXWn.tk r}ztt |WYdd}~XnX|jj dkr|jj rtj |jj |_d}qd}nd}t |j|_ |rd|_d|_|jrd jt|jjjd d j|_d |_n$d |_d|_|jr<|jj|_d|_dS)z$Set connect ip, and ip6 or ip4 mode.FlistZlist6TNr"rJr/:rBr!zin-addr )iplistlower ipaddressZ ip_addressrwrZ IPAddress ValueErrorrXrversionZ ipv4_mappedZ IPv4Addresscr!vjoinr}ZexplodedreplaceupperrYcidrmax)rRrYrJrrrrrxs@      z query.set_ipcCs.|j}|j}xdD]}|||<|||<qWdS)Nr5r3r6)r5r3r6)rsrr)rRrarsrrrYrrrset_default_explanations  zquery.set_default_explanationcCs |j}xdD] }|||<q WdS)Nr5r3r6)r5r3r6)rs)rRrarsrYrrrset_explanations zquery.set_explanationcCsh|jsb|j}|sd|_nH|j|kr.|j|_n4d|j}x(|D]}|j|r>||_Pq>W|d|_|jS)NrAr/r)rmvalidated_ptrsrlendswith)rRrmZsfxrlrrrgetps      z query.getpcCshtj|jdrdS|j}|j|\}}}|dkr^|jrP|jjrP|jj\}}}nd \}}||_|||fS) zReturn a best guess based on a default SPF record. >>> q = query('1.2.3.4','','SUPERVISION1',receiver='example.com') >>> q.best_guess()[0] 'none' r r8rBr6r4)r8rrB)r4r) RE_TOPLABsplitrl perm_errorr^rQ)rRrZpernrerrr best_guessszquery.best_guesscCsBg|_d|_d|_d|_i|_yzd|_|sL|j|j}|jrL|j d|j||r\|j dd}|j rr|rrt ||j }|j ||jd}|jr||j_|j|Stk r}z.|j|_|jr|jj|jdddt|fSd}~Xn\tk r<}z>|js||_|j|_|jr |jj|jd d d t|fSd}~XnXdS) a Returns (result, mta-status-code, explanation) where result in ['fail', 'softfail', 'neutral' 'permerror', 'pass', 'temperror', 'none'] Examples: >>> q = query(s='strong-bad@email.example.com', ... h='mx.example.org', i='192.0.2.3') >>> q.check(spf='v=spf1 ?all') ('neutral', 250, 'access neither permitted nor denied') >>> q.check(spf='v=spf1 redirect=controlledmail.com exp=_exp.controlledmail.com') ('fail', 550, 'SPF fail - not authorized') >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ?all moo') ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo') >>> q.check(spf='v=spf1 ip4:192.0.0.n ?all') ('permerror', 550, 'SPF Permanent Error: Invalid IP4 address: ip4:192.0.0.n') >>> q.check(spf='v=spf1 ip4:192.0.2.3 ip4:192.0.0.n ?all') ('permerror', 550, 'SPF Permanent Error: Invalid IP4 address: ip4:192.0.0.n') >>> q.check(spf='v=spf1 ip6:2001:db8:ZZZZ:: ?all') ('permerror', 550, 'SPF Permanent Error: Invalid IP6 address: ip6:2001:db8:ZZZZ::') >>> q.check(spf='v=spf1 =a ?all moo') ('permerror', 550, 'SPF Permanent Error: Unknown qualifier, RFC 4408 para 4.6.1, found in: =a') >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ~all') ('pass', 250, 'sender SPF authorized') >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo=') ('pass', 250, 'sender SPF authorized') >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all match.sub-domains_9=yes') ('pass', 250, 'sender SPF authorized') >>> q.strict = False >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo') ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo') >>> q.perm_error.ext ('pass', 250, 'sender SPF authorized') >>> q.strict = True >>> q.check(spf='v=spf1 ip4:192.1.0.0/16 moo -all') ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo') >>> q.check(spf='v=spf1 ip4:192.1.0.0/16 ~all') ('softfail', 250, 'domain owner discourages use of this host') >>> q.check(spf='v=spf1 -ip4:192.1.0.0/6 ~all') ('fail', 550, 'SPF fail - not authorized') # Assumes DNS available >>> q.check() ('none', 250, '') >>> q.check(spf='v=spf1 ip4:1.2.3.4 -a:example.net -all') ('fail', 550, 'SPF fail - not authorized') >>> q.libspf_local='ip4:192.0.2.3 a:example.org' >>> q.check(spf='v=spf1 ip4:1.2.3.4 -a:example.net -all') ('pass', 250, 'sender SPF authorized') >>> q.check(spf='v=spf1 ip4:1.2.3.4 -all exp=_exp.controlledmail.com') ('fail', 550, 'Controlledmail.com does not send mail from itself.') >>> q.check(spf='v=spf1 ip4:1.2.3.4 ?all exp=_exp.controlledmail.com') ('neutral', 250, 'access neither permitted nor denied') Nrtop  r7izSPF Temporary Error: r6i&zSPF Permanent Error: )rPr mechanismrvoptionsrudns_spfrlr\r|rrtinsert_libspf_local_policycheck1rQrrOprobr)rrX)rRrZrcrrrrr^sBF    z query.checkcCs|tkr|jrtdtdy(z|j|}|_|j||S||_XWnFtk r}z*|j|_|j rt|j j |j ddd|fSd}~XnXdS)NzToo many levels of recursionr;rzSPF Ambiguity Warning: %s) MAX_RECURSIONrAssertionErrorrXrlcheck0rrOrrPr))rRrdomain recursionZtmprrrrros  z query.check1cGsP|jrt||jsJy t|Wn(tk rH}z ||_WYdd}~XnX|jS)N)rrXr)rRrOrrrr note_errors zquery.note_errorcCs"tj|drtd||j|S)zvalidate and expand domain-specr zInvalid domain found (use FQDN)r)rrrXexpand)rRargrrr expand_domains zquery.expand_domaincCs|jdr"|jd||dd!}t||j\}}}}|r^tj|d}|rZ|dd}nd}|tkrz|jd|t|}|dkrtj|r|jd |}d }|d"kr|dkrd }n|d krt d ||dkrd}n|dkrt d||j dkr|}n|d ks tj|r||d kr,|jd|d |}}|dk r@t d||dkrPd }n|d krdt d |tj|st d|n|dkr|dk rt d||dkrd}n|dkrt d|t j|st d|n.|dk s|dk r|t krt d||j }|d#kr|dkr*| r*t d||j|}|sDt d||dkrx||jkrx|dkrnt d|t d||||||fS|dkr|jdr|jd||t kr|||||fS|ddt kr|jd |}n |jd|}|||||fS)$a Parse and validate a mechanism. Returns mech,m,arg,cidrlength,result Examples: >>> q = query(s='strong-bad@email.example.com.', ... h='mx.example.org', i='192.0.2.3') >>> q.validate_mechanism('A') ('A', 'a', 'email.example.com', 32, 'pass') >>> q = query(s='strong-bad@email.example.com', ... h='mx.example.org', i='192.0.2.3') >>> q.validate_mechanism('A//64') ('A//64', 'a', 'email.example.com', 32, 'pass') >>> q.validate_mechanism('A/24//64') ('A/24//64', 'a', 'email.example.com', 24, 'pass') >>> q.validate_mechanism('?mx:%{d}/27') ('?mx:%{d}/27', 'mx', 'email.example.com', 27, 'neutral') >>> try: q.validate_mechanism('ip4:1.2.3.4/247') ... except PermError as x: print(x) Invalid IP4 CIDR length: ip4:1.2.3.4/247 >>> try: q.validate_mechanism('ip4:1.2.3.4/33') ... except PermError as x: print(x) Invalid IP4 CIDR length: ip4:1.2.3.4/33 >>> try: q.validate_mechanism('a:example.com:8080') ... except PermError as x: print(x) Invalid domain found (use FQDN): example.com:8080 >>> try: q.validate_mechanism('ip4:1.2.3.444/24') ... except PermError as x: print(x) Invalid IP4 address: ip4:1.2.3.444/24 >>> try: q.validate_mechanism('ip4:1.2.03.4/24') ... except PermError as x: print(x) Invalid IP4 address: ip4:1.2.03.4/24 >>> try: q.validate_mechanism('-all:3030') ... except PermError as x: print(x) Invalid all mechanism format - only qualifier allowed with all: -all:3030 >>> q.validate_mechanism('-mx:%%%_/.Clara.de/27') ('-mx:%%%_/.Clara.de/27', 'mx', '% /.Clara.de', 27, 'fail') >>> q.validate_mechanism('~exists:%{i}.%{s1}.100/86400.rate.%{d}') ('~exists:%{i}.%{s1}.100/86400.rate.%{d}', 'exists', '192.0.2.3.com.100/86400.rate.email.example.com', 32, 'softfail') >>> q.validate_mechanism('a:mail.example.com.') ('a:mail.example.com.', 'a', 'mail.example.com', 32, 'pass') >>> try: q.validate_mechanism('a:mail.example.com,') ... except PermError as x: print(x) Do not separate mechnisms with commas: a:mail.example.com, >>> q = query(s='strong-bad@email.example.com', ... h='mx.example.org', i='2001:db8:1234::face:b007') >>> q.validate_mechanism('A//64') ('A//64', 'a', 'email.example.com', 64, 'pass') >>> q.validate_mechanism('A/16') ('A/16', 'a', 'email.example.com', 128, 'pass') >>> q.validate_mechanism('A/16//48') ('A/16//48', 'a', 'email.example.com', 48, 'pass') ,z%Do not separate mechnisms with commasNr rr2zUnknown mechanism foundrz'Use the ip4 mechanism for ip4 addressesrIrErzInvalid IP4 CIDR lengthrzInvalid IP6 CIDR lengthrJz Missing IP4zDual CIDR not allowedzInvalid IP4 addresszInvalid IP6 addresszCIDR not allowedrFrGrHzimplicit exists not allowedz empty domain:zinclude has trivial recursionz include mechanism missing domainrKrz>Invalid all mechanism format - only qualifier allowed with allz0Unknown qualifier, RFC 4408 para 4.6.1, found inr)rrE)rrErFrGrH)rrparse_mechanismrlRESULTSgetCOMMON_MISTAKESRE_IP4matchrXrRE_IP6ALL_MECHANISMSrrcount)rRrPmr cidrlengthZ cidr6lengthresultrrrrvalidate_mechanismsF                                       zquery.validate_mechanismc Cs|sddtdfS|jd}|djdkrP|jdkrBtd|jddtdfSdd |dd D}|j}d }d }g}g}x|D]}tj|dd } t| d kr|j |j |q| \} } | |kr| d krt d||j d| ||j | | dkrV| st d| |j | } | ry&|j| } | rD| rD|j| Wn YnXq| d kr|j|j | }|st d| q| dkr|jdkrtd|j r|jr|j| } tj| |}q| dkr|sx4| jdD]} | rd|j| <qWq|j| dqWxt|D]\}} } }}| dkr|j|j| }|jr`|jd| ||j|| |d\}}}|dkrP|dkr|j d| |d }qq| dkrPq| dkr|jyt|j| ddkrPWntk rYnXq| dkr2|j|j|j| |j|r$Pn| dkr\|j|j|j| |r$Pn| d kr|jd!kr$y|j| g|rPWn"tj k rt d"|YnXnv| d#kr|jd#kr$y|j| g|rPWn"tj k rt d"|YnXn$| d$kr|jt!|j"| rPqW|r|j|}|sJt d%||jr`|jd |||sxt#|j$|_i|_|j|||S|}d }|s||_%|d&kr|d'||fS|d||fSd S)(zTest this query information against SPF text. Returns (result, mta-status-code, explanation) where result in ['fail', 'unknown', 'pass', 'none'] r8rrrzv=spf1r zInvalid SPF record incSsg|] }|r|qSrr)rrPrrrrWsz query.check0..Nr4rDredirectz"redirect= MUST appear at most oncez%s= MUST appear at most oncerazexp has empty domain-spec:zredirect has empty domain:defaultz"The default= modifier is obsolete.opr/TrHr2z+No valid SPF record for included domain: %srKrGr!rrErIzin-addrz syntax errorrJrFz!redirect domain has no SPF recordr3i&)&rqrrrrrlrs RE_MODIFIERlenr)rrXrrget_explanationr check_lookupsryrrrrrr\r|rdns_a cidrmatchr!dns_mxrsocketr@ domainmatchrrprrr)rRrrrsrrZmechsZ modifiersrPrmodrrarrrrlr_rcZtxtZredirect_recordrrrr=s                                  z query.check0cCsB|jd|_|jtdkr*tdtd|jtkr>|jddS)Nr r-zMore than %d DNS lookupszToo many DNS lookups)ru MAX_LOOKUPrXr)rRrrrrs   zquery.check_lookupsc Csv|r`y8|j|dd}t|dkr:t|jt|dddSWqrtk r\|jdkrXYqrXn|jdkrrtddS) zExpand an explanation.T) ignore_voidr rF)stripdotzEmpty domain-spec on exp=N)dns_txtrrrto_asciirXr)rRspecrrrrrs   zquery.get_explanationc Cs|jddkr:t}x&|jdD]}|j|rtd|qWd}d}xtj|D]}||||j7}||j|j}|dkr|d7}n|dkr|d7}n|d kr|d 7}n|d j } | d kr|j n| d kr|rtd|t || |} | rJ| |krtd|t | |ddt j| } | |d krBtj| d} || 7}|j}qPW|||d7}|r|jdr|dd}|jddkrt|dkr||jddd}|S)a%Do SPF RFC macro expansion. Examples: >>> q = query(s='strong-bad@email.example.com', ... h='mx.example.org', i='192.0.2.3') >>> q.p = 'mx.example.org' >>> q.r = 'example.net' >>> q.expand('%{d}') 'email.example.com' >>> q.expand('%{d4}') 'email.example.com' >>> q.expand('%{d3}') 'email.example.com' >>> q.expand('%{d2}') 'example.com' >>> q.expand('%{d1}') 'com' >>> q.expand('%{p}') 'mx.example.org' >>> q.expand('%{p2}') 'example.org' >>> q.expand('%{dr}') 'com.example.email' >>> q.expand('%{d2r}') 'example.email' >>> q.expand('%{l}') 'strong-bad' >>> q.expand('%{l-}') 'strong.bad' >>> q.expand('%{lr}') 'strong-bad' >>> q.expand('%{lr-}') 'bad.strong' >>> q.expand('%{l1r-}') 'strong' >>> q.expand('%{c}',stripdot=False) '192.0.2.3' >>> q.expand('%{r}',stripdot=False) 'example.net' >>> q.expand('%{ir}.%{v}._spf.%{d2}') '3.2.0.192.in-addr._spf.example.com' >>> q.expand('%{lr-}.lp._spf.%{d2}') 'bad.strong.lp._spf.example.com' >>> q.expand('%{lr-}.lp.%{ir}.%{v}._spf.%{d2}') 'bad.strong.lp.3.2.0.192.in-addr._spf.example.com' >>> q.expand('%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}') '3.2.0.192.in-addr.strong.lp._spf.example.com' >>> try: q.expand('%(ir).%{v}.%{l1r-}.lp._spf.%{d2}') ... except PermError as x: print(x) invalid-macro-char : %(ir) >>> q.expand('%{p2}.trusted-domains.example.net') 'example.org.trusted-domains.example.net' >>> q.expand('%{p2}.trusted-domains.example.net.') 'example.org.trusted-domains.example.net' >>> q = query(s='@email.example.com', ... h='mx.example.org', i='192.0.2.3') >>> q.p = 'mx.example.org' >>> q.expand('%{l}') 'postmaster' %rr/zinvalid-macro-char rBz%%z%_rz%-z%20rDrmZcrtz&c,r,t macros allowed in exp= text onlyzUnknown Macro Encounteredr r r?Nrr)findRE_INVALID_MACROrsearchrXRE_CHARfinditerstartendrrgetattr expand_oneJOINERSr urllibparseZquoterrrindex) rRr1rZregexlabelrrrYZmacroZletter expansionrrrrrsLW             z query.expandcCsx(|jdD]}| s"t|dkr dSq Wdd|j|D}t|dkrd|jr\td|jtdt|dkr|jd krt|d S|jdkrLyd d|j|d d dD}Wn8t k r}z|jdkrt |g}WYdd}~XnXt|dkrtdt|dkrL|jdkr@t|dkr@|d |d kr@t dt|d St|dkrft|d St rdd|j|dt d dD}t|dkrt|d SdS)zGet the SPF record recorded in DNS for a specific domain name. Returns None if not found, or if more than one record is found. r/?NcSsg|]}tj|r|qSr)RE_SPFr)rrkrrrrsz!query.dns_spf..r zcache=z'Two or more type TXT spf records found.rDrcSsg|]}tj|r|qSr)rr)rrkrrrrsr&T)rz'Two or more type SPF spf records found.zLv=spf1 records of both type TXT and SPF (type 99) present, but not identicalcSsg|]}tj|r|qSr)rr)rrkrrrrsz._spf.) rrrr\r{rorXrrrrDELEGATE)rRrrrbrrrrrs@      ,   z query.dns_spfr%c Cst|rpyF|j|||d}|rHdd|D}t|dtr:|Sdd|DSWn$tk rntd||fYnXgS)z,Get a list of TXT records for a domain name.)rcSs&g|]}|r|dddj|qS)rN)r)rrrrrrsz!query.dns_txt..rcSsg|]}|jdqS)zutf-8)encode)rr1rrrrsz.Non-ascii characters found in %s record for %s)r' isinstancebytes UnicodeErrorrX)rR domainnameZrrrZdns_listrrrrrsz query.dns_txtcszj|d}jrPt}t|tkr.tdtjdkrXt|dkrXtd|ntd}|jfdd|d |DS) zSGet a list of IP addresses for all MX exchanges for a domain name. r#z More than %d MX records returnedr rz$No MX records found for mx mechanismr-cs(g|] }j|djD]}|qqS)r )rr!)rrEr)rRrrrsz query.dns_mx..N)r'rMAX_MXrrXrsort)rRrZmxnamesmaxr)rRrrs      z query.dns_mxr!cCsZ|sgS|j||}|jdkr8t|dkr8td|||dkrVttkrVdd|DS|S)z5Get a list of IP addresses for a domainname. r rzNo %s records found forr"cSsg|] }t|qSr)r)rrLrrrrszquery.dns_a..)r'rrrrr)rRrr!rnrrrrs  z query.dns_ac sjrzt}jdkryJjj}t||krDd|}t|jnt|dkr\tdjWqtdjYqXntd}jfddjjd|DS) z=Figure out the validated PTR domain names for the connect IP.r z!More than %d PTR records returnedrz&No PTR records found for ptr mechanismr-cs&g|]}jj|jr|qSr)rrr!)rrm)rrRrrrsz(query.validated_ptrs..N)rMAX_PTRdns_ptrrYrrrr)rRrZptrnamesZwarningr)rrRrrs"    zquery.validated_ptrscCs|jdt||jfdS)z-Get a list of domain names for an IP address.z %s.%s.arpar$)r' reverse_dotsr)rRrYrrrr sz query.dns_ptrr#CNAMEr"r$r&cCs|s tdt|}|jdr*|dd}tdd|jddsDgS|j}|jj||fg}|rf|S|df}|jj|}|jo|j d }|r|d }n&t j } |j d krt d |j |jkr|j d kr|j } n|j} tj} xt|||j| D]\} } |r td | | | d j| df} | |krF| }|jj||fg}|rFP| ddksd|| df| kr|rvtd | | |jj| gj| qW|jj||fg}|j d kr|j tj| |_ | rL|rL|si}nt|tkrtdt|||<|jjd|kr(|jdkrLtd|n$|j|||d}|rL||j||f<| r| r|jd7_|jtkrtdt|S)aDNS query. If the result is in cache, return that. Otherwise pull the result from DNS, and cache ALL answers, so additional info is available for further queries later. CNAMEs are followed. If there is no data, [] is returned. pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF'] post: isinstance(__return__, types.ListType) Examples: >>> c = query(s='strong-bad@email.example.com', ... h='parallel.kitterman.org',i='192.0.2.123') >>> "".join( chr(x) for x in bytearray(c.dns('parallel.kitterman.org', 'TXT')[0][0]) ) 'v=spf1 include:long.kitterman.org include:cname.kitterman.org -all' z Invalid queryr/Nr cSs |odt|kodkSS)Nr@)r)ryrrr:szquery.dns..Trzcname.rz)DNS Error: exceeded max query lookup timezresult=z addcache=z Length of CNAME chain exceeds %dz CNAME loop)cnamesz Void lookup limit of %d exceededr)rMrrrrrrorr\ startswithr( SAFE2CACHEr]rr rj DNSLookuprr{ setdefaultr)r MAX_CNAMErXrstriprr'rvMAX_VOID_LOOKUPS)rRr rrrrZcnamekZcnamedebugZ safe2cacher Ztimethenkrrrrr'"sp            z query.dnscCs$yylxfdd|DD]T}|j|d}t|jtrB|j|jrhdSq||jkrZ|jj|q|jj|jqWWn|t k rxfdd|DD]T}|j|d}t|jtr|j|jrdSq||jkr|jj|q|jj|jqWYnXWn0t k r}zt t |WYdd}~XnXdS)aBMatch connect IP against a CIDR network of other IP addresses. Examples: >>> c = query(s='strong-bad@email.example.com', ... h='mx.example.org', i='192.0.2.3') >>> c.p = 'mx.example.org' >>> c.r = 'example.com' >>> c.cidrmatch(['192.0.2.3'],32) True >>> c.cidrmatch(['192.0.2.2'],32) False >>> c.cidrmatch(['192.0.2.2'],31) True >>> six = query(s='strong-bad@email.example.com', ... h='mx.example.org', i='2001:0db8:0:0:0:0:0:0001') >>> six.p = 'mx.example.org' >>> six.r = 'example.com' >>> six.cidrmatch(['2001:0DB8::'],127) True >>> six.cidrmatch(['2001:0DB8::'],128) False >>> six.cidrmatch(['2001:0DB8:0:0:0:0:0:0001'],128) True cSsg|]}tj|qSr)rZ ip_network)rrLrrrrsz#query.cidrmatch..)Z new_prefixTcSsg|]}tj|ddqS)F)r)rZ IPNetwork)rrLrrrrsNF) Zsupernetrrbool __contains__rwrr)rLrrrXr)rRZipaddrsnZnetwrkZnetworkrrrrrss,        zquery.cidrmatchcCsddl}djdd|jdD}|jj|}xr|jD]h}|jdkr6|j|_|j |_ |j dj dkr|j dj |_ |j dj |_|j dj d kr6|j dj |_q6WdS) acSet SPF values from RFC 5451 Authentication Results header. Useful when SPF has already been run on a trusted gateway machine. Expects the entire header as an input. Examples: >>> q = query('192.0.2.3','strong-bad@email.example.com','mx.example.org') >>> q.mechanism = 'unknown' >>> p = q.parse_header_ar('''Authentication-Results: bmsi.com; spf=neutral \n (abuse@kitterman.com: 192.0.2.3 is neither permitted nor denied by domain of email.example.com) \n smtp.mailfrom=email.example.com \n (sender=strong-bad@email.example.com; helo=mx.example.org; client-ip=192.0.2.3; receiver=abuse@kitterman.com; mechanism=?all)''') >>> q.get_header(q.result, header_type='authres', aid='bmsi.com') 'Authentication-Results: bmsi.com; spf=neutral (unknown: 192.0.2.3 is neither permitted nor denied by domain of email.example.com) smtp.mailfrom=email.example.com (sender=email.example.com; helo=mx.example.org; client-ip=192.0.2.3; receiver=unknown; mechanism=unknown)' >>> p = q.parse_header_ar('''Authentication-Results: bmsi.com; spf=None (mail.bmsi.com: test; client-ip=163.247.46.150) smtp.mailfrom=admin@squiebras.cl (helo=mail.squiebras.cl; receiver=mail.bmsi.com;\n mechanism=mx/24)''') >>> q.get_header(q.result, header_type='authres', aid='bmsi.com') 'Authentication-Results: bmsi.com; spf=none (unknown: 192.0.2.3 is neither permitted nor denied by domain of email.example.com) smtp.mailfrom=admin@squiebras.cl (sender=admin@squiebras.cl; helo=mx.example.org; client-ip=192.0.2.3; receiver=unknown; mechanism=unknown)' rNrcss|]}|jVqdS)N)strip)rr1rrr sz(query.parse_header_ar..rrrerd)authresrrAuthenticationResultsHeaderparseresultsmethod authserv_idrzrZ propertiesr valuerlr1rZ)rRvalrZarobjZresobjrrrparse_header_ars   zquery.parse_header_arcCsX|jdd}|dj|_d|_t|dkr0dS|d}|jdrx|jd}|dkrZ|jS|d||_||dd}t}|j dd |i}x|j dd D]\}}|d kr|j |q|d kr||_ q|d kr||_ q|dkr||_q|dkr||_q|dkr||_q|dkr ||_q|jdr|||dd<qWt|j |j \|_|_|S)aSet SPF values from Received-SPF header. Useful when SPF has already been run on a trusted gateway machine. Examples: >>> q = query('0.0.0.0','','') >>> p = q.parse_header_spf('''Pass (test) client-ip=70.98.79.77; ... envelope-from="evelyn@subjectsthum.com"; helo=mail.subjectsthum.com; ... receiver=mail.bmsi.com; mechanism=a; identity=mailfrom''') >>> q.get_header(q.result) 'Pass (test) client-ip=70.98.79.77; envelope-from="evelyn@subjectsthum.com"; helo=mail.subjectsthum.com; receiver=mail.bmsi.com; mechanism=a; identity=mailfrom' >>> o = q.parse_header_spf('''None (mail.bmsi.com: test) ... client-ip=163.247.46.150; envelope-from="admin@squiebras.cl"; ... helo=mail.squiebras.cl; receiver=mail.bmsi.com; mechanism=mx/24; ... x-bestguess=pass; x-helo-spf=neutral; identity=mailfrom''') >>> q.get_header(q.result,**o) 'None (mail.bmsi.com: test) client-ip=163.247.46.150; envelope-from="admin@squiebras.cl"; helo=mail.squiebras.cl; receiver=mail.bmsi.com; mechanism=mx/24; x-bestguess=pass; x-helo-spf=neutral; identity=mailfrom' >>> o['bestguess'] 'pass' Nr rrDr8()z Received-SPFz; )rz client-ipz envelope-fromrdr[problemridentityzx-)rrrrrrrcommentrZ add_headerZ get_paramsrxr1rZrnrPrfrgr0rh)rRrrposrOrmrrrrrparse_header_spfsD        zquery.parse_header_spfcCs"|jdr|j|S|j|SdS)a Set SPF values from Received-SPF or RFC 5451 Authentication Results header. Useful when SPF has already been run on a trusted gateway machine. Auto detects the header type and parses it. Use parse_header_spf or parse_header_ar for each type if required. Examples: >>> q = query('0.0.0.0','','') >>> p = q.parse_header('''Pass (test) client-ip=70.98.79.77; ... envelope-from="evelyn@subjectsthum.com"; helo=mail.subjectsthum.com; ... receiver=mail.bmsi.com; mechanism=a; identity=mailfrom''') >>> q.get_header(q.result) 'Pass (test) client-ip=70.98.79.77; envelope-from="evelyn@subjectsthum.com"; helo=mail.subjectsthum.com; receiver=mail.bmsi.com; mechanism=a; identity=mailfrom' >>> r = q.parse_header('''None (mail.bmsi.com: test) ... client-ip=163.247.46.150; envelope-from="admin@squiebras.cl"; ... helo=mail.squiebras.cl; receiver=mail.bmsi.com; mechanism=mx/24; ... x-bestguess=pass; x-helo-spf=neutral; identity=mailfrom''') >>> q.get_header(q.result,**r) 'None (mail.bmsi.com: test) client-ip=163.247.46.150; envelope-from="admin@squiebras.cl"; helo=mail.squiebras.cl; receiver=mail.bmsi.com; mechanism=mx/24; x-bestguess=pass; x-helo-spf=neutral; identity=mailfrom' >>> r['bestguess'] 'pass' >>> q = query('192.0.2.3','strong-bad@email.example.com','mx.example.org') >>> q.mechanism = 'unknown' >>> p = q.parse_header_ar('''Authentication-Results: bmsi.com; spf=neutral \n (abuse@kitterman.com: 192.0.2.3 is neither permitted nor denied by domain of email.example.com) \n smtp.mailfrom=email.example.com \n (sender=strong-bad@email.example.com; helo=mx.example.org; client-ip=192.0.2.3; receiver=abuse@kitterman.com; mechanism=?all)''') >>> q.get_header(q.result, header_type='authres', aid='bmsi.com') 'Authentication-Results: bmsi.com; spf=neutral (unknown: 192.0.2.3 is neither permitted nor denied by domain of email.example.com) smtp.mailfrom=email.example.com (sender=email.example.com; helo=mx.example.org; client-ip=192.0.2.3; receiver=unknown; mechanism=unknown)' >>> p = q.parse_header_ar('''Authentication-Results: bmsi.com; spf=None (mail.bmsi.com: test; client-ip=163.247.46.150) smtp.mailfrom=admin@squiebras.cl (helo=mail.squiebras.cl; receiver=mail.bmsi.com; mechanism=mx/24)''') >>> q.get_header(q.result, header_type='authres', aid='bmsi.com') 'Authentication-Results: bmsi.com; spf=none (unknown: 192.0.2.3 is neither permitted nor denied by domain of email.example.com) smtp.mailfrom=admin@squiebras.cl (sender=admin@squiebras.cl; helo=mx.example.org; client-ip=192.0.2.3; receiver=unknown; mechanism=unknown)' zAuthentication-Results:N)rrr )rRrrrr parse_headers  zquery.parse_headerrc Ks|dkr|stdddl}|s&|j}|j}t|j}ddddd d d d } |j} | d kr^d} n t|j} | |} |dkr|jrtdj |j} nd} t|j }t |dr|j }nd||j |f}d| |fg}|dkrpx4d%D],}t|}|r|jd|jdd|fqWxBtt|jD].\}}|r"|jd|jddt|fq"W|jdd| fdj |S|dkr| rt|j||j| ||jdj|j|j|j|j|d gd!St|j||j| ||jd"j|j|j|j|d#gd!Sntd$j|dS)&aD Generate Received-SPF or Authentication Results header based on the last lookup. >>> q = query(s='strong-bad@email.example.com', h='mx.example.org', ... i='192.0.2.3') >>> q.r='abuse@kitterman.com' >>> q.check(spf='v=spf1 ?all') ('neutral', 250, 'access neither permitted nor denied') >>> q.get_header('neutral') 'Neutral (abuse@kitterman.com: 192.0.2.3 is neither permitted nor denied by domain of email.example.com) client-ip=192.0.2.3; envelope-from="strong-bad@email.example.com"; helo=mx.example.org; receiver=abuse@kitterman.com; mechanism=?all; identity=mailfrom' >>> q.check(spf='v=spf1 redirect=controlledmail.com exp=_exp.controlledmail.com') ('fail', 550, 'SPF fail - not authorized') >>> q.get_header('fail') 'Fail (abuse@kitterman.com: domain of email.example.com does not designate 192.0.2.3 as permitted sender) client-ip=192.0.2.3; envelope-from="strong-bad@email.example.com"; helo=mx.example.org; receiver=abuse@kitterman.com; mechanism=-all; identity=mailfrom' >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ?all moo') ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo') >>> q.get_header('permerror') 'PermError (abuse@kitterman.com: permanent error in processing domain of email.example.com: Unknown mechanism found) client-ip=192.0.2.3; envelope-from="strong-bad@email.example.com"; helo=mx.example.org; receiver=abuse@kitterman.com; problem=moo; identity=mailfrom' >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ~all') ('pass', 250, 'sender SPF authorized') >>> q.get_header('pass') 'Pass (abuse@kitterman.com: domain of email.example.com designates 192.0.2.3 as permitted sender) client-ip=192.0.2.3; envelope-from="strong-bad@email.example.com"; helo=mx.example.org; receiver=abuse@kitterman.com; mechanism="ip4:192.0.0.0/8"; identity=mailfrom' >>> q.check(spf='v=spf1 ?all') ('neutral', 250, 'access neither permitted nor denied') >>> q.get_header('neutral', header_type = 'authres', aid='bmsi.com') 'Authentication-Results: bmsi.com; spf=neutral (abuse@kitterman.com: 192.0.2.3 is neither permitted nor denied by domain of email.example.com) smtp.mailfrom=email.example.com (sender=strong-bad@email.example.com; helo=mx.example.org; client-ip=192.0.2.3; receiver=abuse@kitterman.com; mechanism=?all)' >>> p = query(s='strong-bad@email.example.com', h='mx.example.org', ... i='192.0.2.3') >>> p.r='abuse@kitterman.com' >>> p.check(spf='v=spf1 redirect=controlledmail.com exp=_exp.controlledmail.com') ('fail', 550, 'SPF fail - not authorized') >>> p.ident = 'helo' >>> p.get_header('fail', header_type = 'authres', aid='bmsi.com') 'Authentication-Results: bmsi.com; spf=fail (abuse@kitterman.com: domain of email.example.com does not designate 192.0.2.3 as permitted sender) smtp.helo=mx.example.org (sender=strong-bad@email.example.com; client-ip=192.0.2.3; receiver=abuse@kitterman.com; mechanism=-all)' >>> q.check(spf='v=spf1 ?all') ('neutral', 250, 'access neither permitted nor denied') >>> try: q.get_header('neutral', header_type = 'dkim') ... except SyntaxError as x: print(x) Unknown results header type: dkim rzKauthserv-id missing for Authentication Results header type, see RFC5451 2.3rNZPassZNeutralZFailZSoftFailNonerrX)r2r4r3r5r8r7r6rdr6rr z%s: %sz%s (%s)r client_ip envelope_fromr[r rz%s=%s;r`r=zx-%s=%s;z%s=%sr z@sender={0}; helo={1}; client-ip={2}; receiver={3}; mechanism={4})rresult_commentZ smtp_mailfromZsmtp_mailfrom_comment)rrz6sender={0}; client-ip={1}; receiver={2}; mechanism={3})rrZ smtp_heloZsmtp_helo_commentz Unknown results header type: {0})rrrdr[r r) SyntaxErrorrrnr quote_valuerZrfr1rPrrhasattrr get_header_commentlocalsr)rsortedr}itemsrrZSPFAuthenticationResultrlformat)rRr_r[Z header_typeZaidZkvrrrdZresmapr rtagr rr rrrrr get_headers`1       $  zquery.get_headercCs|j}|dkrd||jfS|dkr2d||jfS|dkrHd|j|fS|dkr^d|j|fS|dkrtd ||jfS|d krd |S|d krd ||jfStd|dS)z)Return comment for Received-SPF header. r2z.domain of %s designates %s as permitted senderr5zDtransitioning domain of %s does not designate %s as permitted senderr4z2%s is neither permitted nor denied by domain of %sr8r6z.permanent error in processing domain of %s: %sr7z1temporary error in processing during lookup of %sr3z6domain of %s does not designate %s as permitted senderz'invalid SPF result for header comment: N)rhrrr)rRr_Zsenderrrrrs,      zquery.get_header_comment)N)T)r%F)r!)r#r!)r#r#)rr!)r!r!)r"r")r$r$)r%r%)r&r&)NF)NrN)$rTrUrVrWMAX_PER_LOOKUP_TIMErNr|rxrrr DEFAULT_SPFrr^rrrrrrrrrrrrrrrr'rrr rrrrrrrr(^sT0(  p ", 9    Q7!/% ir(cCsL|s d|fS|jdd}|ddkr,d|d<t|dkr@t|Sd|fSdS)atGiven a sender email s and a HELO domain h, create a valid tuple (l, d) local-part and domain-part. Examples: >>> split_email('', 'wayforward.net') ('postmaster', 'wayforward.net') >>> split_email('foo.com', 'wayforward.net') ('postmaster', 'foo.com') >>> split_email('terry@wayforward.net', 'optsw.com') ('terry', 'wayforward.net') Z postmaster@r rrBrDN)rrtuple)r1rZpartsrrrrgs   rgcCs:|dkstj|r|Sd|jddjddjdddS)aQuote the value for a key-value pair in Received-SPF header field if needed. No quoting needed for a dot-atom value. Examples: >>> 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) N"\z\\z\"z\x00) RE_DOT_ATOMrr)r1rrrrsrcCstj|}t|dkr.|dt|d}}nd}tj|}t|dkr`|dt|d}}nd}|jdd}t|dkr|j}|dkrd}||||fS|dj|d||fS)aBreaks 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) r rr NrrDrG) RE_DUAL_CIDRrrriRE_CIDRr)rrlrZcidr6Zcidrrrrrs"       rcCs|jd}|jdj|S)zReverse dotted IP addresses or domain names. Example: >>> reverse_dots('192.168.0.145') '145.0.168.192' >>> reverse_dots('email.example.com') 'com.example.email' r/)rreverser)r rrrrrs rcCs<|j}x.|D]&}|j}||ks0|jd|rdSqWdS)agrep 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 r/TF)rr)ZptrsZ domainsuffixrFrrrrs  rcCsh|s|Stj|dd\}}}|s(d}t|||}|r@|j|r^|t| ddd}dj|S)Nr r-r/rDrB)RE_ARGSrr(rir)rrjoinerlnr( delimitersrrrr6s rcCs`gd}}xF|D]>}||krF|j|d}|r:|j|qN|j|q||7}qW|j||S)aSplit 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'] rB)r))rr,r*relementrrrrrAs       rcCsx|s|S|jdd}|rp|jxJ|D]>}tj|ds*|j|}|g|||<|jdj|}Pq*W|Sd|S)aReturns 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' r Nrrzv=spf1 )rr(rrrr)Zspftxtr9rrPwhererrrr^s   rc Cs2y |jdStk r,tdt|YnXdS)z*Raise PermError if arg is not 7-bit ascii.asciizNon-ascii characters foundN)rrrXrepr)r1rrrrs rc Cs2y |jdStk r,tdt|YnXdS)z*Raise PermError if arg is not 7-bit ascii.r/zNon-ascii characters foundN)decoderrXr0)r1rrrrs cCsddl}ddl}|j|S)Nr)doctestrZtestmod)r2rrrr_testsr3__main__r zhvs:helpr\r-v --verbose-s--strict-h--helpz 127.0.0.1Z localhostrA)rYr1rZr[zTemporary DNS error: z PermError: r )rYr1rZr[r\rzresult:zguessed:zlax:)rYr1rZr[rr\)Tr)Tr )rrErFrGrHrIrJrK)NNF)N)N)r6r7)r8r9)r:r;){Z __future__r __author__Z __email__ __version__ZMODULEZUSAGEresysrstructrjZ urllib.parserrZurllib functoolsrZ email.messager ImportErrorZ email.Messagerrrrrwr{rr+r'Z dns.resolverZ dns.exceptionrZ rdatatyper&Z_by_textrrZTypeZtypemapZLibZ RRunpackerZ getTXTdataZ getSPFdataZDiscoverNameServerscompile IGNORECASErrZPAT_CHARrrr)r&r'rZPAT_IP4rrr%rrrrqrrZTRUSTED_FORWARDERSrrrrrrrrrrMrrrXrbr^objectr(rgrrrrrrr version_inforr3rTZgetoptargvZoptsZ GetoptErrorerrexitr\rrhrrirZ gethostnameqrrrYr1rZrnrrrrQrrLrrrrsh +  !                O!4  3