Skip to content

Commit 2f11b62

Browse files
committed
Limit the SPF reject-all check to domains without MX records that have fallback A/AAAA records
Fixes #90.
1 parent d7fd074 commit 2f11b62

File tree

4 files changed

+32
-27
lines changed

4 files changed

+32
-27
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
In Development
2+
--------------
3+
4+
* The new SPF reject-all record check is now limited to domains that do not have MX records but do have an A/AAAA record fallback.
5+
16
Version 1.3.0 (September 18, 2022)
27
----------------------------------
38

README.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,7 @@ later in the document about that.)
109109

110110
The validator checks that the domain name in the email address has a
111111
DNS MX record (except a NULL MX record) indicating that it can receive
112-
email and that it does not have a reject-all SPF record (`v=spf1 -all`)
113-
which would indicate that it cannot send email.
112+
email (or a fallback A-record, see below).
114113
There is nothing to be gained by trying to actually contact an SMTP
115114
server, so that's not done here. For privacy, security, and practicality
116115
reasons servers are good at not giving away whether an address is
@@ -128,7 +127,7 @@ The `validate_email` function also accepts the following keyword arguments
128127
require the
129128
[SMTPUTF8](https://tools.ietf.org/html/rfc6531) extension. You can also set `email_validator.ALLOW_SMTPUTF8` to `False` to turn it off for all calls by default.
130129

131-
`check_deliverability=True`: If true, DNS queries check that a non-null MX (or A/AAAA record as an MX fallback) is present for the domain-part of the email address and that a reject-all SPF record is not present. Set to `False` to skip these DNS checks. DNS is slow and sometimes unavailable, so consider whether these checks are useful for your use case. It is recommended to pass `False` when performing validation for login pages (but not account creation pages) since re-validation of the domain by querying DNS at every login is probably undesirable. You can also set `email_validator.CHECK_DELIVERABILITY` to `False` to turn this off for all calls by default.
130+
`check_deliverability=True`: If true, a DNS query is made to check that a non-null MX record is present for the domain-part of the email address (or if not, an A/AAAA record as an MX fallback can be present but in that case a reject-all SPF record must not be present). Set to `False` to skip this DNS-based check. DNS is slow and sometimes unavailable, so consider whether these checks are useful for your use case. It is recommended to pass `False` when performing validation for login pages (but not account creation pages) since re-validation of a previously validated domain in your database by querying DNS at every login is probably undesirable. You can also set `email_validator.CHECK_DELIVERABILITY` to `False` to turn this off for all calls by default.
132131

133132
`allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.
134133
`@example.com`), e.g. for validating Postfix aliases.
@@ -382,7 +381,7 @@ are:
382381
| `smtputf8` | A boolean indicating that the [SMTPUTF8](https://tools.ietf.org/html/rfc6531) feature of your mail relay will be required to transmit messages to this address because the local part of the address has non-ASCII characters (the local part cannot be IDNA-encoded). If `allow_smtputf8=False` is passed as an argument, this flag will always be false because an exception is raised if it would have been true. |
383382
| `mx` | A list of (priority, domain) tuples of MX records specified in the DNS for the domain (see [RFC 5321 section 5](https://tools.ietf.org/html/rfc5321#section-5)). May be `None` if the deliverability check could not be completed because of a temporary issue like a timeout. |
384383
| `mx_fallback_type` | `None` if an `MX` record is found. If no MX records are actually specified in DNS and instead are inferred, through an obsolete mechanism, from A or AAAA records, the value is the type of DNS record used instead (`A` or `AAAA`). May be `None` if the deliverability check could not be completed because of a temporary issue like a timeout. |
385-
| `spf` | Any SPF record found while checking deliverability. |
384+
| `spf` | Any SPF record found while checking deliverability. Only set if the SPF record is queried. |
386385

387386
Assumptions
388387
-----------
@@ -394,11 +393,10 @@ or likely to cause trouble:
394393
* The validator assumes the email address is intended to be
395394
usable on the public Internet. The domain part
396395
of the email address must be a resolvable domain name
397-
(without NULL MX or SPF -all DNS records) if deliverability
398-
checks are turned on.
396+
(see the deliverability checks described above).
399397
Most [Special Use Domain Names](https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml)
400-
and their subdomains and
401-
domain names without a `.` are rejected as a syntax error
398+
and their subdomains, as well as
399+
domain names without a `.`, are rejected as a syntax error
402400
(except see the `test_environment` parameter above).
403401
* Obsolete email syntaxes are rejected:
404402
The "quoted string" form of the local part of the email address (RFC

email_validator/__init__.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ def dns_resolver_resolve_shim(domain, record):
648648

649649
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
650650

651-
# If there was no MX record, fall back to an A record.
651+
# If there was no MX record, fall back to an A record, as SMTP servers do.
652652
try:
653653
response = dns_resolver_resolve_shim(domain, "A")
654654
deliverability_info["mx"] = [(0, str(r)) for r in response]
@@ -666,23 +666,25 @@ def dns_resolver_resolve_shim(domain, record):
666666
# this domain is not deliverable.
667667
raise EmailUndeliverableError("The domain name %s does not exist." % domain_i18n)
668668

669-
try:
670-
# Check for a SPF reject all ("v=spf1 -all") record which indicates
671-
# no emails are sent from this domain, which like a NULL MX record
672-
# would indicate that the domain is not used for email.
673-
response = dns_resolver_resolve_shim(domain, "TXT")
674-
for rec in response:
675-
value = b"".join(rec.strings)
676-
if value.startswith(b"v=spf1 "):
677-
deliverability_info["spf"] = value.decode("ascii", errors='replace')
678-
if value == b"v=spf1 -all":
679-
raise EmailUndeliverableError("The domain name %s does not send email." % domain_i18n)
680-
except dns.resolver.NoAnswer:
681-
# No TXT records means there is no SPF policy, so we cannot take any action.
682-
pass
683-
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN):
684-
# Failure to resolve at this step will be ignored.
685-
pass
669+
# Check for a SPF reject-all record ("v=spf1 -all") which indicates
670+
# no emails are sent from this domain (similar to a NULL MX record
671+
# but for sending rather than receiving). In combination with the
672+
# absence of an MX record, this is probably a good sign that the
673+
# domain is not used for email.
674+
try:
675+
response = dns_resolver_resolve_shim(domain, "TXT")
676+
for rec in response:
677+
value = b"".join(rec.strings)
678+
if value.startswith(b"v=spf1 "):
679+
deliverability_info["spf"] = value.decode("ascii", errors='replace')
680+
if value == b"v=spf1 -all":
681+
raise EmailUndeliverableError("The domain name %s does not send email." % domain_i18n)
682+
except dns.resolver.NoAnswer:
683+
# No TXT records means there is no SPF policy, so we cannot take any action.
684+
pass
685+
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN):
686+
# Failure to resolve at this step will be ignored.
687+
pass
686688

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

tests/test_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ def test_dict_accessor():
528528

529529
def test_deliverability_found():
530530
response = validate_email_deliverability('gmail.com', 'gmail.com')
531-
assert response.keys() == {'mx', 'mx_fallback_type', 'spf'}
531+
assert response.keys() == {'mx', 'mx_fallback_type'}
532532
assert response['mx_fallback_type'] is None
533533
assert len(response['mx']) > 1
534534
assert len(response['mx'][0]) == 2

0 commit comments

Comments
 (0)