|
| 1 | +from .exceptions_types import EmailUndeliverableError |
| 2 | + |
| 3 | +import dns.resolver |
| 4 | +import dns.exception |
| 5 | + |
| 6 | + |
| 7 | +def caching_resolver(*, timeout=None, cache=None): |
| 8 | + if timeout is None: |
| 9 | + from . import DEFAULT_TIMEOUT |
| 10 | + timeout = DEFAULT_TIMEOUT |
| 11 | + resolver = dns.resolver.Resolver() |
| 12 | + resolver.cache = cache or dns.resolver.LRUCache() |
| 13 | + resolver.lifetime = timeout # timeout, in seconds |
| 14 | + return resolver |
| 15 | + |
| 16 | + |
| 17 | +def validate_email_deliverability(domain, domain_i18n, timeout=None, dns_resolver=None): |
| 18 | + # Check that the domain resolves to an MX record. If there is no MX record, |
| 19 | + # try an A or AAAA record which is a deprecated fallback for deliverability. |
| 20 | + # Raises an EmailUndeliverableError on failure. On success, returns a dict |
| 21 | + # with deliverability information. |
| 22 | + |
| 23 | + # If no dns.resolver.Resolver was given, get dnspython's default resolver. |
| 24 | + # Override the default resolver's timeout. This may affect other uses of |
| 25 | + # dnspython in this process. |
| 26 | + if dns_resolver is None: |
| 27 | + from . import DEFAULT_TIMEOUT |
| 28 | + if timeout is None: |
| 29 | + timeout = DEFAULT_TIMEOUT |
| 30 | + dns_resolver = dns.resolver.get_default_resolver() |
| 31 | + dns_resolver.lifetime = timeout |
| 32 | + |
| 33 | + deliverability_info = {} |
| 34 | + |
| 35 | + def dns_resolver_resolve_shim(domain, record): |
| 36 | + try: |
| 37 | + # dns.resolver.Resolver.resolve is new to dnspython 2.x. |
| 38 | + # https://dnspython.readthedocs.io/en/latest/resolver-class.html#dns.resolver.Resolver.resolve |
| 39 | + return dns_resolver.resolve(domain, record) |
| 40 | + except AttributeError: |
| 41 | + # dnspython 2.x is only available in Python 3.6 and later. For earlier versions |
| 42 | + # of Python, we maintain compatibility with dnspython 1.x which has a |
| 43 | + # dnspython.resolver.Resolver.query method instead. The only difference is that |
| 44 | + # query may treat the domain as relative and use the system's search domains, |
| 45 | + # which we prevent by adding a "." to the domain name to make it absolute. |
| 46 | + # dns.resolver.Resolver.query is deprecated in dnspython version 2.x. |
| 47 | + # https://dnspython.readthedocs.io/en/latest/resolver-class.html#dns.resolver.Resolver.query |
| 48 | + return dns_resolver.query(domain + ".", record) |
| 49 | + |
| 50 | + try: |
| 51 | + # We need a way to check how timeouts are handled in the tests. So we |
| 52 | + # have a secret variable that if set makes this method always test the |
| 53 | + # handling of a timeout. |
| 54 | + if getattr(validate_email_deliverability, 'TEST_CHECK_TIMEOUT', False): |
| 55 | + raise dns.exception.Timeout() |
| 56 | + |
| 57 | + try: |
| 58 | + # Try resolving for MX records. |
| 59 | + response = dns_resolver_resolve_shim(domain, "MX") |
| 60 | + |
| 61 | + # For reporting, put them in priority order and remove the trailing dot in the qnames. |
| 62 | + mtas = sorted([(r.preference, str(r.exchange).rstrip('.')) for r in response]) |
| 63 | + |
| 64 | + # Remove "null MX" records from the list (their value is (0, ".") but we've stripped |
| 65 | + # trailing dots, so the 'exchange' is just ""). If there was only a null MX record, |
| 66 | + # email is not deliverable. |
| 67 | + mtas = [(preference, exchange) for preference, exchange in mtas |
| 68 | + if exchange != ""] |
| 69 | + if len(mtas) == 0: |
| 70 | + raise EmailUndeliverableError("The domain name %s does not accept email." % domain_i18n) |
| 71 | + |
| 72 | + deliverability_info["mx"] = mtas |
| 73 | + deliverability_info["mx_fallback_type"] = None |
| 74 | + |
| 75 | + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): |
| 76 | + |
| 77 | + # If there was no MX record, fall back to an A record, as SMTP servers do. |
| 78 | + try: |
| 79 | + response = dns_resolver_resolve_shim(domain, "A") |
| 80 | + deliverability_info["mx"] = [(0, str(r)) for r in response] |
| 81 | + deliverability_info["mx_fallback_type"] = "A" |
| 82 | + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): |
| 83 | + |
| 84 | + # If there was no A record, fall back to an AAAA record. |
| 85 | + try: |
| 86 | + response = dns_resolver_resolve_shim(domain, "AAAA") |
| 87 | + deliverability_info["mx"] = [(0, str(r)) for r in response] |
| 88 | + deliverability_info["mx_fallback_type"] = "AAAA" |
| 89 | + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): |
| 90 | + |
| 91 | + # If there was no MX, A, or AAAA record, then mail to |
| 92 | + # this domain is not deliverable. |
| 93 | + raise EmailUndeliverableError("The domain name %s does not exist." % domain_i18n) |
| 94 | + |
| 95 | + # Check for a SPF reject-all record ("v=spf1 -all") which indicates |
| 96 | + # no emails are sent from this domain (similar to a NULL MX record |
| 97 | + # but for sending rather than receiving). In combination with the |
| 98 | + # absence of an MX record, this is probably a good sign that the |
| 99 | + # domain is not used for email. |
| 100 | + try: |
| 101 | + response = dns_resolver_resolve_shim(domain, "TXT") |
| 102 | + for rec in response: |
| 103 | + value = b"".join(rec.strings) |
| 104 | + if value.startswith(b"v=spf1 "): |
| 105 | + deliverability_info["spf"] = value.decode("ascii", errors='replace') |
| 106 | + if value == b"v=spf1 -all": |
| 107 | + raise EmailUndeliverableError("The domain name %s does not send email." % domain_i18n) |
| 108 | + except dns.resolver.NoAnswer: |
| 109 | + # No TXT records means there is no SPF policy, so we cannot take any action. |
| 110 | + pass |
| 111 | + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN): |
| 112 | + # Failure to resolve at this step will be ignored. |
| 113 | + pass |
| 114 | + |
| 115 | + except dns.exception.Timeout: |
| 116 | + # A timeout could occur for various reasons, so don't treat it as a failure. |
| 117 | + return { |
| 118 | + "unknown-deliverability": "timeout", |
| 119 | + } |
| 120 | + |
| 121 | + except EmailUndeliverableError: |
| 122 | + # Don't let these get clobbered by the wider except block below. |
| 123 | + raise |
| 124 | + |
| 125 | + except Exception as e: |
| 126 | + # Unhandled conditions should not propagate. |
| 127 | + raise EmailUndeliverableError( |
| 128 | + "There was an error while checking if the domain name in the email address is deliverable: " + str(e) |
| 129 | + ) |
| 130 | + |
| 131 | + return deliverability_info |
0 commit comments