Skip to content

Commit f90d256

Browse files
committed
Remove local part length check unless new strict flag is given, fixes #158
1 parent 98800ba commit f90d256

File tree

6 files changed

+47
-10
lines changed

6 files changed

+47
-10
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ The `validate_email` function also accepts the following keyword arguments
150150
`allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.
151151
`@example.com`), e.g. for validating Postfix aliases.
152152

153+
`strict=False`: Set to `True` to perform additional syntax checks (currently only a local part length check). This should be used by mail service providers at address creation to ensure email addresses meet broad compatibility requirements.
153154

154155
### DNS timeout and cache
155156

email_validator/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def caching_resolver(*args, **kwargs):
3131
ALLOW_QUOTED_LOCAL = False
3232
ALLOW_DOMAIN_LITERAL = False
3333
ALLOW_DISPLAY_NAME = False
34+
STRICT = False
3435
GLOBALLY_DELIVERABLE = True
3536
CHECK_DELIVERABILITY = True
3637
TEST_ENVIRONMENT = False

email_validator/rfc_constants.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,24 @@
3636
QTEXT_INTL = re.compile(r"[\u0020-\u007E\u0080-\U0010FFFF]")
3737

3838
# Length constants
39+
3940
# RFC 3696 + errata 1003 + errata 1690 (https://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690)
40-
# explains the maximum length of an email address is 254 octets.
41+
# explains the maximum length of an email address is 254 octets based on RFC 5321 4.5.3.1.3. A
42+
# maximum local part length is also given at RFC 5321 4.5.3.1.1.
43+
#
44+
# But RFC 5321 4.5.3.1 says that these (and other) limits are in a sense suggestions, and longer
45+
# local parts have been seen in the wild. Consequntely, the local part length is only checked
46+
# in "strict" mode. Although the email address maximum length is also somewhat of a suggestion,
47+
# I don't like the idea of having no length checks performed, so I'm leaving that to always be
48+
# checked.
4149
EMAIL_MAX_LENGTH = 254
4250
LOCAL_PART_MAX_LENGTH = 64
51+
52+
# Although RFC 5321 4.5.3.1.2 gives a (suggested, see above) limit of 255 octets, RFC 1035 2.3.4 also
53+
# imposes a length limit (255 octets). But per https://stackoverflow.com/questions/32290167/what-is-the-maximum-length-of-a-dns-name,
54+
# two of those octets are taken up by the optional final dot and null root label.
4355
DNS_LABEL_LENGTH_LIMIT = 63 # in "octets", RFC 1035 2.3.1
44-
DOMAIN_MAX_LENGTH = 253 # in "octets" as transmitted, RFC 1035 2.3.4 and RFC 5321 4.5.3.1.2, and see https://stackoverflow.com/questions/32290167/what-is-the-maximum-length-of-a-dns-name
56+
DOMAIN_MAX_LENGTH = 253 # in "octets" as transmitted
4557

4658
# RFC 2142
4759
CASE_INSENSITIVE_MAILBOX_NAMES = [

email_validator/syntax.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ class LocalPartValidationResult(TypedDict):
229229

230230

231231
def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_empty_local: bool = False,
232-
quoted_local_part: bool = False) -> LocalPartValidationResult:
232+
quoted_local_part: bool = False, strict: bool = False) -> LocalPartValidationResult:
233233
"""Validates the syntax of the local part of an email address."""
234234

235235
if len(local) == 0:
@@ -251,7 +251,7 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
251251
# internationalized, then the UTF-8 encoding may be longer, but
252252
# that may not be relevant. We will check the total address length
253253
# instead.
254-
if len(local) > LOCAL_PART_MAX_LENGTH:
254+
if strict and len(local) > LOCAL_PART_MAX_LENGTH:
255255
reason = get_length_reason(local, limit=LOCAL_PART_MAX_LENGTH)
256256
raise EmailSyntaxError(f"The email address is too long before the @-sign {reason}.")
257257

email_validator/validate_email.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def validate_email(
2222
allow_quoted_local: Optional[bool] = None,
2323
allow_domain_literal: Optional[bool] = None,
2424
allow_display_name: Optional[bool] = None,
25+
strict: Optional[bool] = None,
2526
check_deliverability: Optional[bool] = None,
2627
test_environment: Optional[bool] = None,
2728
globally_deliverable: Optional[bool] = None,
@@ -36,7 +37,7 @@ def validate_email(
3637

3738
# Fill in default values of arguments.
3839
from . import ALLOW_SMTPUTF8, ALLOW_EMPTY_LOCAL, ALLOW_QUOTED_LOCAL, ALLOW_DOMAIN_LITERAL, ALLOW_DISPLAY_NAME, \
39-
GLOBALLY_DELIVERABLE, CHECK_DELIVERABILITY, TEST_ENVIRONMENT, DEFAULT_TIMEOUT
40+
STRICT, GLOBALLY_DELIVERABLE, CHECK_DELIVERABILITY, TEST_ENVIRONMENT, DEFAULT_TIMEOUT
4041
if allow_smtputf8 is None:
4142
allow_smtputf8 = ALLOW_SMTPUTF8
4243
if allow_empty_local is None:
@@ -47,6 +48,8 @@ def validate_email(
4748
allow_domain_literal = ALLOW_DOMAIN_LITERAL
4849
if allow_display_name is None:
4950
allow_display_name = ALLOW_DISPLAY_NAME
51+
if strict is None:
52+
strict = STRICT
5053
if check_deliverability is None:
5154
check_deliverability = CHECK_DELIVERABILITY
5255
if test_environment is None:
@@ -95,7 +98,8 @@ def validate_email(
9598
local_part_info = validate_email_local_part(local_part,
9699
allow_smtputf8=allow_smtputf8,
97100
allow_empty_local=allow_empty_local,
98-
quoted_local_part=is_quoted_local_part)
101+
quoted_local_part=is_quoted_local_part,
102+
strict=strict)
99103
ret.local_part = local_part_info["local_part"]
100104
ret.ascii_local_part = local_part_info["ascii_local_part"]
101105
ret.smtputf8 = local_part_info["smtputf8"]
@@ -118,7 +122,8 @@ def validate_email(
118122
validate_email_local_part(normalized_local_part,
119123
allow_smtputf8=allow_smtputf8,
120124
allow_empty_local=allow_empty_local,
121-
quoted_local_part=is_quoted_local_part)
125+
quoted_local_part=is_quoted_local_part,
126+
strict=strict)
122127
except EmailSyntaxError as e:
123128
raise EmailSyntaxError("After Unicode normalization: " + str(e)) from e
124129
ret.local_part = normalized_local_part

tests/test_syntax.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,6 @@ def test_domain_literal() -> None:
415415
('test@\n', 'The part after the @-sign contains invalid characters: U+000A.'),
416416
('bad"quotes"@example.com', 'The email address contains invalid characters before the @-sign: \'"\'.'),
417417
('obsolete."quoted"[email protected]', 'The email address contains invalid characters before the @-sign: \'"\'.'),
418-
('11111111112222222222333333333344444444445555555555666666666677777@example.com', 'The email address is too long before the @-sign (1 character too many).'),
419-
('111111111122222222223333333333444444444455555555556666666666777777@example.com', 'The email address is too long before the @-sign (2 characters too many).'),
420-
('\uFB2C111111122222222223333333333444444444455555555556666666666777777@example.com', 'After Unicode normalization: The email address is too long before the @-sign (2 characters too many).'),
421418
('me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444444445555555555.com', 'The email address is too long after the @-sign (1 character too many).'),
422419
('me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444.com', 'The email address is too long after the @-sign (1 byte too many after IDNA encoding).'),
423420
('me@\uFB2C1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444.com', 'The email address is too long after the @-sign (5 bytes too many after IDNA encoding).'),
@@ -467,6 +464,22 @@ def test_email_invalid_syntax(email_input: str, error_msg: str) -> None:
467464
assert str(exc_info.value) == error_msg
468465

469466

467+
@pytest.mark.parametrize(
468+
'email_input,error_msg',
469+
[
470+
('11111111112222222222333333333344444444445555555555666666666677777@example.com', 'The email address is too long before the @-sign (1 character too many).'),
471+
('111111111122222222223333333333444444444455555555556666666666777777@example.com', 'The email address is too long before the @-sign (2 characters too many).'),
472+
('\uFB2C111111122222222223333333333444444444455555555556666666666777777@example.com', 'After Unicode normalization: The email address is too long before the @-sign (2 characters too many).'),
473+
])
474+
def test_email_invalid_syntax_strict(email_input: str, error_msg: str) -> None:
475+
# Since these all have syntax errors, deliverability
476+
# checks do not arise.
477+
validate_email(email_input, check_deliverability=False) # pass without strict
478+
with pytest.raises(EmailSyntaxError) as exc_info:
479+
validate_email(email_input, strict=True, check_deliverability=False)
480+
assert str(exc_info.value) == error_msg
481+
482+
470483
@pytest.mark.parametrize(
471484
'email_input',
472485
[
@@ -728,6 +741,11 @@ def test_pyisemail_tests(email_input: str, status: str) -> None:
728741
validate_email(email_input, test_environment=True)
729742
validate_email(email_input, allow_quoted_local=True, allow_domain_literal=True, test_environment=True)
730743

744+
elif status == "ISEMAIL_RFC5322_LOCAL_TOOLONG":
745+
# Requires strict.
746+
with pytest.raises(EmailSyntaxError):
747+
validate_email(email_input, strict=True, test_environment=True)
748+
731749
elif status == "ISEMAIL_RFC5321_QUOTEDSTRING":
732750
# Quoted-literal local parts are only valid with an option.
733751
with pytest.raises(EmailSyntaxError):

0 commit comments

Comments
 (0)