123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- from typing import Any, List, Optional, Tuple, TypedDict
- import ipaddress
- from .exceptions_types import EmailUndeliverableError
- import dns.resolver
- import dns.exception
- def caching_resolver(*, timeout: Optional[int] = None, cache: Any = None, dns_resolver: Optional[dns.resolver.Resolver] = None) -> dns.resolver.Resolver:
- 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()
- resolver.lifetime = timeout # timeout, in seconds
- return resolver
- DeliverabilityInfo = TypedDict("DeliverabilityInfo", {
- "mx": List[Tuple[int, str]],
- "mx_fallback_type": Optional[str],
- "unknown-deliverability": str,
- }, total=False)
- def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Optional[int] = None, dns_resolver: Optional[dns.resolver.Resolver] = None) -> DeliverabilityInfo:
- # 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: DeliverabilityInfo = {}
- 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 or AAA record
- # (RFC 5321 Section 5). Check A first since it's more common.
- # If the A/AAAA response has no Globally Reachable IP address,
- # treat the response as if it were NoAnswer, i.e., the following
- # address types are not allowed fallbacks: Private-Use, Loopback,
- # Link-Local, and some other obscure ranges. See
- # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
- # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
- # (Issue #134.)
- def is_global_addr(address: Any) -> bool:
- try:
- ipaddr = ipaddress.ip_address(address)
- except ValueError:
- return False
- return ipaddr.is_global
- try:
- response = dns_resolver.resolve(domain, "A")
- if not any(is_global_addr(r.address) for r in response):
- raise dns.resolver.NoAnswer # fall back to AAAA
- deliverability_info["mx"] = [(0, domain)]
- 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")
- if not any(is_global_addr(r.address) for r in response):
- raise dns.resolver.NoAnswer
- deliverability_info["mx"] = [(0, domain)]
- 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 "):
- 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
|