Skip to content

Commit bdf1103

Browse files
committed
[WIP] Make specialized exception types for each check
1 parent 5f1f6df commit bdf1103

File tree

3 files changed

+224
-87
lines changed

3 files changed

+224
-87
lines changed

email_validator/deliverability.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import ipaddress
44

5-
from .exceptions import EmailUndeliverableError
5+
from .exceptions import (EmailUndeliverableError,
6+
EmailUndeliverableNullMxError, EmailUndeliverableNoMxError,
7+
EmailUndeliverableFallbackDeniesSendingMailError,
8+
EmailUndeliverableNoDomainError, EmailUndeliverableOtherError)
69

710
import dns.resolver
811
import dns.exception
@@ -60,7 +63,7 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
6063
mtas = [(preference, exchange) for preference, exchange in mtas
6164
if exchange != ""]
6265
if len(mtas) == 0: # null MX only, if there were no MX records originally a NoAnswer exception would have occurred
63-
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not accept email.")
66+
raise EmailUndeliverableNullMxError(domain_i18n)
6467

6568
deliverability_info["mx"] = mtas
6669
deliverability_info["mx_fallback_type"] = None
@@ -110,7 +113,7 @@ def is_global_addr(address: Any) -> bool:
110113
# this domain is not deliverable, although the domain
111114
# name has other records (otherwise NXDOMAIN would
112115
# have been raised).
113-
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not accept email.") from e
116+
raise EmailUndeliverableNoMxError(domain_i18n) from e
114117

115118
# Check for a SPF (RFC 7208) reject-all record ("v=spf1 -all") which indicates
116119
# no emails are sent from this domain (similar to a Null MX record
@@ -123,15 +126,15 @@ def is_global_addr(address: Any) -> bool:
123126
value = b"".join(rec.strings)
124127
if value.startswith(b"v=spf1 "):
125128
if value == b"v=spf1 -all":
126-
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not send email.")
129+
raise EmailUndeliverableFallbackDeniesSendingMailError(domain_i18n)
127130
except dns.resolver.NoAnswer:
128131
# No TXT records means there is no SPF policy, so we cannot take any action.
129132
pass
130133

131134
except dns.resolver.NXDOMAIN as e:
132135
# The domain name does not exist --- there are no records of any sort
133136
# for the domain name.
134-
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not exist.") from e
137+
raise EmailUndeliverableNoDomainError(domain_i18n) from e
135138

136139
except dns.resolver.NoNameservers:
137140
# All nameservers failed to answer the query. This might be a problem
@@ -152,8 +155,6 @@ def is_global_addr(address: Any) -> bool:
152155

153156
except Exception as e:
154157
# Unhandled conditions should not propagate.
155-
raise EmailUndeliverableError(
156-
"There was an error while checking if the domain name in the email address is deliverable: " + str(e)
157-
) from e
158+
raise EmailUndeliverableOtherError(domain_i18n) from e
158159

159160
return deliverability_info

email_validator/exceptions.py

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from dataclasses import dataclass
2+
import unicodedata
3+
4+
15
class EmailNotValidError(ValueError):
26
"""Parent class of all exceptions raised by this module."""
37
pass
@@ -8,6 +12,153 @@ class EmailSyntaxError(EmailNotValidError):
812
pass
913

1014

15+
class EmailSyntaxNoAtSignError(EmailSyntaxError):
16+
"""Exception raised when an email address is missing an @-sign."""
17+
def __str__(self):
18+
return "An email address must have an @-sign."
19+
20+
21+
@dataclass
22+
class EmailSyntaxAtSignConfusedError(EmailSyntaxNoAtSignError):
23+
"""Exception raised when an email address is missing an @-sign but a confusable character is present."""
24+
character: str
25+
def __str__(self):
26+
return f"The email address has the {self.character} character instead of a regular at-sign."
27+
28+
29+
def safe_character_display(c: str) -> str:
30+
# Return safely displayable characters in quotes.
31+
if c == '\\':
32+
return f"\"{c}\"" # can't use repr because it escapes it
33+
if unicodedata.category(c)[0] in ("L", "N", "P", "S"):
34+
return repr(c)
35+
36+
# Construct a hex string in case the unicode name doesn't exist.
37+
if ord(c) < 0xFFFF:
38+
h = f"U+{ord(c):04x}".upper()
39+
else:
40+
h = f"U+{ord(c):08x}".upper()
41+
42+
# Return the character name or, if it has no name, the hex string.
43+
return unicodedata.name(c, h)
44+
45+
46+
@dataclass
47+
class EmailInvalidCharactersError(EmailSyntaxError):
48+
"""Exception raised when an email address fails validation because it contains invalid characters."""
49+
characters: list[str]
50+
def __str__(self):
51+
return ", ".join(safe_character_display(c) for c in self.characters)
52+
53+
54+
class EmailInvalidCharactersAfterQuotedString(EmailInvalidCharactersError):
55+
"""Exception raised when an email address fails validation because it contains invalid characters after a quoted string."""
56+
def __str__(self):
57+
return "Extra character(s) found after close quote: " + EmailInvalidCharactersError.__str__(self) + "."
58+
59+
60+
class EmailInvalidCharactersInUnquotedDisplayName(EmailInvalidCharactersError):
61+
"""Exception raised when an email address fails validation because it contains invalid characters after a quoted string."""
62+
def __str__(self):
63+
return "The display name contains invalid characters when not quoted: " + EmailInvalidCharactersError.__str__(self) + "."
64+
65+
66+
class EmailIntlCharactersInLocalPart(EmailInvalidCharactersError):
67+
"""Exception raised when an email address fails validation because it contains invalid characters after a quoted string."""
68+
def __str__(self):
69+
return "Internationalized characters before the @-sign are not supported: " + EmailInvalidCharactersError.__str__(self) + "."
70+
71+
72+
class EmailInvalidCharactersInLocalPart(EmailInvalidCharactersError):
73+
"""Exception raised when an email address fails validation because it contains invalid characters in the local part."""
74+
def __str__(self):
75+
return "The email address contains invalid characters before the @-sign: " + EmailInvalidCharactersError.__str__(self) + "."
76+
77+
78+
class EmailUnsafeCharactersError(EmailInvalidCharactersError):
79+
"""Exception raised when an email address fails validation because it contains invalid characters in the local part."""
80+
def __str__(self):
81+
return "The email address contains unsafe characters: " + EmailInvalidCharactersError.__str__(self) + "."
82+
83+
84+
class EmailInvalidCharactersInDomainPart(EmailInvalidCharactersError):
85+
"""Exception raised when an email address fails validation because it contains invalid characters after a quoted string."""
86+
def __str__(self):
87+
return f"The part after the @-sign contains invalid characters: " + EmailInvalidCharactersError.__str__(self) + "."
88+
89+
90+
class EmailInvalidCharactersInDomainPartAfterUnicodeNormalization(EmailInvalidCharactersError):
91+
"""Exception raised when an email address fails validation because it contains invalid characters after a quoted string."""
92+
def __str__(self):
93+
return f"The part after the @-sign contains invalid characters after Unicode normalization: " + EmailInvalidCharactersError.__str__(self) + "."
94+
95+
96+
class EmailInvalidCharactersInDomainAddressLiteral(EmailInvalidCharactersError):
97+
"""Exception raised when an email address fails validation because it contains invalid characters after a quoted string."""
98+
def __str__(self):
99+
return f"The part after the @-sign contains invalid characters in brackets: " + EmailInvalidCharactersError.__str__(self) + "."
100+
101+
102+
class EmailBracketedAddressMissingCloseBracket(EmailSyntaxError):
103+
"""Exception raised when an email address begins with an angle bracket but does not end with an angle bracket."""
104+
def __str__(self):
105+
return "An open angle bracket at the start of the email address has to be followed by a close angle bracket at the end."
106+
107+
108+
class EmailBracketedAddressExtraneousText(EmailSyntaxError):
109+
"""Exception raised when an email address in angle brackets has text after the angle brackets."""
110+
def __str__(self):
111+
return "There can't be anything after the email address."
112+
113+
114+
class EmailNoLocalPartError(EmailSyntaxError):
115+
"""Exception raised when an email address in angle brackets has text after the angle brackets."""
116+
def __str__(self):
117+
return "There must be something before the @-sign."
118+
119+
120+
@dataclass
121+
class EmailUnhandledSyntaxError(EmailSyntaxError):
122+
"""Exception raised when an email address has an unhandled error."""
123+
message: str
124+
def __str__(self):
125+
return self.message
126+
127+
128+
@dataclass
11129
class EmailUndeliverableError(EmailNotValidError):
12130
"""Exception raised when an email address fails validation because its domain name does not appear deliverable."""
13-
pass
131+
domain: str
132+
133+
134+
@dataclass
135+
class EmailUndeliverableNullMxError(EmailUndeliverableError):
136+
"""Exception raised when an email address fails validation because its domain name has a Null MX record indicating that it cannot receive mail."""
137+
# See https://www.rfc-editor.org/rfc/rfc7505.
138+
def __str__(self):
139+
return f"The domain name {self.domain} does not accept email."
140+
141+
@dataclass
142+
class EmailUndeliverableNoMxError(EmailUndeliverableError):
143+
"""Exception raised when an email address fails validation because its domain name has no MX, A, or AAAA record indicating how to deliver mail."""
144+
def __str__(self):
145+
return f"The domain name {self.domain} does not accept email."
146+
147+
@dataclass
148+
class EmailUndeliverableFallbackDeniesSendingMailError(EmailUndeliverableError):
149+
"""Exception raised when an email address fails validation because its domain name has no MX record and it has a SPF record indicating it does not send mail."""
150+
def __str__(self):
151+
return f"The domain name {self.domain} does not send email."
152+
153+
@dataclass
154+
class EmailUndeliverableNoDomainError(EmailUndeliverableError):
155+
"""Exception raised when an email address fails validation because its domain name does not exist in DNS."""
156+
def __str__(self):
157+
return f"The domain name {self.domain} does not exist."
158+
159+
160+
@dataclass
161+
class EmailUndeliverableOtherError(EmailNotValidError):
162+
"""Exception raised when an email address fails validation because of an unhandled exception."""
163+
def __str__(self):
164+
return "There was an error while checking if the domain name in the email address is deliverable."

0 commit comments

Comments
 (0)