1
2 """SPF (Sender Policy Framework) implementation.
3
4 Copyright (c) 2003, Terence Way
5 Portions Copyright (c) 2004,2005,2006 Stuart Gathman <stuart@bmsi.com>
6 Portions Copyright (c) 2005,2006 Scott Kitterman <scott@kitterman.com>
7 This module is free software, and you may redistribute it and/or modify
8 it under the same terms as Python itself, so long as this copyright message
9 and disclaimer are retained in their original form.
10
11 IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
12 SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
13 THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
14 DAMAGE.
15
16 THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
17 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
18 PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
19 AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
20 SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
21
22 For more information about SPF, a tool against email forgery, see
23 http://www.openspf.org/
24
25 For news, bugfixes, etc. visit the home page for this implementation at
26 http://www.wayforward.net/spf/
27 http://sourceforge.net/projects/pymilter/
28 """
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256 __author__ = "Terence Way"
257 __email__ = "terry@wayforward.net"
258 __version__ = "2.1: January 22, 2007"
259 MODULE = 'spf'
260
261 USAGE = """To check an incoming mail request:
262 % python spf.py {ip} {sender} {helo}
263 % python spf.py 69.55.226.139 tway@optsw.com mx1.wayforward.net
264
265 To test an SPF record:
266 % python spf.py "v=spf1..." {ip} {sender} {helo}
267 % python spf.py "v=spf1 +mx +ip4:10.0.0.1 -all" 10.0.0.1 tway@foo.com a
268
269 To fetch an SPF record:
270 % python spf.py {domain}
271 % python spf.py wayforward.net
272
273 To test this script (and to output this usage message):
274 % python spf.py
275 """
276
277 import re
278 import socket
279 import struct
280 import time
281 import urllib
282
289
290 RE_SPF = re.compile(r'^v=spf1$|^v=spf1 ',re.IGNORECASE)
291
292
293 RE_MODIFIER = re.compile(r'^([a-z][a-z0-9_\-\.]*)=', re.IGNORECASE)
294
295
296 PAT_CHAR = r'%(%|_|-|(\{[^\}]*\}))'
297 RE_CHAR = re.compile(PAT_CHAR)
298
299
300 RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)')
301
302 RE_DUAL_CIDR = re.compile(r'//(0|[1-9]\d*)$')
303 RE_CIDR = re.compile(r'/(0|[1-9]\d*)$')
304
305 PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
306 RE_IP4 = re.compile(PAT_IP4+'$')
307
308 RE_TOPLAB = re.compile(
309 r'\.(?:[0-9a-z]*[a-z][0-9a-z]*|[0-9a-z]+-[0-9a-z-]*[0-9a-z])\.?$|%s'
310 % PAT_CHAR, re.IGNORECASE)
311
312 RE_DOT_ATOM = re.compile(r'%(atext)s+([.]%(atext)s+)*$' % {
313 'atext': r"[0-9a-z!#$%&'*+/=?^_`{}|~-]" }, re.IGNORECASE)
314
315 RE_IP6 = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
316 '|::(?:%(hex4)s:){5}%(ls32)s$'
317 '|(?:%(hex4)s)?::(?:%(hex4)s:){4}%(ls32)s$'
318 '|(?:(?:%(hex4)s:){0,1}%(hex4)s)?::(?:%(hex4)s:){3}%(ls32)s$'
319 '|(?:(?:%(hex4)s:){0,2}%(hex4)s)?::(?:%(hex4)s:){2}%(ls32)s$'
320 '|(?:(?:%(hex4)s:){0,3}%(hex4)s)?::%(hex4)s:%(ls32)s$'
321 '|(?:(?:%(hex4)s:){0,4}%(hex4)s)?::%(ls32)s$'
322 '|(?:(?:%(hex4)s:){0,5}%(hex4)s)?::%(hex4)s$'
323 '|(?:(?:%(hex4)s:){0,6}%(hex4)s)?::$'
324 % {
325 'ls32': r'(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|%s)'%PAT_IP4,
326 'hex4': r'[0-9a-f]{1,4}'
327 }, re.IGNORECASE)
328
329
330
331
332 JOINERS = {'l': '.', 's': '.'}
333
334 RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail',
335 'pass': 'pass', 'fail': 'fail', 'permerror': 'permerror',
336 'error': 'temperror', 'neutral': 'neutral', 'softfail': 'softfail',
337 'none': 'none', 'local': 'local', 'trusted': 'trusted',
338 'ambiguous': 'ambiguous', 'unknown': 'permerror' }
339
340 EXPLANATIONS = {'pass': 'sender SPF authorized',
341 'fail': 'SPF fail - not authorized',
342 'permerror': 'permanent error in processing',
343 'temperror': 'temporary DNS error in processing',
344 'softfail': 'domain owner discourages use of this host',
345 'neutral': 'access neither permitted nor denied',
346 'none': '',
347
348 'local': 'No SPF result due to local policy',
349 'trusted': 'No SPF check - trusted-forwarder.org',
350
351 'ambiguous': 'No error, but results may vary'
352 }
353
354
355 try:
356 bool, True, False = bool, True, False
357 except NameError:
358 False, True = 0, 1
359 - def bool(x): return not not x
360
361
362 DELEGATE = None
363
364
365 DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
366
367
368
369 TRUSTED_FORWARDERS = 'v=spf1 ?include:spf.trusted-forwarder.org -all'
370
371
372 MAX_LOOKUP = 10
373 MAX_MX = 10
374 MAX_PTR = 10
375 MAX_CNAME = 10
376 MAX_RECURSION = 20
377
378 ALL_MECHANISMS = ('a', 'mx', 'ptr', 'exists', 'include', 'ip4', 'ip6', 'all')
379 COMMON_MISTAKES = {
380 'prt': 'ptr', 'ip': 'ip4', 'ipv4': 'ip4', 'ipv6': 'ip6', 'all.': 'all'
381 }
382
383
384
386 "SPF Warning - ambiguous results"
387 - def __init__(self, msg, mech=None, ext=None):
388 Exception.__init__(self, msg, mech)
389 self.msg = msg
390 self.mech = mech
391 self.ext = ext
393 if self.mech:
394 return '%s: %s' %(self.msg, self.mech)
395 return self.msg
396
398 "Temporary SPF error"
399 - def __init__(self, msg, mech=None, ext=None):
400 Exception.__init__(self, msg, mech)
401 self.msg = msg
402 self.mech = mech
403 self.ext = ext
405 if self.mech:
406 return '%s: %s '%(self.msg, self.mech)
407 return self.msg
408
410 "Permanent SPF error"
411 - def __init__(self, msg, mech=None, ext=None):
412 Exception.__init__(self, msg, mech)
413 self.msg = msg
414 self.mech = mech
415 self.ext = ext
417 if self.mech:
418 return '%s: %s'%(self.msg, self.mech)
419 return self.msg
420
421 -def check2(i, s, h, local=None, receiver=None):
422 """Test an incoming MAIL FROM:<s>, from a client with ip address i.
423 h is the HELO/EHLO domain name. This is the RFC4408 compliant pySPF2.0
424 interface. The interface returns an SPF result and explanation only.
425 SMTP response codes are not returned since RFC 4408 does not specify
426 receiver policy. Applications updated for RFC 4408 should use this
427 interface.
428
429 Returns (result, explanation) where result in
430 ['pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', 'neutral' ].
431
432 Example:
433 #>>> check2(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')
434
435 """
436 res,_,exp = query(i=i, s=s, h=h, local=local, receiver=receiver).check()
437 return res,exp
438
439 -def check(i, s, h, local=None, receiver=None):
440 """Test an incoming MAIL FROM:<s>, from a client with ip address i.
441 h is the HELO/EHLO domain name. This is the pre-RFC SPF Classic interface.
442 Applications written for pySPF 1.6/1.7 can use this interface to allow
443 pySPF2 to be a drop in replacement for older versions. With the exception
444 of result codes, performance in RFC 4408 compliant.
445
446 Returns (result, code, explanation) where result in
447 ['pass', 'unknown', 'fail', 'error', 'softfail', 'none', 'neutral' ].
448
449 Example:
450 #>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')
451
452 """
453 res,code,exp = query(i=i, s=s, h=h, local=local, receiver=receiver).check()
454 if res == 'permerror':
455 res = 'unknown'
456 elif res == 'tempfail':
457 res =='error'
458 return res, code, exp
459
461 """A query object keeps the relevant information about a single SPF query:
462
463 - i: ip address of SMTP client in dotted notation
464 - s: sender declared in MAIL FROM:<>
465 - l: local part of sender s
466 - d: current domain, initially domain part of sender s
467 - h: EHLO/HELO domain
468 - v: 'in-addr' for IPv4 clients and 'ip6' for IPv6 clients
469 - t: current timestamp
470 - p: SMTP client domain name
471 - o: domain part of sender s
472 - r: receiver
473 - c: pretty ip address (different from i for IPv6)
474
475 This is also, by design, the same variables used in SPF macro
476 expansion.
477
478 Also keeps cache: DNS cache.
479 """
480 - def __init__(self, i, s, h, local=None, receiver=None, strict=True):
481 self.s, self.h = s, h
482 if not s and h:
483 self.s = 'postmaster@' + h
484 self.ident = 'helo'
485 else:
486 self.ident = 'mailfrom'
487 self.l, self.o = split_email(s, h)
488 self.t = str(int(time.time()))
489 self.d = self.o
490 self.p = None
491 if receiver:
492 self.r = receiver
493 else:
494 self.r = 'unknown'
495
496
497
498 self.cache = {}
499 self.defexps = dict(EXPLANATIONS)
500 self.exps = dict(EXPLANATIONS)
501 self.libspf_local = local
502 self.lookups = 0
503
504 self.strict = strict
505 if i:
506 self.set_ip(i)
507
509 "Set connect ip, and ip6 or ip4 mode."
510 if RE_IP4.match(i):
511 self.ip = addr2bin(i)
512 ip6 = False
513 else:
514 self.ip = bin2long6(inet_pton(i))
515 if (self.ip >> 32) == 0xFFFF:
516 self.ip = self.ip & 0xFFFFFFFFL
517 ip6 = False
518 else:
519 ip6 = True
520
521 if ip6:
522 self.c = inet_ntop(
523 struct.pack("!QQ", self.ip>>64, self.ip&0xFFFFFFFFFFFFFFFFL))
524 self.i = '.'.join(list('%032X'%self.ip))
525 self.A = 'AAAA'
526 self.v = 'ip6'
527 self.cidrmax = 128
528 else:
529 self.c = socket.inet_ntoa(struct.pack("!L", self.ip))
530 self.i = self.c
531 self.A = 'A'
532 self.v = 'in-addr'
533 self.cidrmax = 32
534
536 exps = self.exps
537 defexps = self.defexps
538 for i in 'softfail', 'fail', 'permerror':
539 exps[i] = exp
540 defexps[i] = exp
541
543 exps = self.exps
544 for i in 'softfail', 'fail', 'permerror':
545 exps[i] = exp
546
547
549 if not self.p:
550 p = self.validated_ptrs()
551 if not p:
552 self.p = "unknown"
553 elif self.d in p:
554 self.p = self.d
555 else:
556 sfx = '.' + self.d
557 for d in p:
558 if d.endswith(sfx):
559 self.p = d
560 break
561 else:
562 self.p = p[0]
563 return self.p
564
566 """Return a best guess based on a default SPF record.
567 >>> q = query('1.2.3.4','','SUPERVISION1',receiver='example.com')
568 >>> q.best_guess()[0]
569 'none'
570 """
571 if RE_TOPLAB.split(self.d)[-1]:
572 return ('none', 250, '')
573 return self.check(spf)
574
575
576 - def check(self, spf=None):
577 """
578 Returns (result, mta-status-code, explanation) where result
579 in ['fail', 'softfail', 'neutral' 'permerror', 'pass', 'temperror', 'none']
580
581 Examples:
582 >>> q = query(s='strong-bad@email.example.com',
583 ... h='mx.example.org', i='192.0.2.3')
584 >>> q.check(spf='v=spf1 ?all')
585 ('neutral', 250, 'access neither permitted nor denied')
586
587 >>> q.check(spf='v=spf1 redirect=controlledmail.com exp=_exp.controlledmail.com')
588 ('fail', 550, 'SPF fail - not authorized')
589
590 >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ?all moo')
591 ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
592
593 >>> q.check(spf='v=spf1 =a ?all moo')
594 ('permerror', 550, 'SPF Permanent Error: Unknown qualifier, RFC 4408 para 4.6.1, found in: =a')
595
596 >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ~all')
597 ('pass', 250, 'sender SPF authorized')
598
599 >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo=')
600 ('pass', 250, 'sender SPF authorized')
601
602 >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all match.sub-domains_9=yes')
603 ('pass', 250, 'sender SPF authorized')
604
605 >>> q.strict = False
606 >>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo')
607 ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
608 >>> q.perm_error.ext
609 ('pass', 250, 'sender SPF authorized')
610
611 >>> q.strict = True
612 >>> q.check(spf='v=spf1 ip4:192.1.0.0/16 moo -all')
613 ('permerror', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
614
615 >>> q.check(spf='v=spf1 ip4:192.1.0.0/16 ~all')
616 ('softfail', 250, 'domain owner discourages use of this host')
617
618 >>> q.check(spf='v=spf1 -ip4:192.1.0.0/6 ~all')
619 ('fail', 550, 'SPF fail - not authorized')
620
621 # Assumes DNS available
622 >>> q.check()
623 ('none', 250, '')
624
625 >>> q.check(spf='v=spf1 ip4:1.2.3.4 -a:example.net -all')
626 ('fail', 550, 'SPF fail - not authorized')
627 >>> q.libspf_local='ip4:192.0.2.3 a:example.org'
628 >>> q.check(spf='v=spf1 ip4:1.2.3.4 -a:example.net -all')
629 ('pass', 250, 'sender SPF authorized')
630
631 >>> q.check(spf='v=spf1 ip4:1.2.3.4 -all exp=_exp.controlledmail.com')
632 ('fail', 550, 'Controlledmail.com does not send mail from itself.')
633
634 >>> q.check(spf='v=spf1 ip4:1.2.3.4 ?all exp=_exp.controlledmail.com')
635 ('neutral', 250, 'access neither permitted nor denied')
636 """
637 self.mech = []
638
639
640
641
642 self.perm_error = None
643 self.mechanism = None
644 self.options = {}
645
646 try:
647 self.lookups = 0
648 if not spf:
649 spf = self.dns_spf(self.d)
650 if self.libspf_local and spf:
651 spf = insert_libspf_local_policy(
652 spf, self.libspf_local)
653 rc = self.check1(spf, self.d, 0)
654 if self.perm_error:
655
656 self.perm_error.ext = rc
657 raise self.perm_error
658 return rc
659
660 except TempError, x:
661 self.prob = x.msg
662 if x.mech:
663 self.mech.append(x.mech)
664 return ('temperror', 451, 'SPF Temporary Error: ' + str(x))
665 except PermError, x:
666 if not self.perm_error:
667 self.perm_error = x
668 self.prob = x.msg
669 if x.mech:
670 self.mech.append(x.mech)
671
672
673 return ('permerror', 550, 'SPF Permanent Error: ' + str(x))
674
675 - def check1(self, spf, domain, recursion):
676
677
678 if recursion > MAX_RECURSION:
679
680
681
682
683
684 if self.strict:
685 raise AssertionError('Too many levels of recursion')
686
687
688 raise PermError('Too many levels of recursion')
689 try:
690 try:
691 tmp, self.d = self.d, domain
692 return self.check0(spf, recursion)
693 finally:
694 self.d = tmp
695 except AmbiguityWarning,x:
696 self.prob = x.msg
697 if x.mech:
698 self.mech.append(x.mech)
699 return ('ambiguous', 000, 'SPF Ambiguity Warning: %s' % x)
700
702 if self.strict:
703 raise PermError(*msg)
704
705 if not self.perm_error:
706 try:
707 raise PermError(*msg)
708 except PermError, x:
709
710 self.perm_error = x
711 return self.perm_error
712
713 - def expand_domain(self,arg):
714 "validate and expand domain-spec"
715
716 if RE_TOPLAB.split(arg)[-1]:
717 raise PermError('Invalid domain found (use FQDN)', arg)
718 return self.expand(arg)
719
721 """Parse and validate a mechanism.
722 Returns mech,m,arg,cidrlength,result
723
724 Examples:
725 >>> q = query(s='strong-bad@email.example.com.',
726 ... h='mx.example.org', i='192.0.2.3')
727 >>> q.validate_mechanism('A')
728 ('A', 'a', 'email.example.com', 32, 'pass')
729
730 >>> q = query(s='strong-bad@email.example.com',
731 ... h='mx.example.org', i='192.0.2.3')
732 >>> q.validate_mechanism('A')
733 ('A', 'a', 'email.example.com', 32, 'pass')
734
735 >>> q.validate_mechanism('?mx:%{d}/27')
736 ('?mx:%{d}/27', 'mx', 'email.example.com', 27, 'neutral')
737
738 >>> try: q.validate_mechanism('ip4:1.2.3.4/247')
739 ... except PermError,x: print x
740 Invalid IP4 CIDR length: ip4:1.2.3.4/247
741
742 >>> try: q.validate_mechanism('ip4:1.2.3.4/33')
743 ... except PermError,x: print x
744 Invalid IP4 CIDR length: ip4:1.2.3.4/33
745
746 >>> try: q.validate_mechanism('a:example.com:8080')
747 ... except PermError,x: print x
748 Invalid domain found (use FQDN): example.com:8080
749
750 >>> try: q.validate_mechanism('ip4:1.2.3.444/24')
751 ... except PermError,x: print x
752 Invalid IP4 address: ip4:1.2.3.444/24
753
754 >>> try: q.validate_mechanism('ip4:1.2.03.4/24')
755 ... except PermError,x: print x
756 Invalid IP4 address: ip4:1.2.03.4/24
757
758 >>> try: q.validate_mechanism('-all:3030')
759 ... except PermError,x: print x
760 Invalid all mechanism format - only qualifier allowed with all: -all:3030
761
762 >>> q.validate_mechanism('-mx:%%%_/.Clara.de/27')
763 ('-mx:%%%_/.Clara.de/27', 'mx', '% /.Clara.de', 27, 'fail')
764
765 >>> q.validate_mechanism('~exists:%{i}.%{s1}.100/86400.rate.%{d}')
766 ('~exists:%{i}.%{s1}.100/86400.rate.%{d}', 'exists', '192.0.2.3.com.100/86400.rate.email.example.com', 32, 'softfail')
767
768 >>> q.validate_mechanism('a:mail.example.com.')
769 ('a:mail.example.com.', 'a', 'mail.example.com', 32, 'pass')
770
771 >>> try: q.validate_mechanism('a:mail.example.com,')
772 ... except PermError,x: print x
773 Do not separate mechnisms with commas: a:mail.example.com,
774 """
775 if mech.endswith( "," ):
776 self.note_error('Do not separate mechnisms with commas', mech)
777 mech = mech[:-1]
778
779 m, arg, cidrlength, cidr6length = parse_mechanism(mech, self.d)
780
781 if m:
782 result = RESULTS.get(m[0])
783 if result:
784
785 m = m[1:]
786 else:
787
788 result = 'pass'
789 if m in COMMON_MISTAKES:
790 self.note_error('Unknown mechanism found', mech)
791 m = COMMON_MISTAKES[m]
792
793 if m == 'a' and RE_IP4.match(arg):
794 x = self.note_error(
795 'Use the ip4 mechanism for ip4 addresses', mech)
796 m = 'ip4'
797
798
799
800 if m in ('a', 'mx'):
801 if cidrlength is None:
802 cidrlength = 32;
803 elif cidrlength > 32:
804 raise PermError('Invalid IP4 CIDR length', mech)
805 if cidr6length is None:
806 cidr6length = 128
807 elif cidr6length > 128:
808 raise PermError('Invalid IP6 CIDR length', mech)
809 if self.v == 'ip6':
810 cidrlength = cidr6length
811 elif m == 'ip4':
812 if cidr6length is not None:
813 raise PermError('Dual CIDR not allowed', mech)
814 if cidrlength is None:
815 cidrlength = 32;
816 elif cidrlength > 32:
817 raise PermError('Invalid IP4 CIDR length', mech)
818 if not RE_IP4.match(arg):
819 raise PermError('Invalid IP4 address', mech)
820 elif m == 'ip6':
821 if cidr6length is not None:
822 raise PermError('Dual CIDR not allowed', mech)
823 if cidrlength is None:
824 cidrlength = 128
825 elif cidrlength > 128:
826 raise PermError('Invalid IP6 CIDR length', mech)
827 if not RE_IP6.match(arg):
828 raise PermError('Invalid IP6 address', mech)
829 else:
830 if cidrlength is not None or cidr6length is not None:
831 raise PermError('CIDR not allowed', mech)
832 cidrlength = self.cidrmax
833
834 if m in ('a', 'mx', 'ptr', 'exists', 'include'):
835 if m == 'exists' and not arg:
836 raise PermError('implicit exists not allowed', mech)
837 arg = self.expand_domain(arg)
838 if not arg:
839 raise PermError('empty domain:',mech)
840 if m == 'include':
841 if arg == self.d:
842 if mech != 'include':
843 raise PermError('include has trivial recursion', mech)
844 raise PermError('include mechanism missing domain', mech)
845 return mech, m, arg, cidrlength, result
846
847
848 if m == 'all' and mech.count(':'):
849
850 self.note_error(
851 'Invalid all mechanism format - only qualifier allowed with all'
852 , mech)
853 if m in ALL_MECHANISMS:
854 return mech, m, arg, cidrlength, result
855 if m[1:] in ALL_MECHANISMS:
856 x = self.note_error(
857 'Unknown qualifier, RFC 4408 para 4.6.1, found in', mech)
858 else:
859 x = self.note_error('Unknown mechanism found', mech)
860 return mech, m, arg, cidrlength, x
861
862 - def check0(self, spf, recursion):
863 """Test this query information against SPF text.
864
865 Returns (result, mta-status-code, explanation) where
866 result in ['fail', 'unknown', 'pass', 'none']
867 """
868
869 if not spf:
870 return ('none', 250, EXPLANATIONS['none'])
871
872
873 spf = spf.split()
874
875
876
877
878
879 if spf[0].lower() != 'v=spf1':
880 assert strict > 1
881 raise AmbiguityWarning('Invalid SPF record in', self.d)
882 spf = spf[1:]
883
884
885 exps = self.exps
886 redirect = None
887
888
889
890
891 default = 'neutral'
892 mechs = []
893
894 modifiers = []
895
896
897 for mech in spf:
898 m = RE_MODIFIER.split(mech)[1:]
899 if len(m) != 2:
900 mechs.append(self.validate_mechanism(mech))
901 continue
902
903 mod,arg = m
904 if mod in modifiers:
905 if mod == 'redirect':
906 raise PermError('redirect= MUST appear at most once',mech)
907 self.note_error('%s= MUST appear at most once'%mod,mech)
908
909 modifiers.append(mod)
910 if mod == 'exp':
911
912 arg = self.expand_domain(arg)
913 exp = self.get_explanation(arg)
914 if exp and not recursion:
915
916 self.set_explanation(exp)
917 elif mod == 'redirect':
918 self.check_lookups()
919 redirect = self.expand_domain(arg)
920 if not redirect:
921 raise PermError('redirect has empty domain:',arg)
922 elif mod == 'default':
923 arg = self.expand(arg)
924
925 default = RESULTS.get(arg, default)
926 elif mod == 'op':
927 if not recursion:
928 for v in arg.split('.'):
929 if v: self.options[v] = True
930 else:
931
932 self.expand(arg)
933
934
935
936 for mech, m, arg, cidrlength, result in mechs:
937
938 if m == 'include':
939 self.check_lookups()
940 res, code, txt = self.check1(self.dns_spf(arg),
941 arg, recursion + 1)
942 if res == 'pass':
943 break
944 if res == 'none':
945 self.note_error(
946 'No valid SPF record for included domain: %s' %arg,
947 mech)
948 res = 'neutral'
949 continue
950 elif m == 'all':
951 break
952
953 elif m == 'exists':
954 self.check_lookups()
955 try:
956 if len(self.dns_a(arg,'A')) > 0:
957 break
958 except AmbiguityWarning:
959
960
961 pass
962
963 elif m == 'a':
964 self.check_lookups()
965 if self.cidrmatch(self.dns_a(arg,self.A), cidrlength):
966 break
967
968 elif m == 'mx':
969 self.check_lookups()
970 if self.cidrmatch(self.dns_mx(arg), cidrlength):
971 break
972
973 elif m == 'ip4':
974 if self.v == 'in-addr':
975 try:
976 if self.cidrmatch([arg], cidrlength): break
977 except socket.error:
978 raise PermError('syntax error', mech)
979
980 elif m == 'ip6':
981 if self.v == 'ip6':
982 try:
983 arg = inet_pton(arg)
984 if self.cidrmatch([arg], cidrlength): break
985 except socket.error:
986 raise PermError('syntax error', mech)
987
988 elif m == 'ptr':
989 self.check_lookups()
990 if domainmatch(self.validated_ptrs(), arg):
991 break
992
993 else:
994
995 if redirect:
996
997 redirect_record = self.dns_spf(redirect)
998 if not redirect_record:
999 raise PermError('redirect domain has no SPF record',
1000 redirect)
1001
1002 if not recursion:
1003 self.exps = dict(self.defexps)
1004 self.options = {}
1005 return self.check1(redirect_record, redirect, recursion)
1006 result = default
1007 mech = None
1008
1009 if not recursion:
1010 self.mechanism = mech
1011 if result == 'fail':
1012 return (result, 550, exps[result])
1013 else:
1014 return (result, 250, exps[result])
1015
1022
1024 """Expand an explanation."""
1025 if spec:
1026 a = self.dns_txt(spec)
1027 if len(a) == 1:
1028 try:
1029 return self.expand(a[0], stripdot=False)
1030 except PermError:
1031
1032 pass
1033 if self.strict > 1:
1034 raise PermError('Empty domain-spec on exp=')
1035
1036
1037 return None
1038
1040 """Do SPF RFC macro expansion.
1041
1042 Examples:
1043 >>> q = query(s='strong-bad@email.example.com',
1044 ... h='mx.example.org', i='192.0.2.3')
1045 >>> q.p = 'mx.example.org'
1046 >>> q.r = 'example.net'
1047
1048 >>> q.expand('%{d}')
1049 'email.example.com'
1050
1051 >>> q.expand('%{d4}')
1052 'email.example.com'
1053
1054 >>> q.expand('%{d3}')
1055 'email.example.com'
1056
1057 >>> q.expand('%{d2}')
1058 'example.com'
1059
1060 >>> q.expand('%{d1}')
1061 'com'
1062
1063 >>> q.expand('%{p}')
1064 'mx.example.org'
1065
1066 >>> q.expand('%{p2}')
1067 'example.org'
1068
1069 >>> q.expand('%{dr}')
1070 'com.example.email'
1071
1072 >>> q.expand('%{d2r}')
1073 'example.email'
1074
1075 >>> q.expand('%{l}')
1076 'strong-bad'
1077
1078 >>> q.expand('%{l-}')
1079 'strong.bad'
1080
1081 >>> q.expand('%{lr}')
1082 'strong-bad'
1083
1084 >>> q.expand('%{lr-}')
1085 'bad.strong'
1086
1087 >>> q.expand('%{l1r-}')
1088 'strong'
1089
1090 >>> q.expand('%{c}',stripdot=False)
1091 '192.0.2.3'
1092
1093 >>> q.expand('%{r}',stripdot=False)
1094 'example.net'
1095
1096 >>> q.expand('%{ir}.%{v}._spf.%{d2}')
1097 '3.2.0.192.in-addr._spf.example.com'
1098
1099 >>> q.expand('%{lr-}.lp._spf.%{d2}')
1100 'bad.strong.lp._spf.example.com'
1101
1102 >>> q.expand('%{lr-}.lp.%{ir}.%{v}._spf.%{d2}')
1103 'bad.strong.lp.3.2.0.192.in-addr._spf.example.com'
1104
1105 >>> q.expand('%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}')
1106 '3.2.0.192.in-addr.strong.lp._spf.example.com'
1107
1108 >>> try: q.expand('%(ir).%{v}.%{l1r-}.lp._spf.%{d2}')
1109 ... except PermError,x: print x
1110 invalid-macro-char : %(ir)
1111
1112 >>> q.expand('%{p2}.trusted-domains.example.net')
1113 'example.org.trusted-domains.example.net'
1114
1115 >>> q.expand('%{p2}.trusted-domains.example.net.')
1116 'example.org.trusted-domains.example.net'
1117
1118 >>> q = query(s='@email.example.com',
1119 ... h='mx.example.org', i='192.0.2.3')
1120 >>> q.p = 'mx.example.org'
1121 >>> q.expand('%{l}')
1122 'postmaster'
1123
1124 """
1125 macro_delimiters = ['{', '%', '-', '_']
1126 end = 0
1127 result = ''
1128 macro_count = str.count('%')
1129 if macro_count != 0:
1130 labels = str.split('.')
1131 for label in labels:
1132 is_macro = False
1133 if len(label) > 1:
1134 if label[0] == '%':
1135 for delimit in macro_delimiters:
1136 if label[1] == delimit:
1137 is_macro = True
1138 if not is_macro:
1139 raise PermError ('invalid-macro-char ', label)
1140 break
1141 for i in RE_CHAR.finditer(str):
1142 result += str[end:i.start()]
1143 macro = str[i.start():i.end()]
1144 if macro == '%%':
1145 result += '%'
1146 elif macro == '%_':
1147 result += ' '
1148 elif macro == '%-':
1149 result += '%20'
1150 else:
1151 letter = macro[2].lower()
1152
1153 if letter == 'p':
1154 self.getp()
1155 elif letter in 'crt' and stripdot:
1156 raise PermError(
1157 'c,r,t macros allowed in exp= text only', macro)
1158 expansion = getattr(self, letter, self)
1159 if expansion:
1160 if expansion == self:
1161 raise PermError('Unknown Macro Encountered', macro)
1162 e = expand_one(expansion, macro[3:-1], JOINERS.get(letter))
1163 if letter != macro[2]:
1164 e = urllib.quote(e)
1165 result += e
1166
1167 end = i.end()
1168 result += str[end:]
1169 if stripdot and result.endswith('.'):
1170 result = result[:-1]
1171 if result.count('.') != 0:
1172 if len(result) > 253:
1173 result = result[(result.index('.')+1):]
1174 return result
1175
1177 """Get the SPF record recorded in DNS for a specific domain
1178 name. Returns None if not found, or if more than one record
1179 is found.
1180 """
1181
1182
1183 for label in domain.split('.'):
1184 if not label or len(label) > 63:
1185 return None
1186
1187 a = [t for t in self.dns_txt(domain) if RE_SPF.match(t)]
1188 if len(a) > 1:
1189 raise PermError('Two or more type TXT spf records found.')
1190 if len(a) == 1 and self.strict < 2:
1191 return a[0]
1192
1193 try:
1194 b = [t for t in self.dns_99(domain) if RE_SPF.match(t)]
1195 except TempError,x:
1196
1197 if self.strict > 1: raise TempError(x)
1198 b = []
1199
1200 if len(b) > 1:
1201 raise PermError('Two or more type SPF spf records found.')
1202 if len(b) == 1:
1203 if self.strict > 1 and len(a) == 1 and a[0] != b[0]:
1204
1205 raise AmbiguityWarning(
1206 'v=spf1 records of both type TXT and SPF (type 99) present, but not identical')
1207 return b[0]
1208 if len(a) == 1:
1209 return a[0]
1210 if DELEGATE:
1211 a = [t
1212 for t in self.dns_txt(domain+'._spf.'+DELEGATE)
1213 if RE_SPF.match(t)
1214 ]
1215 if len(a) == 1: return a[0]
1216 return None
1217
1219 "Get a list of TXT records for a domain name."
1220 if domainname:
1221 return [''.join(a) for a in self.dns(domainname, 'TXT')]
1222 return []
1223 - def dns_99(self, domainname):
1224 "Get a list of type SPF=99 records for a domain name."
1225 if domainname:
1226 return [''.join(a) for a in self.dns(domainname, 'SPF')]
1227 return []
1228
1229 - def dns_mx(self, domainname):
1230 """Get a list of IP addresses for all MX exchanges for a
1231 domain name.
1232 """
1233
1234
1235 mxnames = self.dns(domainname, 'MX')
1236 if self.strict:
1237 max = MAX_MX
1238 if self.strict > 1:
1239 if len(mxnames) > MAX_MX:
1240 raise AmbiguityWarning(
1241 'More than %d MX records returned'%MAX_MX)
1242 if len(mxnames) == 0:
1243 raise AmbiguityWarning(
1244 'No MX records found for mx mechanism', domainname)
1245 else:
1246 max = MAX_MX * 4
1247 return [a for mx in mxnames[:max] for a in self.dns_a(mx[1],self.A)]
1248
1249 - def dns_a(self, domainname, A='A'):
1250 """Get a list of IP addresses for a domainname.
1251 """
1252 if not domainname: return []
1253 if self.strict > 1:
1254 alist = self.dns(domainname, A)
1255 if len(alist) == 0:
1256 raise AmbiguityWarning(
1257 'No %s records found for'%A, domainname)
1258 else:
1259 return alist
1260 return self.dns(domainname, A)
1261
1263 """Figure out the validated PTR domain names for the connect IP."""
1264
1265 if self.strict:
1266 max = MAX_PTR
1267 if self.strict > 1:
1268
1269 try:
1270 ptrnames = self.dns_ptr(self.i)
1271 if len(ptrnames) > max:
1272 warning = 'More than %d PTR records returned' % max
1273 raise AmbiguityWarning(warning, self.i)
1274 else:
1275 if len(ptrnames) == 0:
1276 raise AmbiguityWarning(
1277 'No PTR records found for ptr mechanism', self.c)
1278 except:
1279 raise AmbiguityWarning(
1280 'No PTR records found for ptr mechanism', self.i)
1281 else:
1282 max = MAX_PTR * 4
1283 cidrlength = self.cidrmax
1284 return [p for p in self.dns_ptr(self.i)[:max]
1285 if self.cidrmatch(self.dns_a(p,self.A),cidrlength)]
1286
1288 """Get a list of domain names for an IP address."""
1289 return self.dns('%s.%s.arpa'%(reverse_dots(i),self.v), 'PTR')
1290
1291
1292
1293
1294
1295 SAFE2CACHE = {
1296 ('MX','A'): None,
1297 ('MX','MX'): None,
1298 ('CNAME','A'): None,
1299 ('CNAME','CNAME'): None,
1300 ('A','A'): None,
1301 ('AAAA','AAAA'): None,
1302 ('PTR','PTR'): None,
1303 ('TXT','TXT'): None,
1304 ('SPF','SPF'): None
1305 }
1306
1307 - def dns(self, name, qtype, cnames=None):
1308 """DNS query.
1309
1310 If the result is in cache, return that. Otherwise pull the
1311 result from DNS, and cache ALL answers, so additional info
1312 is available for further queries later.
1313
1314 CNAMEs are followed.
1315
1316 If there is no data, [] is returned.
1317
1318 pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
1319 post: isinstance(__return__, types.ListType)
1320 """
1321 result = self.cache.get( (name, qtype) )
1322 cname = None
1323
1324 if not result:
1325 safe2cache = query.SAFE2CACHE
1326 for k, v in DNSLookup(name, qtype, self.strict):
1327 if k == (name, 'CNAME'):
1328 cname = v
1329 if (qtype,k[1]) in safe2cache:
1330 self.cache.setdefault(k, []).append(v)
1331 result = self.cache.get( (name, qtype), [])
1332 if not result and cname:
1333 if not cnames:
1334 cnames = {}
1335 elif len(cnames) >= MAX_CNAME:
1336
1337 raise PermError('Length of CNAME chain exceeds %d' % MAX_CNAME)
1338 cnames[name] = cname
1339 if cname in cnames:
1340 raise PermError, 'CNAME loop'
1341 result = self.dns(cname, qtype, cnames=cnames)
1342 return result
1343
1345 """Match connect IP against a list of other IP addresses."""
1346 try:
1347 if self.v == 'ip6':
1348 MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
1349 bin = bin2long6
1350 else:
1351 MASK = 0xFFFFFFFFL
1352 bin = addr2bin
1353 c = ~(MASK >> n) & MASK & self.ip
1354 for ip in [bin(ip) for ip in ipaddrs]:
1355 if c == ~(MASK >> n) & MASK & ip: return True
1356 except socket.error: pass
1357 return False
1358
1360 if not receiver:
1361 receiver = self.r
1362 client_ip = self.c
1363 helo = quote_value(self.h)
1364 if self.ident == 'helo':
1365 envelope_from = None
1366 else:
1367 envelope_from = quote_value(self.s)
1368 if res == 'permerror' and self.mech:
1369 tag = ' '.join([res] + self.mech)
1370 problem = quote_value(' '.join(self.mech))
1371 else:
1372 tag = res
1373 problem = None
1374 mechanism = quote_value(self.mechanism)
1375 res = ['%s (%s: %s)' % (tag,receiver,self.get_header_comment(res))]
1376 for k in ('client_ip','envelope_from','helo','receiver',
1377 'problem','mechanism'):
1378 v = locals()[k]
1379 if v: res.append('%s=%s;'%(k,v))
1380 res.append('identity=%s'%self.ident)
1381 return ' '.join(res)
1382
1384 """Return comment for Received-SPF header.
1385 """
1386 sender = self.o
1387 if res == 'pass':
1388 return \
1389 "domain of %s designates %s as permitted sender" \
1390 % (sender, self.c)
1391 elif res == 'softfail': return \
1392 "transitioning domain of %s does not designate %s as permitted sender" \
1393 % (sender, self.c)
1394 elif res == 'neutral': return \
1395 "%s is neither permitted nor denied by domain of %s" \
1396 % (self.c, sender)
1397 elif res == 'none': return \
1398 "%s is neither permitted nor denied by domain of %s" \
1399 % (self.c, sender)
1400
1401 elif res == 'permerror': return \
1402 "permanent error in processing domain of %s: %s" \
1403 % (sender, self.prob)
1404 elif res == 'temperror': return \
1405 "temporary error in processing during lookup of %s" % sender
1406 elif res == 'fail': return \
1407 "domain of %s does not designate %s as permitted sender" \
1408 % (sender, self.c)
1409 raise ValueError("invalid SPF result for header comment: "+res)
1410
1412 """Given a sender email s and a HELO domain h, create a valid tuple
1413 (l, d) local-part and domain-part.
1414
1415 >>> split_email('', 'wayforward.net')
1416 ('postmaster', 'wayforward.net')
1417
1418 >>> split_email('foo.com', 'wayforward.net')
1419 ('postmaster', 'foo.com')
1420
1421 >>> split_email('terry@wayforward.net', 'optsw.com')
1422 ('terry', 'wayforward.net')
1423 """
1424 if not s:
1425 return 'postmaster', h
1426 else:
1427 parts = s.split('@', 1)
1428 if parts[0] == '':
1429 parts[0] = 'postmaster'
1430 if len(parts) == 2:
1431 return tuple(parts)
1432 else:
1433 return 'postmaster', s
1434
1436 """Quote the value for a key-value pair in Received-SPF header field
1437 if needed. No quoting needed for a dot-atom value.
1438
1439 >>> quote_value('foo@bar.com')
1440 '"foo@bar.com"'
1441 >>> quote_value('mail.example.com')
1442 'mail.example.com'
1443 >>> quote_value('A:1.2.3.4')
1444 '"A:1.2.3.4"'
1445 >>> quote_value('abc"def')
1446 '"abc\\\\"def"'
1447 >>> quote_value(r'abc\def')
1448 '"abc\\\\\\\\def"'
1449 >>> quote_value('abc..def')
1450 '"abc..def"'
1451 >>> quote_value('')
1452 '""'
1453 >>> quote_value(None)
1454 """
1455 if s is None or RE_DOT_ATOM.match(s):
1456 return s
1457 return '"' + s.replace('\\',r'\\').replace('"',r'\"'
1458 ).replace('\x00',r'\x00') + '"'
1459
1461 """Breaks A, MX, IP4, and PTR mechanisms into a (name, domain,
1462 cidr,cidr6) tuple. The domain portion defaults to d if not present,
1463 the cidr defaults to 32 if not present.
1464
1465 Examples:
1466
1467 >>> parse_mechanism('a', 'foo.com')
1468 ('a', 'foo.com', None, None)
1469 >>> parse_mechanism('exists','foo.com')
1470 ('exists', None, None, None)
1471 >>> parse_mechanism('a:bar.com', 'foo.com')
1472 ('a', 'bar.com', None, None)
1473 >>> parse_mechanism('a/24', 'foo.com')
1474 ('a', 'foo.com', 24, None)
1475 >>> parse_mechanism('A:foo:bar.com/16//48', 'foo.com')
1476 ('a', 'foo:bar.com', 16, 48)
1477 >>> parse_mechanism('-exists:%{i}.%{s1}.100/86400.rate.%{d}','foo.com')
1478 ('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', None, None)
1479 >>> parse_mechanism('mx:%%%_/.Claranet.de/27','foo.com')
1480 ('mx', '%%%_/.Claranet.de', 27, None)
1481 >>> parse_mechanism('mx:%{d}//97','foo.com')
1482 ('mx', '%{d}', None, 97)
1483 >>> parse_mechanism('iP4:192.0.0.0/8','foo.com')
1484 ('ip4', '192.0.0.0', 8, None)
1485 """
1486
1487 a = RE_DUAL_CIDR.split(str)
1488 if len(a) == 3:
1489 str, cidr6 = a[0], int(a[1])
1490 else:
1491 cidr6 = None
1492 a = RE_CIDR.split(str)
1493 if len(a) == 3:
1494 str, cidr = a[0], int(a[1])
1495 else:
1496 cidr = None
1497
1498 a = str.split(':', 1)
1499 if len(a) < 2:
1500 str = str.lower()
1501 if str == 'exists': d = None
1502 return str, d, cidr, cidr6
1503 return a[0].lower(), a[1], cidr, cidr6
1504
1506 """Reverse dotted IP addresses or domain names.
1507
1508 Examples:
1509
1510 >>> reverse_dots('192.168.0.145')
1511 '145.0.168.192'
1512 >>> reverse_dots('email.example.com')
1513 'com.example.email'
1514 """
1515 a = name.split('.')
1516 a.reverse()
1517 return '.'.join(a)
1518
1519 -def domainmatch(ptrs, domainsuffix):
1520 """grep for a given domain suffix against a list of validated PTR
1521 domain names.
1522
1523 Examples:
1524
1525 >>> domainmatch(['FOO.COM'], 'foo.com')
1526 1
1527 >>> domainmatch(['moo.foo.com'], 'FOO.COM')
1528 1
1529 >>> domainmatch(['moo.bar.com'], 'foo.com')
1530 0
1531 """
1532 domainsuffix = domainsuffix.lower()
1533 for ptr in ptrs:
1534 ptr = ptr.lower()
1535
1536 if ptr == domainsuffix or ptr.endswith('.' + domainsuffix):
1537 return True
1538
1539 return False
1540
1542 """Convert a string IPv4 address into an unsigned integer.
1543
1544 Examples:
1545
1546 >>> addr2bin('127.0.0.1')
1547 2130706433L
1548 >>> addr2bin('127.0.0.1') == socket.INADDR_LOOPBACK
1549 1
1550 >>> addr2bin('255.255.255.254')
1551 4294967294L
1552 >>> addr2bin('192.168.0.1')
1553 3232235521L
1554
1555 Unlike DNS.addr2bin, the n, n.n, and n.n.n forms for IP addresses
1556 are handled as well:
1557
1558 >>> addr2bin('10.65536')
1559 167837696L
1560 >>> 10 * (2 ** 24) + 65536
1561 167837696
1562 >>> addr2bin('10.93.512')
1563 173867520L
1564 >>> 10 * (2 ** 24) + 93 * (2 ** 16) + 512
1565 173867520
1566 """
1567 return struct.unpack("!L", socket.inet_aton(str))[0]
1568
1570 """Convert binary IP6 address into an unsigned Python long integer."""
1571 h, l = struct.unpack("!QQ", str)
1572 return h << 64 | l
1573
1574 if hasattr(socket,'has_ipv6') and socket.has_ipv6:
1576 return socket.inet_ntop(socket.AF_INET6,s)
1578 return socket.inet_pton(socket.AF_INET6,s)
1579 else:
1580 from SPF.pyip6 import inet_ntop, inet_pton
1581
1583 if not str:
1584 return expansion
1585 ln, reverse, delimiters = RE_ARGS.split(str)[1:4]
1586 if not delimiters:
1587 delimiters = '.'
1588 expansion = split(expansion, delimiters, joiner)
1589 if reverse: expansion.reverse()
1590 if ln: expansion = expansion[-int(ln)*2+1:]
1591 return ''.join(expansion)
1592
1593 -def split(str, delimiters, joiner=None):
1594 """Split a string into pieces by a set of delimiter characters. The
1595 resulting list is delimited by joiner, or the original delimiter if
1596 joiner is not specified.
1597
1598 Examples:
1599
1600 >>> split('192.168.0.45', '.')
1601 ['192', '.', '168', '.', '0', '.', '45']
1602 >>> split('terry@wayforward.net', '@.')
1603 ['terry', '@', 'wayforward', '.', 'net']
1604 >>> split('terry@wayforward.net', '@.', '.')
1605 ['terry', '.', 'wayforward', '.', 'net']
1606 """
1607 result, element = [], ''
1608 for c in str:
1609 if c in delimiters:
1610 result.append(element)
1611 element = ''
1612 if joiner:
1613 result.append(joiner)
1614 else:
1615 result.append(c)
1616 else:
1617 element += c
1618 result.append(element)
1619 return result
1620
1622 """Returns spftxt with local inserted just before last non-fail
1623 mechanism. This is how the libspf{2} libraries handle "local-policy".
1624
1625 Examples:
1626
1627 >>> insert_libspf_local_policy('v=spf1 -all')
1628 'v=spf1 -all'
1629 >>> insert_libspf_local_policy('v=spf1 -all','mx')
1630 'v=spf1 -all'
1631 >>> insert_libspf_local_policy('v=spf1','a mx ptr')
1632 'v=spf1 a mx ptr'
1633 >>> insert_libspf_local_policy('v=spf1 mx -all','a ptr')
1634 'v=spf1 mx a ptr -all'
1635 >>> insert_libspf_local_policy('v=spf1 mx -include:foo.co +all','a ptr')
1636 'v=spf1 mx a ptr -include:foo.co +all'
1637
1638 # FIXME: is this right? If so, "last non-fail" is a bogus description.
1639 >>> insert_libspf_local_policy('v=spf1 mx ?include:foo.co +all','a ptr')
1640 'v=spf1 mx a ptr ?include:foo.co +all'
1641 >>> spf='v=spf1 ip4:1.2.3.4 -a:example.net -all'
1642 >>> local='ip4:192.0.2.3 a:example.org'
1643 >>> insert_libspf_local_policy(spf,local)
1644 'v=spf1 ip4:1.2.3.4 ip4:192.0.2.3 a:example.org -a:example.net -all'
1645 """
1646
1647
1648
1649
1650 if not local: return spftxt
1651 spf = spftxt.split()[1:]
1652 if spf:
1653
1654
1655 spf.reverse()
1656 for mech in spf:
1657
1658
1659 if not RESULTS.get(mech[0]):
1660
1661 where = spf.index(mech)
1662 spf[where:where] = [local]
1663 spf.reverse()
1664 local = ' '.join(spf)
1665 break
1666 else:
1667 return spftxt
1668
1669
1670
1671 return 'v=spf1 '+local
1672
1674 import doctest, spf
1675 return doctest.testmod(spf)
1676
1677 if __name__ == '__main__':
1678 import sys
1679 if len(sys.argv) == 1:
1680 print USAGE
1681 _test()
1682 elif len(sys.argv) == 2:
1683 q = query(i='127.0.0.1', s='localhost', h='unknown',
1684 receiver=socket.gethostname())
1685 print q.dns_spf(sys.argv[1])
1686 elif len(sys.argv) == 4:
1687 print check(i=sys.argv[1], s=sys.argv[2], h=sys.argv[3],
1688 receiver=socket.gethostname())
1689 elif len(sys.argv) == 5:
1690 i, s, h = sys.argv[2:]
1691 q = query(i=i, s=s, h=h, receiver=socket.gethostname(),
1692 strict=False)
1693 print q.check(sys.argv[1]),q.mechanism
1694 if q.perm_error and q.perm_error.ext:
1695 print q.perm_error.ext
1696 if q.options: print q.options
1697 else:
1698 print USAGE
1699