from typing import Optional, Any, Dict from .exceptions_types import EmailUndeliverableError import dns.resolver import dns.exception def caching_resolver(*, timeout: Optional[int] = None, cache=None, dns_resolver=None): if timeout is None: from . import DEFAULT_TIMEOUT timeout = DEFAULT_TIMEOUT resolver = dns_resolver or dns.resolver.Resolver() resolver.cache = cache or dns.resolver.LRUCache() # type: ignore resolver.lifetime = timeout # type: ignore # timeout, in seconds return resolver def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Optional[int] = None, dns_resolver=None): # Check that the domain resolves to an MX record. If there is no MX record, # try an A or AAAA record which is a deprecated fallback for deliverability. # Raises an EmailUndeliverableError on failure. On success, returns a dict # with deliverability information. # If no dns.resolver.Resolver was given, get dnspython's default resolver. # Override the default resolver's timeout. This may affect other uses of # dnspython in this process. if dns_resolver is None: from . import DEFAULT_TIMEOUT if timeout is None: timeout = DEFAULT_TIMEOUT dns_resolver = dns.resolver.get_default_resolver() dns_resolver.lifetime = timeout elif timeout is not None: raise ValueError("It's not valid to pass both timeout and dns_resolver.") deliverability_info: Dict[str, Any] = {} try: try: # Try resolving for MX records (RFC 5321 Section 5). response = dns_resolver.resolve(domain, "MX") # For reporting, put them in priority order and remove the trailing dot in the qnames. mtas = sorted([(r.preference, str(r.exchange).rstrip('.')) for r in response]) # RFC 7505: Null MX (0, ".") records signify the domain does not accept email. # Remove null MX records from the mtas list (but we've stripped trailing dots, # so the 'exchange' is just "") so we can check if there are no non-null MX # records remaining. mtas = [(preference, exchange) for preference, exchange in mtas if exchange != ""] if len(mtas) == 0: # null MX only, if there were no MX records originally a NoAnswer exception would have occurred raise EmailUndeliverableError(f"The domain name {domain_i18n} does not accept email.") deliverability_info["mx"] = mtas deliverability_info["mx_fallback_type"] = None except dns.resolver.NoAnswer: # If there was no MX record, fall back to an A record. (RFC 5321 Section 5) try: response = dns_resolver.resolve(domain, "A") deliverability_info["mx"] = [(0, str(r)) for r in response] deliverability_info["mx_fallback_type"] = "A" except dns.resolver.NoAnswer: # If there was no A record, fall back to an AAAA record. # (It's unclear if SMTP servers actually do this.) try: response = dns_resolver.resolve(domain, "AAAA") deliverability_info["mx"] = [(0, str(r)) for r in response] deliverability_info["mx_fallback_type"] = "AAAA" except dns.resolver.NoAnswer as e: # If there was no MX, A, or AAAA record, then mail to # this domain is not deliverable, although the domain # name has other records (otherwise NXDOMAIN would # have been raised). raise EmailUndeliverableError(f"The domain name {domain_i18n} does not accept email.") from e # Check for a SPF (RFC 7208) reject-all record ("v=spf1 -all") which indicates # no emails are sent from this domain (similar to a Null MX record # but for sending rather than receiving). In combination with the # absence of an MX record, this is probably a good sign that the # domain is not used for email. try: response = dns_resolver.resolve(domain, "TXT") for rec in response: value = b"".join(rec.strings) if value.startswith(b"v=spf1 "): deliverability_info["spf"] = value.decode("ascii", errors='replace') if value == b"v=spf1 -all": raise EmailUndeliverableError(f"The domain name {domain_i18n} does not send email.") except dns.resolver.NoAnswer: # No TXT records means there is no SPF policy, so we cannot take any action. pass except dns.resolver.NXDOMAIN as e: # The domain name does not exist --- there are no records of any sort # for the domain name. raise EmailUndeliverableError(f"The domain name {domain_i18n} does not exist.") from e except dns.resolver.NoNameservers: # All nameservers failed to answer the query. This might be a problem # with local nameservers, maybe? We'll allow the domain to go through. return { "unknown-deliverability": "no_nameservers", } except dns.exception.Timeout: # A timeout could occur for various reasons, so don't treat it as a failure. return { "unknown-deliverability": "timeout", } except EmailUndeliverableError: # Don't let these get clobbered by the wider except block below. raise except Exception as e: # Unhandled conditions should not propagate. raise EmailUndeliverableError( "There was an error while checking if the domain name in the email address is deliverable: " + str(e) ) from e return deliverability_info