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

Source Code for Module spf

   1  #!/usr/bin/env python 
   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  # Changes: 
  31  #    9-dec-2003, v1.1, Meng Weng Wong added PTR code, THANK YOU 
  32  #   11-dec-2003, v1.2, ttw added macro expansion, exp=, and redirect= 
  33  #   13-dec-2003, v1.3, ttw added %{o} original domain macro, 
  34  #                      print spf result on command line, support default=, 
  35  #                      support localhost, follow DNS CNAMEs, cache DNS results 
  36  #                      during query, support Python 2.2 for Mac OS X 
  37  #   16-dec-2003, v1.4, ttw fixed include handling (include is a mechanism, 
  38  #                      complete with status results, so -include: should work. 
  39  #                      Expand macros AFTER looking for status characters ?-+ 
  40  #                      so altavista.com SPF records work. 
  41  #   17-dec-2003, v1.5, ttw use socket.inet_aton() instead of DNS.addr2bin, so 
  42  #                      n, n.n, and n.n.n forms for IPv4 addresses work, and to 
  43  #                      ditch the annoying Python 2.4 FutureWarning 
  44  #   18-dec-2003, v1.6, Failures on Intel hardware: endianness.  Use ! on 
  45  #                      struct.pack(), struct.unpack(). 
  46  # 
  47  # Development taken over by Stuart Gathman <stuart@bmsi.com>. 
  48  # 
  49  # $Log: spf.py,v $ 
  50  # Revision 1.133  2007/01/19 23:25:33  customdesigned 
  51  # Fix validated_ptrs and best_guess. 
  52  # 
  53  # Revision 1.132  2007/01/17 00:47:17  customdesigned 
  54  # Test for and fix illegal implicit mechanisms. 
  55  # 
  56  # Revision 1.131  2007/01/16 23:54:58  customdesigned 
  57  # Test and fix for invalid domain-spec. 
  58  # 
  59  # Revision 1.130  2007/01/15 02:21:10  customdesigned 
  60  # Forget op= on redirect. 
  61  # 
  62  # Revision 1.129  2007/01/14 23:01:58  customdesigned 
  63  # Consolidate duplicate modifier handling. 
  64  # 
  65  # Revision 1.128  2007/01/14 22:56:56  customdesigned 
  66  # op= draft actually uses '.' for separator. 
  67  # 
  68  # Revision 1.127  2007/01/14 05:07:16  customdesigned 
  69  # PermError for duplicate redirect even in lax mode. 
  70  # 
  71  # Revision 1.126  2007/01/14 05:05:25  customdesigned 
  72  # Permerror for duplicate exp= or redirect= 
  73  # 
  74  # Revision 1.125  2007/01/14 04:56:25  customdesigned 
  75  # Parse op= to create a dictionary of option keywords. 
  76  # 
  77  # Revision 1.124  2007/01/13 20:08:54  customdesigned 
  78  # Make exp= compliant with 6.2/4 
  79  # 
  80  # Revision 1.123  2007/01/11 18:49:37  customdesigned 
  81  # Add mechanism to Received-SPF header. 
  82  # 
  83  # Revision 1.122  2007/01/11 18:25:54  customdesigned 
  84  # Record matching mechanism. 
  85  # 
  86  # Revision 1.121  2006/12/30 17:01:52  customdesigned 
  87  # Missed a spot for new result names. 
  88  # 
  89  # Revision 1.120  2006/12/28 04:54:21  customdesigned 
  90  # Skip optional trailing ";" in Received-SPF 
  91  # 
  92  # Revision 1.119  2006/12/28 04:37:12  customdesigned 
  93  # Forgot semicolons. 
  94  # 
  95  # Revision 1.118  2006/12/28 04:04:27  customdesigned 
  96  # Optimize get_header to remove useless key-value pairs. 
  97  # 
  98  # Revision 1.117  2006/12/23 06:31:16  customdesigned 
  99  # Fully quote values in key-value pairs. 
 100  # 
 101  # Revision 1.116  2006/12/23 01:54:54  customdesigned 
 102  # Properly quote key-value pairs in Received-SPF header.  Add test 
 103  # case extension to test it.  Test python IP6 parsing. 
 104  # 
 105  # Revision 1.115  2006/12/22 21:56:37  customdesigned 
 106  # Index error reporting non-mech permerror. 
 107  # 
 108  # Revision 1.114  2006/12/19 02:09:55  customdesigned 
 109  # Remove trailing comma in lax mode. 
 110  # 
 111  # Revision 1.113  2006/12/18 21:34:37  kitterma 
 112  # Updated README to include dnspython. Fixed typo in last commit message. 
 113  # 
 114  # Revision 1.112  2006/12/18 16:58:11  kitterma 
 115  # Added specific error message for mechanisms separated by a comma. 
 116  # 
 117  # Revision 1.111  2006/12/16 21:01:47  customdesigned 
 118  # Move pure python ip6 support to driver package. 
 119  # 
 120  # Revision 1.110  2006/12/16 20:45:58  customdesigned 
 121  # Update version. 
 122  # 
 123  # Revision 1.109  2006/12/16 20:45:23  customdesigned 
 124  # Move dns drivers to package directory. 
 125  # 
 126  # Revision 1.108  2006/11/08 01:27:00  customdesigned 
 127  # Return all key-value-pairs in Received-SPF header for all results. 
 128  # 
 129  # Revision 1.107  2006/11/04 21:58:12  customdesigned 
 130  # Prevent cache poisoning by bogus additional RRs in PTR DNS response. 
 131  # 
 132  # Revision 1.106  2006/10/16 20:48:24  customdesigned 
 133  # More DOS limit tests. 
 134  # 
 135  # Revision 1.105  2006/10/07 22:06:28  kitterma 
 136  # Pass strict status to DNSLookup - will be needed for TCP failover. 
 137  # 
 138  # Revision 1.104  2006/10/07 21:59:37  customdesigned 
 139  # long/empty label tests and fix. 
 140  # 
 141  # Revision 1.103  2006/10/07 18:16:20  customdesigned 
 142  # Add tests for and fix RE_TOPLAB. 
 143  # 
 144  # Revision 1.102  2006/10/05 13:57:15  customdesigned 
 145  # Remove isSPF and make missing space after version tag a warning. 
 146  # 
 147  # Revision 1.101  2006/10/05 13:39:11  customdesigned 
 148  # SPF version tag is case insensitive. 
 149  # 
 150  # Revision 1.100  2006/10/04 02:14:04  customdesigned 
 151  # Remove incomplete saving of result.  Was messing up bmsmilter.  Would 
 152  # be useful if done consistently - and disabled when passing spf= to check(). 
 153  # 
 154  # Revision 1.99  2006/10/03 21:00:26  customdesigned 
 155  # Correct fat fingered merge error. 
 156  # 
 157  # Revision 1.98  2006/10/03 17:35:45  customdesigned 
 158  # Provide python inet_ntop and inet_pton when not socket.has_ipv6 
 159  # 
 160  # Revision 1.97  2006/10/02 17:10:13  customdesigned 
 161  # Test and fix for uppercase macros. 
 162  # 
 163  # Revision 1.96  2006/10/01 01:27:54  customdesigned 
 164  # Switch to pymilter lax processing convention: 
 165  # Always return strict result, extended result in q.perm_error.ext 
 166  # 
 167  # Revision 1.95  2006/09/30 22:53:44  customdesigned 
 168  # Fix getp to obey SHOULDs in RFC. 
 169  # 
 170  # Revision 1.94  2006/09/30 22:23:25  customdesigned 
 171  # p macro tests and fixes 
 172  # 
 173  # Revision 1.93  2006/09/30 20:57:06  customdesigned 
 174  # Remove generator expression for compatibility with python2.3. 
 175  # 
 176  # Revision 1.92  2006/09/30 19:52:52  customdesigned 
 177  # Removed redundant flag and unneeded global. 
 178  # 
 179  # Revision 1.91  2006/09/30 19:37:49  customdesigned 
 180  # Missing L 
 181  # 
 182  # Revision 1.90  2006/09/30 19:29:58  customdesigned 
 183  # pydns returns AAAA RR as binary string 
 184  # 
 185  # Revision 1.89  2006/09/29 20:23:11  customdesigned 
 186  # Optimize cidrmatch 
 187  # 
 188  # Revision 1.88  2006/09/29 19:44:10  customdesigned 
 189  # Fix ptr with ip6 for harsh mode. 
 190  # 
 191  # Revision 1.87  2006/09/29 19:26:53  customdesigned 
 192  # Add PTR tests and fix ip6 ptr 
 193  # 
 194  # Revision 1.86  2006/09/29 17:55:22  customdesigned 
 195  # Pass ip6 tests 
 196  # 
 197  # Revision 1.85  2006/09/29 15:58:02  customdesigned 
 198  # Pass self test on non IP6 python. 
 199  # PTR accepts no cidr. 
 200  # 
 201  # Revision 1.83  2006/09/27 18:09:40  kitterma 
 202  # Converted spf.check to return pre-MARID result codes for drop in 
 203  # compatibility with pySPF 1.6/1.7.  Added new procedure, spf.check2 to 
 204  # return RFC4408 results in a two part answer (result, explanation). 
 205  # This is the external API for pySPF 2.0.  No longer any need to branch 
 206  # for 'classic' and RFC compliant pySPF libraries. 
 207  # 
 208  # Revision 1.82  2006/09/27 18:02:21  kitterma 
 209  # Converted max MX limit to ambiguity warning for validator. 
 210  # 
 211  # Revision 1.81  2006/09/27 17:38:14  kitterma 
 212  # Updated initial comments and moved pre-1.7 changes to spf_changelog. 
 213  # 
 214  # Revision 1.80  2006/09/27 17:33:53  kitterma 
 215  # Fixed indentation error in check0. 
 216  # 
 217  # Revision 1.79  2006/09/26 18:05:44  kitterma 
 218  # Removed unused receiver policy definitions. 
 219  # 
 220  # Revision 1.78  2006/09/26 16:15:50  kitterma 
 221  # added additional IP4 and CIDR validation tests - no code changes. 
 222  # 
 223  # Revision 1.77  2006/09/25 19:42:32  customdesigned 
 224  # Fix unknown macro sentinel 
 225  # 
 226  # Revision 1.76  2006/09/25 19:10:40  customdesigned 
 227  # Fix exp= error and add another failing test. 
 228  # 
 229  # Revision 1.75  2006/09/25 02:02:30  kitterma 
 230  # Fixed redirect-cancels-exp test suite failure. 
 231  # 
 232  # Revision 1.74  2006/09/24 04:04:08  kitterma 
 233  # Implemented check for macro 'c' - Macro unimplimented. 
 234  # 
 235  # Revision 1.73  2006/09/24 02:08:35  kitterma 
 236  # Fixed invalid-macro-char test failure. 
 237  # 
 238  # Revision 1.72  2006/09/23 05:45:52  kitterma 
 239  # Fixed domain-name-truncation test failure 
 240  # 
 241  # Revision 1.71  2006/09/22 01:02:54  kitterma 
 242  # pySPF correction for nolocalpart in rfc4408-tests.yml failed, 4.3/2. 
 243  # Added comments to testspf.py on where to get YAML. 
 244  # 
 245  # Revision 1.70  2006/09/18 02:13:27  kitterma 
 246  # Worked through a large number of pylint issues - all 4 spaces, not a mix 
 247  # of 4 spaces, 2 spaces, and tabs. Caught a few minor errors in the process. 
 248  # All built in tests still pass. 
 249  # 
 250  # Revision 1.69  2006/09/17 18:44:25  kitterma 
 251  # Fixed validation mode only crash bug when rDNS check had no PTR record 
 252  # 
 253  # 
 254  # See spf_changelog.txt for earlier changes. 
 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  # for inet_ntoa() and inet_aton() 
 279  import struct  # for pack() and unpack() 
 280  import time    # for time() 
 281  import urllib  # for quote() 
 282   
283 -def DNSLookup(name, qtype, strict=True):
284 try: 285 from SPF.pydns import DNSLookup 286 except: 287 from SPF.dnspython import DNSLookup 288 return DNSLookup(name, qtype, strict)
289 290 RE_SPF = re.compile(r'^v=spf1$|^v=spf1 ',re.IGNORECASE) 291 292 # Regular expression to look for modifiers 293 RE_MODIFIER = re.compile(r'^([a-z][a-z0-9_\-\.]*)=', re.IGNORECASE) 294 295 # Regular expression to find macro expansions 296 PAT_CHAR = r'%(%|_|-|(\{[^\}]*\}))' 297 RE_CHAR = re.compile(PAT_CHAR) 298 299 # Regular expression to break up a macro expansion 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 # Local parts and senders have their delimiters replaced with '.' during 330 # macro expansion 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 #Note: The following are not formally SPF results 348 'local': 'No SPF result due to local policy', 349 'trusted': 'No SPF check - trusted-forwarder.org', 350 #Ambiguous only used in harsh mode for SPF validation 351 'ambiguous': 'No error, but results may vary' 352 } 353 354 # support pre 2.2.1.... 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 # ...pre 2.2.1 361 362 DELEGATE = None 363 364 # standard default SPF record for best_guess 365 DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr' 366 367 #Whitelisted forwarders here. Additional locally trusted forwarders can be 368 #added to this record. 369 TRUSTED_FORWARDERS = 'v=spf1 ?include:spf.trusted-forwarder.org -all' 370 371 # maximum DNS lookups allowed 372 MAX_LOOKUP = 10 #RFC 4408 Para 10.1 373 MAX_MX = 10 #RFC 4408 Para 10.1 374 MAX_PTR = 10 #RFC 4408 Para 10.1 375 MAX_CNAME = 10 # analogous interpretation to MAX_PTR 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 #If harsh processing, for the validator, is invoked, warn if results 384 #likely deviate from the publishers intention.
385 -class AmbiguityWarning(Exception):
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
392 - def __str__(self):
393 if self.mech: 394 return '%s: %s' %(self.msg, self.mech) 395 return self.msg
396
397 -class TempError(Exception):
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
404 - def __str__(self):
405 if self.mech: 406 return '%s: %s '%(self.msg, self.mech) 407 return self.msg
408
409 -class PermError(Exception):
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
416 - def __str__(self):
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
460 -class query(object):
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 # lazy evaluation 491 if receiver: 492 self.r = receiver 493 else: 494 self.r = 'unknown' 495 # Since the cache does not track Time To Live, it is created 496 # fresh for each query. It is important for efficiently using 497 # multiple results provided in DNS answers. 498 self.cache = {} 499 self.defexps = dict(EXPLANATIONS) 500 self.exps = dict(EXPLANATIONS) 501 self.libspf_local = local # local policy 502 self.lookups = 0 503 # strict can be False, True, or 2 (numeric) for harsh 504 self.strict = strict 505 if i: 506 self.set_ip(i)
507
508 - def set_ip(self, i):
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: # IP4 mapped address 516 self.ip = self.ip & 0xFFFFFFFFL 517 ip6 = False 518 else: 519 ip6 = True 520 # NOTE: self.A is not lowercase, so isn't a macro. See query.expand() 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
535 - def set_default_explanation(self, exp):
536 exps = self.exps 537 defexps = self.defexps 538 for i in 'softfail', 'fail', 'permerror': 539 exps[i] = exp 540 defexps[i] = exp
541
542 - def set_explanation(self, exp):
543 exps = self.exps 544 for i in 'softfail', 'fail', 'permerror': 545 exps[i] = exp
546 547 # Compute p macro only if needed
548 - def getp(self):
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
565 - def best_guess(self, spf=DEFAULT_SPF):
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 = [] # unknown mechanisms 638 # If not strict, certain PermErrors (mispelled 639 # mechanisms, strict processing limits exceeded) 640 # will continue processing. However, the exception 641 # that strict processing would raise is saved here 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 # lax processing encountered a permerror, but continued 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 # Pre-Lentczner draft treats this as an unknown result 672 # and equivalent to no SPF record. 673 return ('permerror', 550, 'SPF Permanent Error: ' + str(x))
674
675 - def check1(self, spf, domain, recursion):
676 # spf rfc: 3.7 Processing Limits 677 # 678 if recursion > MAX_RECURSION: 679 # This should never happen in strict mode 680 # because of the other limits we check, 681 # so if it does, there is something wrong with 682 # our code. It is not a PermError because there is not 683 # necessarily anything wrong with the SPF record. 684 if self.strict: 685 raise AssertionError('Too many levels of recursion') 686 # As an extended result, however, it should be 687 # a PermError. 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
701 - def note_error(self, *msg):
702 if self.strict: 703 raise PermError(*msg) 704 # if lax mode, note error and continue 705 if not self.perm_error: 706 try: 707 raise PermError(*msg) 708 except PermError, x: 709 # FIXME: keep a list of errors for even friendlier diagnostics. 710 self.perm_error = x 711 return self.perm_error
712
713 - def expand_domain(self,arg):
714 "validate and expand domain-spec" 715 # any trailing dot was removed by expand() 716 if RE_TOPLAB.split(arg)[-1]: 717 raise PermError('Invalid domain found (use FQDN)', arg) 718 return self.expand(arg)
719
720 - def validate_mechanism(self, mech):
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 # a mechanism 779 m, arg, cidrlength, cidr6length = parse_mechanism(mech, self.d) 780 # map '?' '+' or '-' to 'neutral' 'pass' or 'fail' 781 if m: 782 result = RESULTS.get(m[0]) 783 if result: 784 # eat '?' '+' or '-' 785 m = m[1:] 786 else: 787 # default pass 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 # validate cidr and dual-cidr 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 # validate 'all' mechanism per RFC 4408 ABNF 848 if m == 'all' and mech.count(':'): 849 # print '|'+ arg + '|', mech, self.d, 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 # split string by whitespace, drop the 'v=spf1' 873 spf = spf.split() 874 # Catch case where SPF record has no spaces. 875 # Can never happen with conforming dns_spf(), however 876 # in the future we might want to give warnings 877 # for common mistakes like IN TXT "v=spf1" "mx" "-all" 878 # in relaxed mode. 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 # copy of explanations to be modified by exp= 885 exps = self.exps 886 redirect = None 887 888 # no mechanisms at all cause unknown result, unless 889 # overridden with 'default=' modifier 890 # 891 default = 'neutral' 892 mechs = [] 893 894 modifiers = [] 895 # Look for modifiers 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 # just use last one in lax mode 909 modifiers.append(mod) 910 if mod == 'exp': 911 # always fetch explanation to check permerrors 912 arg = self.expand_domain(arg) 913 exp = self.get_explanation(arg) 914 if exp and not recursion: 915 # only set explanation in base recursion level 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 # default=- is the same as default=fail 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 # spf rfc: 3.6 Unrecognized Mechanisms and Modifiers 932 self.expand(arg) # syntax error on invalid macro 933 934 # Evaluate mechanisms 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 # Exists wants no response sometimes so don't raise 960 # the warning. 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': # match own connection type only 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': # match own connection type only 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 # no matches 995 if redirect: 996 #Catch redirect to a non-existant SPF record. 997 redirect_record = self.dns_spf(redirect) 998 if not redirect_record: 999 raise PermError('redirect domain has no SPF record', 1000 redirect) 1001 # forget modifiers on redirect 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: # record matching mechanism at base level 1010 self.mechanism = mech 1011 if result == 'fail': 1012 return (result, 550, exps[result]) 1013 else: 1014 return (result, 250, exps[result])
1015
1016 - def check_lookups(self):
1017 self.lookups = self.lookups + 1 1018 if self.lookups > MAX_LOOKUP*4: 1019 raise PermError('More than %d DNS lookups'%MAX_LOOKUP*4) 1020 if self.lookups > MAX_LOOKUP: 1021 self.note_error('Too many DNS lookups')
1022
1023 - def get_explanation(self, spec):
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 # RFC4408 6.2/4 syntax errors cause exp= to be ignored 1032 pass 1033 if self.strict > 1: 1034 raise PermError('Empty domain-spec on exp=') 1035 # RFC4408 6.2/4 empty domain spec is ignored 1036 # (unless you give precedence to the grammar). 1037 return None
1038
1039 - def expand(self, str, stripdot=True): # macros='slodipvh'
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 # print letter 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
1176 - def dns_spf(self, domain):
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 # Per RFC 4.3/1, check for malformed domain. This produces 1182 # no results as a special case. 1183 for label in domain.split('.'): 1184 if not label or len(label) > 63: 1185 return None 1186 # for performance, check for most common case of TXT first 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 # check official SPF type first when it becomes more popular 1193 try: 1194 b = [t for t in self.dns_99(domain) if RE_SPF.match(t)] 1195 except TempError,x: 1196 # some braindead DNS servers hang on type 99 query 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 #Changed from permerror to warning based on RFC 4408 Auth 48 change 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] # return TXT if SPF wasn't found 1210 if DELEGATE: # use local record if neither found 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
1218 - def dns_txt(self, domainname):
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 # RFC 4408 section 5.4 "mx" 1234 # To prevent DoS attacks, more than 10 MX names MUST NOT be looked up 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
1262 - def validated_ptrs(self):
1263 """Figure out the validated PTR domain names for the connect IP.""" 1264 # To prevent DoS attacks, more than 10 PTR names MUST NOT be looked up 1265 if self.strict: 1266 max = MAX_PTR 1267 if self.strict > 1: 1268 #Break out the number of PTR records returned for testing 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
1287 - def dns_ptr(self, i):
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 # We have to be careful which additional DNS RRs we cache. For 1292 # instance, PTR records are controlled by the connecting IP, and they 1293 # could poison our local cache with bogus A and MX records. 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 #return result # if too many == NX_DOMAIN 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
1344 - def cidrmatch(self, ipaddrs, n):
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
1359 - def get_header(self, res, receiver=None):
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
1383 - def get_header_comment(self, res):
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 #"%s does not designate permitted sender hosts" % sender 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
1411 -def split_email(s, h):
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
1435 -def quote_value(s):
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
1460 -def parse_mechanism(str, d):
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
1505 -def reverse_dots(name):
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
1541 -def addr2bin(str):
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
1569 -def bin2long6(str):
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:
1575 - def inet_ntop(s):
1576 return socket.inet_ntop(socket.AF_INET6,s)
1577 - def inet_pton(s):
1578 return socket.inet_pton(socket.AF_INET6,s)
1579 else: 1580 from SPF.pyip6 import inet_ntop, inet_pton 1581
1582 -def expand_one(expansion, str, joiner):
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
1621 -def insert_libspf_local_policy(spftxt, local=None):
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 # look to find the all (if any) and then put local 1647 # just after last non-fail mechanism. This is how 1648 # libspf2 handles "local policy", and some people 1649 # apparently find it useful (don't ask me why). 1650 if not local: return spftxt 1651 spf = spftxt.split()[1:] 1652 if spf: 1653 # local policy is SPF mechanisms/modifiers with no 1654 # 'v=spf1' at the start 1655 spf.reverse() #find the last non-fail mechanism 1656 for mech in spf: 1657 # map '?' '+' or '-' to 'neutral' 'pass' 1658 # or 'fail' 1659 if not RESULTS.get(mech[0]): 1660 # actually finds last mech with default result 1661 where = spf.index(mech) 1662 spf[where:where] = [local] 1663 spf.reverse() 1664 local = ' '.join(spf) 1665 break 1666 else: 1667 return spftxt # No local policy adds for v=spf1 -all 1668 # Processing limits not applied to local policy. Suggest 1669 # inserting 'local' mechanism to handle this properly 1670 #MAX_LOOKUP = 100 1671 return 'v=spf1 '+local
1672
1673 -def _test():
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