def fetch_addrs(self, resolver, name, arecs, a4recs): aco = resolver.aquery(name, "A", raise_on_no_answer=False) a4co = resolver.aquery(name, "AAAA", raise_on_no_answer=False) co = asyncio.gather(aco, a4co) (aans, a4ans) = yield from co if aans.rrset != None: for rdata in aans: arecs.append(rdata.address) if a4ans.rrset != None: for rdata in a4ans: a4recs.append(rdata.address)
def fetch_addrs(resolver, name, arecs, a4recs, lim): if lim and lim[0] == 0: raise TooManyQueries lim[0] = lim[0] - 1 cos = [] if arecs: cos.append(resolver.aquery(name, "A", raise_on_no_answer=False)) if a4recs: cos.append(resolver.aquery(name, "AAAA", raise_on_no_answer=False)) results = yield from asyncio.gather(*cos) i = 0 if arecs: aans = results[0] i = i + 1 if aans.rrset != None: for rdata in aans: arecs.append(rdata.address) if a4recs: a4ans = results[i] if a4ans.rrset != None: for rdata in a4ans: a4recs.append(rdata.address)
def fetch_addrs(resolver, name, arecs, a4recs, lim): if lim and lim[0] == 0: raise TooManyQueries lim[0] = lim[0] - 1 cos = [] if arecs: cos.append(resolver.aquery(name, "A", raise_on_no_answer=False)) if a4recs: cos.append(resolver.aquery(name, "AAAA", raise_on_no_answer=False)) results = yield from asyncio.gather(*cos) i = 0 if arecs: aans = results[0] i = i + 1 if aans.rrset != None: for rdata in aans: arecs.append(rdata.address) if a4recs: a4ans = results[i] if a4ans.rrset != None: for rdata in a4ans: a4recs.append(rdata.address)
def get_exchanger_list(domain, resolver, family=socket.AF_UNSPEC, lim=None, implicit=True): resolver = dns.resolver.Resolver() resolver.use_edns(0, 0, 1410) mxs = {} answer = None addressable = False arecs = None a4recs = None if family == socket.AF_UNSPEC or family == socket.AF_INET: arecs = [] if family == socket.AF_UNSPEC or family == socket.AF_INET6: a4recs = [] if lim: if lim[0] == 0: raise TooManyQueries else: lim[0] = lim[0] - 1 try: answer = yield from resolver.aquery(domain, "MX") except dns.resolver.NoAnswer: if not implicit: return None # No answer means there's no MX record, so look for an A or # AAAA record. yield from fetch_addrs(resolver, domain, arecs, a4recs, lim) if ((arecs and len(arecs) > 0) or a4recs and len(a4recs) > 0): mxs = { 0: [ { "exchange" : domain, "a": arecs, "aaaa": a4recs } ] } addressable = True except (NXDOMAIN, TooManyQueries): raise except: return None else: for mx in answer: if mx.rdtype == dns.rdatatype.MX: # If exchange addresses were included in the additional # section, use those. # XXX for SPF, relying on the additional section may be a mistake: # what if it includes some, but not all, relevant data? for rrset in answer.response.additional: if rrset.name == mx.exchange: if rrset.rdtype == dns.rdatatype.A and arecs != None: for rdata in rrset: arecs.append(rdata.address) addressable = True elif rrset.rdtype == dns.rdatatype.AAAA and a4recs != None: for rdata in rrset: a4recs.append(rdata.address) addressable = True # Otherwise, fetch A and/or AAAA records for exchange if not addressable: yield from fetch_addrs(resolver, mx.exchange, arecs, a4recs, lim) if ((arecs and len(arecs) > 0) or a4recs and len(a4recs) > 0): entry = { "exchange": mx.exchange, "a": arecs, "aaaa": a4recs} if mx.preference in mxs: mxs[mx.preference].append(entry) else: mxs[mx.preference] = [entry] addressable = True # If we didn't get a single server IP address either out of the # MX query chain or the A/AAAA query on the name if there was no # MX, then we can't deliver to this address. if not addressable: return None return mxs
def get_exchanger_list(self): resolver = dns.resolver.Resolver() resolver.use_edns(0, 0, 1410) mxs = {} answer = None addressable = False try: answer = yield from resolver.aquery(domain, "MX") except dns.resolver.NoAnswer: # No answer means there's no MX record, so look for an A or # AAAA record. arecs = [] a4recs = [] yield from self.fetch_addrs(resolver, domain, arecs, a4recs) if len(arecs) > 0 or len(a4recs) > 0: mxs = { 0: [ { "exchange" : domain, "a": arecs, "aaaa": a4recs } ] } addressable = True except NXDOMAIN: self.push("550 no such domain.") syslog.syslog(syslog.LOG_INFO, "550 no such domain: %s" % domain) print("550 no such domain: %s" % domain) return False except: return True else: for mx in answer: if mx.rdtype == dns.rdatatype.MX: arecs = [] a4recs = [] # If exchange addresses were included in the additional # section, use those. for rrset in answer.response.additional: if rrset.name == mx.exchange: if rrset.rdtype == dns.rdatatype.A: for rdata in rrset: arecs.append(rdata.address) elif rrset.rdtype == dns.rdatatype.AAAA: for rdata in rrset: a4recs.append(rdata.address) # Otherwise, fetch A and/or AAAA records for exchange if len(arecs) == 0 and len(a4recs) == 0: yield from self.fetch_addrs(resolver, mx.exchange, arecs, a4recs) if len(arecs) > 0 or len(a4recs) > 0: entry = { "exchange": mx.exchange, "a": arecs, "aaaa": a4recs} if mx.preference in mxs: mxs[mx.preference].append(entry) else: mxs[mx.preference] = [entry] addressable = True # If we didn't get a single server IP address either out of the # MX query chain or the A/AAAA query on the name if there was no # MX, then we can't deliver to this address. if not addressable: raise smtp.PermanentFailure(code=550, data=["no exchanger/address for domain"]) return mxs
def get_exchanger_list(domain, resolver, family=socket.AF_UNSPEC, lim=None, implicit=True): resolver = dns.resolver.Resolver() resolver.use_edns(0, 0, 1410) mxs = {} answer = None addressable = False arecs = None a4recs = None if family == socket.AF_UNSPEC or family == socket.AF_INET: arecs = [] if family == socket.AF_UNSPEC or family == socket.AF_INET6: a4recs = [] if lim: if lim[0] == 0: raise TooManyQueries else: lim[0] = lim[0] - 1 try: answer = yield from resolver.aquery(domain, "MX") except dns.resolver.NoAnswer: if not implicit: return None # No answer means there's no MX record, so look for an A or # AAAA record. yield from fetch_addrs(resolver, domain, arecs, a4recs, lim) if ((arecs and len(arecs) > 0) or a4recs and len(a4recs) > 0): mxs = {0: [{"exchange": domain, "a": arecs, "aaaa": a4recs}]} addressable = True except (NXDOMAIN, TooManyQueries): raise except: return None else: for mx in answer: if mx.rdtype == dns.rdatatype.MX: # If exchange addresses were included in the additional # section, use those. # XXX for SPF, relying on the additional section may be a mistake: # what if it includes some, but not all, relevant data? for rrset in answer.response.additional: if rrset.name == mx.exchange: if rrset.rdtype == dns.rdatatype.A and arecs != None: for rdata in rrset: arecs.append(rdata.address) addressable = True elif rrset.rdtype == dns.rdatatype.AAAA and a4recs != None: for rdata in rrset: a4recs.append(rdata.address) addressable = True # Otherwise, fetch A and/or AAAA records for exchange if not addressable: yield from fetch_addrs(resolver, mx.exchange, arecs, a4recs, lim) if ((arecs and len(arecs) > 0) or a4recs and len(a4recs) > 0): entry = { "exchange": mx.exchange, "a": arecs, "aaaa": a4recs } if mx.preference in mxs: mxs[mx.preference].append(entry) else: mxs[mx.preference] = [entry] addressable = True # If we didn't get a single server IP address either out of the # MX query chain or the A/AAAA query on the name if there was no # MX, then we can't deliver to this address. if not addressable: return None return mxs
def check_host_worker(ipaddr, domain, sender, debug=False): limiter[0] = limiter[0] + 1 if limiter[0] > 10: if debug: print("DNS query limiter stopped search.") return "permerror" # RFC 7208 section 4.3: check for invalid domain name try: name = dns.name.from_text(domain) except: if debug: print("failure:", domain, " is not a valid domain name") return "permerror" # RFC 7208 section 4.3: check for existence of local-part # We are assuming that the caller has produced some # vaguely valid sender, either from the MAIL FROM: # or HELO/EHLO. For validating From: headers, # combining the source IP address and sender isn't # sufficient to say that the use isn't permitted, but # it is sufficient to say that the use _is_ permitted. if '@' not in sender: try: sdo = dns.name.from_text(sender) except: if debug: print("failure:", sender, " is not a valid domain name") return "permerror" sender = "postmaster@" + sender else: parts = sender.split("@") if len(parts) != 2: return "permerror" if parts[0] == '': sender = "postmaster@" + parts[1] try: sdo = dns.name.from_text(parts[1]) except: if debug: print("failure:", parts[1], " is not a valid domain name") return "permerror" spfs = [] try: spfRecord = yield from resolver.aquery(domain, "TXT", raise_on_no_answer=True) except dns.resolver.NoAnswer: if debug: print(domain, "IN TXT: No Answer") return None except Exception as x: if debug: print(domain, "IN TXT:", str(x)) return "temperror" else: for rr in spfRecord: text = "".join(rr.strings) if text.startswith("v=spf1 "): spfs.append(text[7:]) if len(spfs) == 0: if debug: print(domain, "IN TXT: No valid SPF record") return None if len(spfs) > 1: if debug: print("More than one SPF record found.") return "permerror" # Terms are separated by exactly one space. # terms = *( 1*SP ( directive / modifier ) ) if debug: print(spfs[0]) terms = spfs[0].split(" ") redirect = None modifiers = {} directives = [] for term in terms: if len(term) == 0: if debug: print("syntax error: zero-length term") return "permerror" # Eliminate the possibility that this is a modifier first, # because they are easy to detect. elif "=" in term: sides = term.split("=") if len(sides) != 2: if debug: print("bogus modifier") return "permerror" # modifiers can only appear once. if sides[0] in modifiers: if debug: print("Duplicate modifier:", sides[0]) return "permerror" exp = False if sides[0] == "exp": exp = True expansion = macro_expand(sides[1], ipaddr, domain, sender, exp) if expansion == None: return "permerror" modifiers[sides[0]] = sides[1] else: # By default, then, this is a directive. directive = Directive(check_host_worker, resolver, debug) if directive.validate(term, ipaddr, domain, sender) == "permerror": return "permerror" directives.append(directive) for directive in directives: status = yield from directive.process(ipaddr, domain, sender) # If this directive matched, don't process any later directives. if directive.matched: if debug: print("Matched:", directive.name) return status # If this directive returned a definite answer, return that answer # (e.g., tempfail, etc.) if status != None and status != "neutral": if debug: print("Status:", status) return status # Since we survived the directives, try the modifiers. for modifier in modifiers: if modifier == "exp": # We could really care less. pass elif modifier == "redirect": if debug: print("Redirect:", modifiers[modifier]) status = yield from check_host_worker(ipaddr, modifiers[modifier], sender, debug) if debug: print("Status:", status) return status return "neutral"
def get_connection(self, user, domain): # We're already connected. Just return the connection. if domain in self.connections: connection = self.connections[domain] if self.connections[domain] not in self.connection_list: status = yield from self.send_rcptto(connection, user + "@" + domain) if status: self.connections[user + "@" + domain] = connection self.connection_list.append(connection) else: print("bad status after send_rcptto.") return status return True resolver = dns.resolver.Resolver() resolver.use_edns(0, 0, 1410) mxs = {} answer = None addressable = False try: answer = yield from resolver.aquery(domain, "MX") except dns.resolver.NoAnswer: # No answer means there's no MX record, so look for an A or # AAAA record. arecs = [] a4recs = [] yield from self.fetch_addrs(resolver, domain, arecs, a4recs) if len(arecs) > 0 or len(a4recs) > 0: mxs = {0: [{"exchange": domain, "a": arecs, "aaaa": a4recs}]} addressable = True except NXDOMAIN: self.push("550 no such domain.") syslog.syslog(syslog.LOG_INFO, "550 no such domain: %s" % domain) print("550 no such domain: %s" % domain) return False except: # Temporary failure; we just have to stash the message for this # address. self.connections[user + "@" + domain] = None self.connections[domain] = None return True else: for mx in answer: if mx.rdtype == dns.rdatatype.MX: arecs = [] a4recs = [] # If exchange addresses were included in the additional # section, use those. for rrset in answer.response.additional: if rrset.name == mx.exchange: if rrset.rdtype == dns.rdatatype.A: for rdata in rrset: arecs.append(rdata.address) elif rrset.rdtype == dns.rdatatype.AAAA: for rdata in rrset: a4recs.append(rdata.address) # Otherwise, fetch A and/or AAAA records for exchange if len(arecs) == 0 and len(a4recs) == 0: yield from self.fetch_addrs(resolver, mx.exchange, arecs, a4recs) if len(arecs) > 0 or len(a4recs) > 0: entry = { "exchange": mx.exchange, "a": arecs, "aaaa": a4recs } if mx.preference in mxs: mxs[mx.preference].append(entry) else: mxs[mx.preference] = [entry] addressable = True # If we didn't get a single server IP address either out of the # MX query chain or the A/AAAA query on the name if there was no # MX, then we can't deliver to this address. if not addressable: self.push("550 no exchanger or addresses for domain.") syslog.syslog(syslog.LOG_INFO, "550 no exchanger or addresses for: %s" % domain) print("550 no exchanger or addresses for: %s" % domain) return False # Our task now is to get a connection to the most preferable # Mail Exchanger (MX) we can reach. # Make a list of all the addresses to try, in order of preference. # We prefer IPv6 for the first attempt, but interleave IPv6 and # IPv4 addresses in case one transport is working and the other # is not. The interleaving is per-exchange, so we always try # exchanges in order of preference and, among exchanges with the # same preference, one exchange at a time. addrs = [] preferences = list(mxs.keys()) preferences.sort() # Iterate across preference levels for pref in preferences: exchanges = mxs[pref] # Iterate across exchanges at a given preference level for exchange in exchanges: arecs = exchange['a'] qrecs = exchange['aaaa'] name = exchange['exchange'] # Interleave the IPv6 and IPv4 addresses for this exchange. lim = max(len(arecs), len(qrecs)) for i in range(0, lim): if i < len(qrecs): addrs.append((qrecs[i], socket.AF_INET6, name)) if i < len(arecs): addrs.append((arecs[i], socket.AF_INET, name)) # Time is of the essence here, because the mail user agent is # waiting, and we want to give the user quick feedback, but we # also want to follow the rules and not boost our spam score # by delivering to a low-preference MX, so we allow about five # seconds to complete a connection rather than the usual 90 # seconds. We start connecting every five seconds, and take # the first connection that completes, dropping the others. # It should be rare that a connection takes longer than five # seconds to complete if the exchange is reachable. connection = yield from self.connect_to_addresses(addrs, 5) if connection != None: status = yield from self.send_rcptto(connection, user + "@" + domain) if status: self.connections[user + "@" + domain] = connection self.connections[domain] = connection self.connection_list.append(connection) else: print("horked in send_rcptto") return status print("no connection returned.") return False
def check_domain(domain): try: result = yield from resolver.aquery(domain, "txt", raise_on_no_answer=True) except: return None return domain
def check_host_worker(ipaddr, domain, sender, debug=False): limiter[0] = limiter[0] + 1 if limiter[0] > 10: if debug: print("DNS query limiter stopped search.") return "permerror" # RFC 7208 section 4.3: check for invalid domain name try: name = dns.name.from_text(domain) except: if debug: print("failure:", domain, " is not a valid domain name") return "permerror" # RFC 7208 section 4.3: check for existence of local-part # We are assuming that the caller has produced some # vaguely valid sender, either from the MAIL FROM: # or HELO/EHLO. For validating From: headers, # combining the source IP address and sender isn't # sufficient to say that the use isn't permitted, but # it is sufficient to say that the use _is_ permitted. if '@' not in sender: try: sdo = dns.name.from_text(sender) except: if debug: print("failure:", sender, " is not a valid domain name") return "permerror" sender = "postmaster@" + sender else: parts = sender.split("@") if len(parts) != 2: return "permerror" if parts[0] == '': sender = "postmaster@" + parts[1] try: sdo = dns.name.from_text(parts[1]) except: if debug: print("failure:", parts[1], " is not a valid domain name") return "permerror" spfs = [] try: spfRecord = yield from resolver.aquery(domain, "TXT", raise_on_no_answer=True) except dns.resolver.NoAnswer: if debug: print(domain, "IN TXT: No Answer") return None except Exception as x: if debug: print(domain, "IN TXT:", str(x)) return "temperror" else: for rr in spfRecord: text = "".join(rr.strings) if text.startswith("v=spf1 "): spfs.append(text[7:]) if len(spfs) == 0: if debug: print(domain, "IN TXT: No valid SPF record") return None if len(spfs) > 1: if debug: print("More than one SPF record found.") return "permerror" # Terms are separated by exactly one space. # terms = *( 1*SP ( directive / modifier ) ) if debug: print(spfs[0]) terms = spfs[0].split(" ") redirect = None modifiers = {} directives = [] for term in terms: if len(term) == 0: if debug: print("syntax error: zero-length term") return "permerror" # Eliminate the possibility that this is a modifier first, # because they are easy to detect. elif "=" in term: sides = term.split("=") if len(sides) != 2: if debug: print("bogus modifier") return "permerror" # modifiers can only appear once. if sides[0] in modifiers: if debug: print("Duplicate modifier:", sides[0]) return "permerror" exp = False if sides[0] == "exp": exp = True expansion = macro_expand(sides[1], ipaddr, domain, sender, exp) if expansion == None: return "permerror" modifiers[sides[0]] = sides[1] else: # By default, then, this is a directive. directive = Directive(check_host_worker, resolver, debug) if directive.validate(term, ipaddr, domain, sender) == "permerror": return "permerror" directives.append(directive) for directive in directives: status = yield from directive.process(ipaddr, domain, sender) # If this directive matched, don't process any later directives. if directive.matched: if debug: print("Matched:", directive.name) return status # If this directive returned a definite answer, return that answer # (e.g., tempfail, etc.) if status != None and status != "neutral": if debug: print("Status:", status) return status # Since we survived the directives, try the modifiers. for modifier in modifiers: if modifier == "exp": # We could really care less. pass elif modifier == "redirect": if debug: print("Redirect:", modifiers[modifier]) status = yield from check_host_worker(ipaddr, modifiers[modifier], sender, debug) if debug: print("Status:", status) return status return "neutral"