Skip to main content
Glama
jezweb

Australian Postcodes MCP Server

deliverability.py7.22 kB
from typing import Any, List, Optional, Tuple, TypedDict import ipaddress from .exceptions 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

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jezweb/australian-postcodes-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server