aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/email_validator/deliverability.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/email_validator/deliverability.py')
-rw-r--r--.venv/lib/python3.12/site-packages/email_validator/deliverability.py159
1 files changed, 159 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/email_validator/deliverability.py b/.venv/lib/python3.12/site-packages/email_validator/deliverability.py
new file mode 100644
index 00000000..90f5f9af
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/email_validator/deliverability.py
@@ -0,0 +1,159 @@
+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