def check_dmarc(msg, spf_result=None, dkim_result=None, dnsfunc=None, psddmarc=False): # get from domain headers, _ = rfc822_parse(msg) from_headers = [ a[1] for a in getaddresses(x[1].decode(errors='ignore').strip() for x in headers if x[0].lower() == b"from") ] if len(from_headers) > 1: # multi-from processing per RFC 7489 6.6.1 domain_results = [] for from_header in from_headers: from_domain = get_domain_part(from_header) domain_results.append( dmarc_per_from(from_domain, spf_result, dkim_result, dnsfunc, psddmarc)) for domain in domain_results: if domain[3] == 'reject': result, result_comment, from_domain, policy = domain return DMARCAuthenticationResult(result=result, result_comment=result_comment, header_from=from_domain, policy=policy) for domain in domain_results: if domain[3] == 'quarantine': result, result_comment, from_domain, policy = domain return DMARCAuthenticationResult(result=result, result_comment=result_comment, header_from=from_domain, policy=policy) for domain in domain_results: if domain[3] == 'none': result, result_comment, from_domain, policy = domain return DMARCAuthenticationResult(result=result, result_comment=result_comment, header_from=from_domain, policy=policy) result, result_comment, from_domain, policy = domain elif len(from_headers) == 1: from_header = from_headers[0] from_domain = get_domain_part(from_header) result, result_comment, from_domain, policy = dmarc_per_from( from_domain, spf_result, dkim_result, dnsfunc, psddmarc) else: result = 'none' if result != 'none': return DMARCAuthenticationResult(result=result, result_comment=result_comment, header_from=from_domain, policy=policy) else: return DMARCAuthenticationResult(result=result, header_from=from_domain)
def send(self, *args): """ Must Be Two Parameters recipient and email body """ if not args: raise SMTPServerError("Fatal error: Email Content must have the recipient and body") if len(args) == 1: raise SMTPServerError("Fatal error: Email Content must have the body") """ try: self.server.connect(self.host,self.port) except Exception,e: raise SMTPServerError(e) """ recipient=args[0] body=args[1] senddate="%s %s"%(time.strftime("%a, %d %b %Y %H:%M:%S",time.localtime()),"+0800") messageid="<%s>"%recipient mimeversion = "1.0" header="From:%s\r\nTo:%s\r\nSubject:%s\r\nMIME-Version:%s\r\nMessage-Id:%s\r\nDate:%s\r\nX-Mailer:%s\r\nContent-Type:%s"%(self.mailfrom,recipient,self.subject,mimeversion,messageid,senddate,XMailer,self.contenttype) content="%s\r\n\r\n%s"%(header,body) content == dkim.rfc822_parse(content) if self.dkim: domain = re.search(r"[^@]+$",self.mailfrom.strip()).group(0) sig = dkim.sign(content,'_dkim',domain,open(self.dkim).read(),include_headers=["From","To","Subject","Date","Message-Id","X-Mailer"],canonicalize=(dkim.Simple, dkim.Relaxed)) content = sig + content Retry = 3 while True: try: self.server.sendmail(self.sender,recipient,content) return True break except smtplib.SMTPServerDisconnected: if Retry == 0: raise SMTPServerError("Lost the Connection with the Server") return False break else: try: self.connect() Retry = Retry - 1 except: Retry = Retry - 1 except Exception,e: if Retry == 0: raise SMTPServerError(e) return False break else: Retry = Retry - 1
def validate_mail(message): # Step 1: Read the from address, so that we at least know where to # send the error message to from_addr = None try: # Get the one and only from address (headers, body) = dkim.rfc822_parse(message) from_addr = __get_raw_email_addr(__get_required_header(headers, "From")) if not is_email_address(from_addr): return (None, False, "'" + str(from_addr) + "' is not valid From: address") except Exception, e: return (None, False, e.__str__())
def validate_mail(message): # Step 1: Read the from address, so that we at least know where to # send the error message to from_addr = None try: # Get the one and only from address (headers, body) = dkim.rfc822_parse(message) from_addr = __get_raw_email_addr(__get_required_header( headers, "From")) if not is_email_address(from_addr): return (None, False, "'" + str(from_addr) + "' is not valid From: address") except Exception, e: return (None, False, e.__str__())
def dkim_canon(canon, message): if True: m = re.match('^(simple|relaxed)/(simple|relaxed)$', canon.strip(' \t\n\r').lower()) if m: try: import dkim (headers, body) = dkim.rfc822_parse(message) ch = dkim.canonicalization.Simple cb = dkim.canonicalization.Simple #print headers #print body #dbginfo('info',str(headers)) if m.group(1) == 'simple': ch = dkim.canonicalization.Simple.canonicalize_headers( headers) else: ch = dkim.canonicalization.Relaxed.canonicalize_headers( headers) #print ch if m.group(2) == 'simple': cb = dkim.canonicalization.Simple.canonicalize_body(body) else: cb = dkim.canonicalization.Relaxed.canonicalize_body(body) #print cb return (ch, cb) except ImportError: print dbginfo( 'warrning', 'Warrning: Missing module: dkimpy. Use: pip install dkimpy' ) print else: #fpr.err('NO MATCH %s' %canon) return None return False
def nofws(message, include_heads): """ No Folding Whitespace 参考: http://tools.ietf.org/html/rfc4870#page-19 """ headers, body = dkim.rfc822_parse(message) headers_dict = dict(headers) heads = [ '%s:%s' % (k, re.sub('\s', '', v)) for k, v in headers_dict.items() if k in include_heads ] data = '\n'.join(heads) + '\n' for e in ('\n' + body.replace('\r', '')).split('\n'): data += re.sub('\s', '', e) + '\n' data = data.rstrip('\n').split('\n') data = '\r\n'.join(data) + '\r\n' return data
def check_dmarc(msg, spf_result=None, dkim_result=None, dnsfunc=None): # get from domain headers, _ = rfc822_parse(msg) from_headers = [x[1] for x in headers if x[0].lower() == b"from"] if len(from_headers) != 1: raise Exception("") from_header = from_headers[0] # kind of janky res = re.search(b'@(.*)>', from_header) from_domain = res.group(1).decode('ascii') # get dmarc record if(dnsfunc): record, _ = receiver_record(from_domain, dnsfunc=dnsfunc) else: record, _ = receiver_record(from_domain) adkim = record.get('adkim', 'r') aspf = record.get('aspf', 'r') # get result result = "fail" if spf_result and spf_result.result == "pass": if aspf == "s" and from_domain == spf_result.smtp_mailfrom: result = "pass" elif aspf == "r" and get_org_domain(from_domain) == get_org_domain(spf_result.smtp_mailfrom): result = "pass" if dkim_result and dkim_result.result == "pass": if adkim == "s" and from_domain == dkim_result.header_d: result = "pass" elif adkim == "r" and get_org_domain(from_domain) == get_org_domain(dkim_result.header_d): result = "pass" return DMARCAuthenticationResult(result=result, header_from=from_domain)
def __validate_mail(message): # First things first (and this is critical), # validate the DKIM signature if not dkim.verify(message): raise ValidateMailException("DKIM signature verification failed\n") # Parse the email (headers, body) = dkim.rfc822_parse(message) # Get the one and only DKIM-Signature header and from address from_addr = __get_raw_email_addr(__get_required_header(headers, "From")) dkim_sig = __get_required_header(headers, "DKIM-Signature") # Check that the from address and the Return-Path address are consistent return_paths = __get_headers_by_name(headers, "Return-Path") if len(return_paths) == 0: raise ValidateMailException("No return paths specified\n") for rp in return_paths: if __get_raw_email_addr(rp) != from_addr: raise ValidateMailException("'Return-Path: " + str(rp) + "' does not match from address.\n") # Check a few things in the DKIM header dkim_fields = parse_dkim_sig(dkim_sig) if 'bh' not in dkim_fields: raise ValidateMailException("Missing the DKIM body hash.\n") if 'h' not in dkim_fields: raise ValidateMailException("Missing DKIM headers key (h=).\n") if 'd' not in dkim_fields: raise ValidateMailException("Missing DKIM domain field.\n") if dkim_fields['d'] != "gmail.com": raise ValidateMailException("Not from gmail.com\n") signed_headers = [ fld.lower().strip() for fld in dkim_fields['h'].split(":") ] if 'from' not in signed_headers: raise ValidateMailException( "From address is not included in signed headers!\n") # Some other magic stuff # NOTE: It is legal for there to be numerous Authentication-Results headers, # but, we won't handle that yet. Instead, just fail if there is more than one. auth_results = __get_required_header(headers, "Authentication-Results").strip() if not re.match(r"^mx\.google\.com;", auth_results): raise ValidateMailException( "Authentication-Results header not from mx.google.com\n") # check various features auth_results = " " + re.sub(r"\s+", " ", auth_results).lower().strip() + " " if " dkim=pass " not in auth_results: raise ValidateMailException( "Authentication-Results failure: No 'dkim=pass'\n") if " dmarc=pass " not in auth_results: raise ValidateMailException( "Authentication-Results failure: No 'dmarc=pass'\n") if " spf=pass " not in auth_results: raise ValidateMailException( "Authentication-Results failure: No 'spf=pass'\n") # Try to get the smtp.mailfrom header if not re.match( r"^.*spf=pass [^;]+? smtp.mailfrom=" + re.escape(from_addr) + "(;.*)?$", auth_results): raise ValidateMailException( "Authentication-Results failure: invalid or missing smtp.mailfrom address\n" ) return from_addr
def __validate_mail(message): # First things first (and this is critical), # validate the DKIM signature if not dkim.verify(message): raise ValidateMailException("DKIM signature verification failed\n") # Parse the email (headers, body) = dkim.rfc822_parse(message) # Get the one and only DKIM-Signature header and from address from_addr = __get_raw_email_addr(__get_required_header(headers, "From")) dkim_sig = __get_required_header(headers, "DKIM-Signature") # Check that the from address and the Return-Path address are consistent return_paths = __get_headers_by_name(headers, "Return-Path") if len(return_paths) == 0: raise ValidateMailException("No return paths specified\n") for rp in return_paths: if __get_raw_email_addr(rp) != from_addr: raise ValidateMailException("'Return-Path: " + str(rp) + "' does not match from address.\n") # Check a few things in the DKIM header dkim_fields = parse_dkim_sig(dkim_sig) if 'bh' not in dkim_fields: raise ValidateMailException("Missing the DKIM body hash.\n") if 'h' not in dkim_fields: raise ValidateMailException("Missing DKIM headers key (h=).\n") if 'd' not in dkim_fields: raise ValidateMailException("Missing DKIM domain field.\n") if dkim_fields['d'] != "gmail.com": raise ValidateMailException("Not from gmail.com\n") signed_headers = [fld.lower().strip() for fld in dkim_fields['h'].split(":")] if 'from' not in signed_headers: raise ValidateMailException("From address is not included in signed headers!\n") # Some other magic stuff # NOTE: It is legal for there to be numerous Authentication-Results headers, # but, we won't handle that yet. Instead, just fail if there is more than one. auth_results = __get_required_header(headers, "Authentication-Results").strip() if not re.match(r"^mx\.google\.com;", auth_results): raise ValidateMailException("Authentication-Results header not from mx.google.com\n") # check various features auth_results = " " + re.sub(r"\s+", " ", auth_results).lower().strip() + " " if " dkim=pass " not in auth_results: raise ValidateMailException("Authentication-Results failure: No 'dkim=pass'\n") if " dmarc=pass " not in auth_results: raise ValidateMailException("Authentication-Results failure: No 'dmarc=pass'\n") if " spf=pass " not in auth_results: raise ValidateMailException("Authentication-Results failure: No 'spf=pass'\n") # Try to get the smtp.mailfrom header if not re.match(r"^.*spf=pass [^;]+? smtp.mailfrom=" + re.escape(from_addr) + "(;.*)?$", auth_results): raise ValidateMailException("Authentication-Results failure: invalid or missing smtp.mailfrom address\n") return from_addr
def check_dmarc(msg, spf_result=None, dkim_result=None, dnsfunc=None, psddmarc=False): def check_psddmarc_list(psdname, dnsfunc=dns_query): """Check psddmarc.org list of PSD DMARC participants""" try: # If the PSD registry is locally available, use it. psdfile_name = resource_filename('authheaders', 'psddmarc.csv') psd_file = open(psdfile_name) psds = [] for line in psd_file.readlines(): sp = line.split(',') if sp[1] == 'Active': psds += sp[0] if psdname in psds: return True else: return False except: # If not, use the DNS query list. psd_list_host = '.psddmarc.org' psd_lookup = psdname + psd_list_host answer = dnsfunc(psd_lookup) if answer: return True else: return False def dmarc_per_from(from_domain, spf_result=None, dkim_result=None, dnsfunc=None, psddmarc=False): # Get dmarc record for domain if (dnsfunc): record, orgdomain = receiver_record(from_domain, dnsfunc=dnsfunc) else: record, orgdomain = receiver_record(from_domain) # Report if DMARC record is From Domain or Org Domain if record and orgdomain: result_comment = 'Used Org Domain Record' elif record: result_comment = 'Used From Domain Record' # Get psddmarc record if doing PSD DMARC, no DMARC record, and PSD is # listed psddomain = False if (not record) and psddmarc: org_domain = get_org_domain(from_domain) if (dnsfunc): if check_psddmarc_list(org_domain.split('.', 1)[-1], dnsfunc=dnsfunc): record, _ = receiver_record(org_domain.split('.', 1)[-1], dnsfunc=dnsfunc) else: if check_psddmarc_list(org_domain.split('.', 1)[-1]): record, _ = receiver_record(org_domain.split('.', 1)[-1]) if record: psddomain = True result_comment = 'Used Public Suffix Domain Record' if record and record.get('p'): # find policy policy = record['p'] if policy[-1:] == '\\': policy = policy[:-1] try: sp = record['sp'] if sp[-1:] == '\\': sp = sp[:-1] except KeyError: sp = policy try: np = record['np'] if np[-1:] == '\\': np = np[:-1] except KeyError: np = None if orgdomain or psddomain: if np: exists = False for qtype in ['a', 'mx', 'aaaa']: if (dnsfunc): res = dnsfunc(from_domain) else: res = dns_query(from_domain, qtype) if res: exists = True break if exists: policy = sp else: policy = np else: policy = sp adkim = record.get('adkim', 'r') aspf = record.get('aspf', 'r') # get result result = "fail" if spf_result and spf_result.result == "pass": # The domain in SPF results often includes the local part, even though # generally it SHOULD NOT (RFC 7601, Section 2.7.2, last paragraph). mail_from_domain = get_domain_part(spf_result.smtp_mailfrom) spf_result.smtp_mailfrom = mail_from_domain if aspf == "s" and from_domain == mail_from_domain: result = "pass" elif aspf == "r" and get_org_domain( from_domain) == get_org_domain(mail_from_domain): result = "pass" if dkim_result and dkim_result.result == "pass": if adkim == "s" and from_domain == dkim_result.header_d: result = "pass" elif adkim == "r" and get_org_domain( from_domain) == get_org_domain(dkim_result.header_d): result = "pass" else: # If no DMARC record, no result result = 'none' result_comment = '' from_domain = '' policy = '' return (result, result_comment, from_domain, policy) # get from domain headers, _ = rfc822_parse(msg) from_headers = [ a[1] for a in getaddresses(x[1].decode(errors='ignore').strip() for x in headers if x[0].lower() == b"from") ] if len(from_headers) > 1: # multi-from processing per RFC 7489 6.6.1 domain_results = [] for from_header in from_headers: from_domain = get_domain_part(from_header) domain_results.append( dmarc_per_from(from_domain, spf_result, dkim_result, dnsfunc, psddmarc)) for domain in domain_results: if domain[3] == 'reject': result, result_comment, from_domain, policy = domain return DMARCAuthenticationResult(result=result, result_comment=result_comment, header_from=from_domain, policy=policy) for domain in domain_results: if domain[3] == 'quarantine': result, result_comment, from_domain, policy = domain return DMARCAuthenticationResult(result=result, result_comment=result_comment, header_from=from_domain, policy=policy) for domain in domain_results: if domain[3] == 'none': result, result_comment, from_domain, policy = domain return DMARCAuthenticationResult(result=result, result_comment=result_comment, header_from=from_domain, policy=policy) result, result_comment, from_domain, policy = domain else: from_header = from_headers[0] from_domain = get_domain_part(from_header) result, result_comment, from_domain, policy = dmarc_per_from( from_domain, spf_result, dkim_result, dnsfunc, psddmarc) if result != 'none': return DMARCAuthenticationResult(result=result, result_comment=result_comment, header_from=from_domain, policy=policy) else: return DMARCAuthenticationResult(result=result, header_from=from_domain)