Release 0.7.0
This commit is contained in:
@@ -45,6 +45,15 @@ For news, bugfixes, etc. visit the home page for this implementation at
|
||||
# Terrence is not responding to email.
|
||||
#
|
||||
# $Log$
|
||||
# Revision 1.13 2004/07/23 19:23:12 stuart
|
||||
# Always fail to match on ip6, until we support it properly.
|
||||
#
|
||||
# Revision 1.12 2004/07/23 18:48:15 stuart
|
||||
# Fold CID parsing into spf
|
||||
#
|
||||
# Revision 1.11 2004/07/21 21:32:01 stuart
|
||||
# Handle CID records (Microsoft XML format).
|
||||
#
|
||||
# Revision 1.10 2004/04/19 22:12:11 stuart
|
||||
# Release 0.6.9
|
||||
#
|
||||
@@ -97,6 +106,144 @@ import struct # for pack() and unpack()
|
||||
import time # for time()
|
||||
|
||||
import DNS # http://pydns.sourceforge.net
|
||||
import xml.sax
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Convert a MS Caller-ID entry (XML) to a SPF entry
|
||||
#
|
||||
# (c) 2004 by Ernesto Baschny
|
||||
# (c) 2004 Python version by Stuart Gathman
|
||||
#
|
||||
# Date: 2004-02-25
|
||||
# Version: 1.0
|
||||
#
|
||||
# Usage:
|
||||
# ./cid2spf.pl "<ep xmlns='http://ms.net/1'>...</ep>"
|
||||
#
|
||||
# Note that the 'include' directives will also have to be checked and
|
||||
# "translated". Future versions of this script might be able to get a
|
||||
# domain name as an argument and "crawl" the DNS for the necessary
|
||||
# information.
|
||||
#
|
||||
# A complete reverse translation (SPF -> CID) might be impossible, since
|
||||
# there are no way to handle:
|
||||
# - PTR and EXISTS mechanism
|
||||
# - MX mechanism with an different domain as argument
|
||||
# - macros
|
||||
#
|
||||
# References:
|
||||
# http://www.microsoft.com/mscorp/twc/privacy/spam_callerid.mspx
|
||||
# http://spf.pobox.com/
|
||||
#
|
||||
# Known bugs:
|
||||
# - Currently it won't handle the exclusions provided in the A and R
|
||||
# tags (prefix '!'). They will show up "as-is" in the SPF record
|
||||
# - I really haven't read the MS-CID specs in-depth, so there are probably
|
||||
# other bugs too :)
|
||||
#
|
||||
# Ernesto Baschny <ernst@baschny.de>
|
||||
#
|
||||
|
||||
class CIDParser(xml.sax.ContentHandler):
|
||||
"Convert a MS Caller-ID entry (XML) to a SPF entry."
|
||||
|
||||
def __init__(self,q=None):
|
||||
self.spf = []
|
||||
self.action = '-all'
|
||||
self.has_servers = None
|
||||
self.spf_entry = None
|
||||
if q:
|
||||
self.spf_query = q
|
||||
else:
|
||||
self.spf_query = query(i='127.0.0.1', s='localhost', h='unknown')
|
||||
|
||||
def startElement(self,tag,attr):
|
||||
if tag == 'm':
|
||||
if self.has_servers != None and not self.has_servers:
|
||||
raise ValueError(
|
||||
"Declared <noMailServers\> and later <m>, this CID entry is not valid."
|
||||
)
|
||||
self.has_servers = True
|
||||
elif tag == 'noMailServers':
|
||||
if self.has_servers:
|
||||
raise ValueError(
|
||||
"Declared <m> and later <noMailServers\>, this CID entry is not valid."
|
||||
)
|
||||
self.has_servers = False
|
||||
elif tag == 'ep':
|
||||
if attr.has_key('testing') and attr.getValue('testing') == 'true':
|
||||
# A CID with 'testing' found:
|
||||
# From the MS-specs:
|
||||
# "Documents in which such attribute is present with a true
|
||||
# value SHOULD be entirely ignored (one should act as if the
|
||||
# document were absent)"
|
||||
# From the SPF-specs:
|
||||
# "Neutral (?): The SPF client MUST proceed as if a domain did
|
||||
# not publish SPF data."
|
||||
# So we set SPF action to "neutral":
|
||||
self.action = '?all'
|
||||
elif tag == 'mx':
|
||||
# The empty MX-tag, same as SPF's MX-mechanism
|
||||
self.spf.append('mx')
|
||||
self.tag = tag
|
||||
|
||||
def characters(self,text):
|
||||
tag = self.tag
|
||||
# Remove starting and trailing spaces from text:
|
||||
text = text.strip()
|
||||
|
||||
if tag == 'a' or tag == 'r':
|
||||
# The A and R tags from MS-CID are both handled by the
|
||||
# ipv4/6-mechanisms from SPF:
|
||||
if text.find(':') < 0:
|
||||
mechanism = 'ip4'
|
||||
else:
|
||||
mechanism = 'ip6'
|
||||
self.spf.append(mechanism + ':' + text)
|
||||
elif tag == 'indirect':
|
||||
# MS-CID's indirect is "sort of" the include from SPF:
|
||||
# Not really true, because the <indirect> tag from MS-CID also
|
||||
# provides a fallback in case the included domain doesn't provide
|
||||
# _ep-records: The inbound MX-servers of the included domains
|
||||
# are added to the list of allowed outgoing mailservers for the
|
||||
# domain that declared the _ep-record with the <indirect> tag.
|
||||
# In SPF you would use the 'mx:domain' to handle this, but this
|
||||
# wouldn't depend on referred domain having or not SPF-records.
|
||||
cid_xml = self.cid_txt(text)
|
||||
if cid_xml:
|
||||
p = CIDParser()
|
||||
xml.sax.parseString(cid_xml,p)
|
||||
if p.has_servers != False:
|
||||
self.spf += p.spf
|
||||
else:
|
||||
self.spf.append('mx:' + text)
|
||||
|
||||
def cid_txt(self,domain):
|
||||
q = self.spf_query
|
||||
domain='_ep.' + domain
|
||||
a = q.dns_txt(domain)
|
||||
if not a: return None
|
||||
if a[0].lower().startswith('<ep ') and a[-1].lower().endswith('</ep>'):
|
||||
return ''.join(a)
|
||||
return None
|
||||
|
||||
def endElement(self,tag):
|
||||
if tag == 'ep':
|
||||
# This is the end... assemble what we've got
|
||||
spf_entry = ['v=spf1']
|
||||
if self.has_servers != False:
|
||||
spf_entry += self.spf
|
||||
spf_entry.append(self.action)
|
||||
self.spf_entry = ' '.join(spf_entry)
|
||||
|
||||
def spf_txt(self,cid_xml):
|
||||
if not cid_xml.startswith('<'):
|
||||
cid_xml = self.cid_txt(cid_xml)
|
||||
if not cid_xml: return None
|
||||
# Parse the beast. Any XML-problem will be reported by xlm.sax
|
||||
self.spf_entry = None
|
||||
xml.sax.parseString(cid_xml,self)
|
||||
return self.spf_entry
|
||||
|
||||
# 32-bit IPv4 address mask
|
||||
MASK = 0xFFFFFFFFL
|
||||
@@ -330,9 +477,14 @@ class query(object):
|
||||
cidrlength):
|
||||
break
|
||||
|
||||
elif m in ('ip4', 'ipv4') and arg != self.d:
|
||||
elif m in ('ip4', 'ipv4', 'ip') and arg != self.d:
|
||||
if cidrmatch(self.i, [arg], cidrlength):
|
||||
break
|
||||
elif m == 'ip6':
|
||||
# Until we support IPV6, we should never
|
||||
# get an IPv6 connection. So this mech
|
||||
# will never match.
|
||||
pass
|
||||
|
||||
elif m in ('ptr', 'prt'):
|
||||
if domainmatch(self.validated_ptrs(self.i),
|
||||
@@ -465,17 +617,24 @@ class query(object):
|
||||
is found.
|
||||
"""
|
||||
a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')]
|
||||
if not a and DELEGATE:
|
||||
a = [t
|
||||
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
|
||||
if t.startswith('v=spf1')
|
||||
]
|
||||
if not a:
|
||||
if DELEGATE:
|
||||
a = [t
|
||||
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
|
||||
if t.startswith('v=spf1')
|
||||
]
|
||||
if not a:
|
||||
# No SPF record: convert and return CID if present
|
||||
p = CIDParser(q=self)
|
||||
return p.spf_txt(domain)
|
||||
|
||||
if len(a) == 1:
|
||||
return a[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def dns_txt(self, domainname):
|
||||
"Get a list of TXT records for a domain name."
|
||||
if domainname:
|
||||
return [t for a in self.dns(domainname, 'TXT') for t in a]
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user