|
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 |
|
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: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
response = dns_resolver.resolve(domain, "MX") |
|
|
|
|
|
mtas = sorted([(r.preference, str(r.exchange).rstrip('.')) for r in response]) |
|
|
|
|
|
|
|
|
|
|
|
mtas = [(preference, exchange) for preference, exchange in mtas |
|
if exchange != ""] |
|
if len(mtas) == 0: |
|
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: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
deliverability_info["mx"] = [(0, domain)] |
|
deliverability_info["mx_fallback_type"] = "A" |
|
|
|
except dns.resolver.NoAnswer: |
|
|
|
|
|
|
|
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: |
|
|
|
|
|
|
|
|
|
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not accept email.") from e |
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
pass |
|
|
|
except dns.resolver.NXDOMAIN as e: |
|
|
|
|
|
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not exist.") from e |
|
|
|
except dns.resolver.NoNameservers: |
|
|
|
|
|
return { |
|
"unknown-deliverability": "no_nameservers", |
|
} |
|
|
|
except dns.exception.Timeout: |
|
|
|
return { |
|
"unknown-deliverability": "timeout", |
|
} |
|
|
|
except EmailUndeliverableError: |
|
|
|
raise |
|
|
|
except Exception as e: |
|
|
|
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 |
|
|