Skip to content

Commit 416ff5c

Browse files
committed
Accept at-signs inside quoted email local parts
1 parent 6ffd862 commit 416ff5c

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

‎src/validators/email.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ def email(
5555
(Literal[True]): If `value` is a valid eMail.
5656
(ValidationError): If `value` is an invalid eMail.
5757
"""
58-
if not value or value.count("@") != 1:
58+
if not value or "@" not in value:
5959
return False
6060

61+
# Quoted local parts may contain at-signs; the final one separates the domain.
6162
username_part, domain_part = value.rsplit("@", 1)
6263

6364
if len(username_part) > 64 or len(domain_part) > 253:

‎tests/test_email.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
("łemłail@here.com",),
2222
("email@localdomain.org",),
2323
('"\\\011"@here.com',),
24+
('"test@test"@example.com',),
2425
],
2526
)
2627
def test_returns_true_on_valid_email(value: str):
@@ -45,7 +46,6 @@ def test_returns_true_on_valid_email(value: str):
4546
("example@inv-.alid-.com",),
4647
("example@inv-.-alid.com",),
4748
("john56789.john56789.john56789.john56789.john56789.john56789.john5@example.com",),
48-
('"test@test"@example.com',),
4949
# Quoted-string format (CR not allowed)
5050
('"\\\012"@here.com',),
5151
# Non-quoted space/semicolon not allowed
@@ -66,3 +66,27 @@ def test_rejects_newline_after_local_part(local_part: str):
6666
# 2026-10-11: A newline before the at-sign is not part of a valid local part.
6767
assert email(f"{local_part}@example.com") is True
6868
assert isinstance(email(f"{local_part}\n@example.com"), ValidationError)
69+
70+
71+
@pytest.mark.parametrize("local_part", ['"@"', '"first@last"', '"first@@last"'])
72+
def test_accepts_at_signs_inside_quoted_local_part(local_part: str):
73+
"""Treat quoted at-signs as local-part content rather than separators."""
74+
assert email(f"{local_part}@example.com") is True
75+
76+
77+
@pytest.mark.parametrize(
78+
"value",
79+
[
80+
"first@last@example.com",
81+
"first@@example.com",
82+
'"first@last@example.com',
83+
'first@last"@example.com',
84+
'"first"@last@example.com',
85+
'"first@last"@@example.com',
86+
'"first@last"@',
87+
'"first@last"@-example.com',
88+
],
89+
)
90+
def test_rejects_invalid_addresses_containing_multiple_at_signs(value: str):
91+
"""Continue validating the complete local part and domain after splitting."""
92+
assert isinstance(email(value), ValidationError)

0 commit comments

Comments
 (0)