Skip to content

Commit 68fb9f2

Browse files
committed
Handle NoNameservers and NXDOMAIN exceptions better
I am not sure what NoNameservers means, so I think it might be that no local nameservers could respond. Local error conditions should not fail deliverability (same as a timeout). NXDOMAIN means no records are present for a domain, so a NXDOMAIN after the MX query can skip straight to rejecting the domain, rather than going to the A/AAAA fallback. Consequently, the error for a missing fallback can be changed from "does not exist" (which is now handled by the NXDOMAIN except block) to "does not accept email."
1 parent 0f0b4a4 commit 68fb9f2

File tree

3 files changed

+26
-23
lines changed

3 files changed

+26
-23
lines changed

email_validator/deliverability.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,31 +45,33 @@ def validate_email_deliverability(domain, domain_i18n, timeout=None, dns_resolve
4545
# email is not deliverable.
4646
mtas = [(preference, exchange) for preference, exchange in mtas
4747
if exchange != ""]
48-
if len(mtas) == 0:
48+
if len(mtas) == 0: # null MX only, if there were no MX records originally a NoAnswer exception would have occurred
4949
raise EmailUndeliverableError("The domain name %s does not accept email." % domain_i18n)
5050

5151
deliverability_info["mx"] = mtas
5252
deliverability_info["mx_fallback_type"] = None
5353

54-
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
55-
54+
except dns.resolver.NoAnswer:
5655
# If there was no MX record, fall back to an A record, as SMTP servers do.
5756
try:
5857
response = dns_resolver.resolve(domain, "A")
5958
deliverability_info["mx"] = [(0, str(r)) for r in response]
6059
deliverability_info["mx_fallback_type"] = "A"
61-
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
60+
61+
except dns.resolver.NoAnswer:
6262

6363
# If there was no A record, fall back to an AAAA record.
6464
try:
6565
response = dns_resolver.resolve(domain, "AAAA")
6666
deliverability_info["mx"] = [(0, str(r)) for r in response]
6767
deliverability_info["mx_fallback_type"] = "AAAA"
68-
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
6968

69+
except dns.resolver.NoAnswer:
7070
# If there was no MX, A, or AAAA record, then mail to
71-
# this domain is not deliverable.
72-
raise EmailUndeliverableError("The domain name %s does not exist." % domain_i18n)
71+
# this domain is not deliverable, although the domain
72+
# name has other records (otherwise NXDOMAIN would
73+
# have been raised).
74+
raise EmailUndeliverableError("The domain name %s does not accept email." % domain_i18n)
7375

7476
# Check for a SPF reject-all record ("v=spf1 -all") which indicates
7577
# no emails are sent from this domain (similar to a NULL MX record
@@ -87,9 +89,18 @@ def validate_email_deliverability(domain, domain_i18n, timeout=None, dns_resolve
8789
except dns.resolver.NoAnswer:
8890
# No TXT records means there is no SPF policy, so we cannot take any action.
8991
pass
90-
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN):
91-
# Failure to resolve at this step will be ignored.
92-
pass
92+
93+
except dns.resolver.NXDOMAIN:
94+
# The domain name does not exist --- there are no records of any sort
95+
# for the domain name.
96+
raise EmailUndeliverableError("The domain name %s does not exist." % domain_i18n)
97+
98+
except dns.resolver.NoNameservers:
99+
# All nameservers failed to answer the query. This might be a problem
100+
# with local nameservers, maybe? We'll allow the domain to go through.
101+
return {
102+
"unknown-deliverability": "no_nameservers",
103+
}
93104

94105
except dns.exception.Timeout:
95106
# A timeout could occur for various reasons, so don't treat it as a failure.

tests/mocked-dns-answers.json

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
{
1717
"query": {
1818
"name": "xkxufoekjvjfjeodlfmdfjcu.com",
19-
"type": "ANY",
19+
"type": "MX",
2020
"class": "IN"
2121
},
2222
"answer": []
2323
},
2424
{
2525
"query": {
2626
"name": "xkxufoekjvjfjeodlfmdfjcu.com",
27-
"type": "AAAA",
27+
"type": "ANY",
2828
"class": "IN"
2929
},
3030
"answer": []
@@ -84,26 +84,18 @@
8484
},
8585
"answer": []
8686
},
87-
{
88-
"query": {
89-
"name": "mail.example",
90-
"type": "AAAA",
91-
"class": "IN"
92-
},
93-
"answer": []
94-
},
9587
{
9688
"query": {
9789
"name": "mail.example.com",
98-
"type": "ANY",
90+
"type": "MX",
9991
"class": "IN"
10092
},
10193
"answer": []
10294
},
10395
{
10496
"query": {
10597
"name": "mail.example.com",
106-
"type": "AAAA",
98+
"type": "ANY",
10799
"class": "IN"
108100
},
109101
"answer": []

tests/mocked_dns_response.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def get(self, key):
101101
# Query the data for a response to an ANY query.
102102
ANY = dns.rdatatype.from_text("ANY")
103103
if (key[0], ANY, key[2]) in self.data and self.data[(key[0], ANY, key[2])] is None:
104-
raise dns.resolver.NoAnswer()
104+
raise dns.resolver.NXDOMAIN()
105105

106106
raise ValueError("Saved DNS data did not contain query: {}".format(key))
107107

0 commit comments

Comments
 (0)