From 416ff5c971c2f58e6f2f4ca9124f1941ccf624b0 Mon Sep 17 00:00:00 2001 From: prnvh Date: Thu, 15 Oct 2026 23:36:44 +0530 Subject: [PATCH] Accept at-signs inside quoted email local parts --- src/validators/email.py | 3 ++- tests/test_email.py | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/validators/email.py b/src/validators/email.py index 46cc594..51b1bf5 100644 --- a/src/validators/email.py +++ b/src/validators/email.py @@ -55,9 +55,10 @@ def email( (Literal[True]): If `value` is a valid eMail. (ValidationError): If `value` is an invalid eMail. """ - if not value or value.count("@") != 1: + if not value or "@" not in value: return False + # Quoted local parts may contain at-signs; the final one separates the domain. username_part, domain_part = value.rsplit("@", 1) if len(username_part) > 64 or len(domain_part) > 253: diff --git a/tests/test_email.py b/tests/test_email.py index 990ef8e..cb424ae 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -21,6 +21,7 @@ ("łemłail@here.com",), ("email@localdomain.org",), ('"\\\011"@here.com',), + ('"test@test"@example.com',), ], ) def test_returns_true_on_valid_email(value: str): @@ -45,7 +46,6 @@ def test_returns_true_on_valid_email(value: str): ("example@inv-.alid-.com",), ("example@inv-.-alid.com",), ("john56789.john56789.john56789.john56789.john56789.john56789.john5@example.com",), - ('"test@test"@example.com',), # Quoted-string format (CR not allowed) ('"\\\012"@here.com',), # Non-quoted space/semicolon not allowed @@ -66,3 +66,27 @@ def test_rejects_newline_after_local_part(local_part: str): # 2026-10-11: A newline before the at-sign is not part of a valid local part. assert email(f"{local_part}@example.com") is True assert isinstance(email(f"{local_part}\n@example.com"), ValidationError) + + +@pytest.mark.parametrize("local_part", ['"@"', '"first@last"', '"first@@last"']) +def test_accepts_at_signs_inside_quoted_local_part(local_part: str): + """Treat quoted at-signs as local-part content rather than separators.""" + assert email(f"{local_part}@example.com") is True + + +@pytest.mark.parametrize( + "value", + [ + "first@last@example.com", + "first@@example.com", + '"first@last@example.com', + 'first@last"@example.com', + '"first"@last@example.com', + '"first@last"@@example.com', + '"first@last"@', + '"first@last"@-example.com', + ], +) +def test_rejects_invalid_addresses_containing_multiple_at_signs(value: str): + """Continue validating the complete local part and domain after splitting.""" + assert isinstance(email(value), ValidationError)