Skip to content

Commit 4b013af

Browse files
committed
Match the email local part without Unicode case-insensitivity
The local-part pattern lists the characters it allows explicitly: ASCII letters and digits, the RFC 5322 specials and U+00A0 to U+024F. Compiled with re.IGNORECASE but not re.ASCII, a character also matches when re treats one of its other-case forms as equal to a listed character. That admitted 31 characters outside those ranges, among them the Kelvin sign (U+212A, lowercase "k"), the Angstrom sign (U+212B, lowercase U+00E5), Greek mu, U+1E9E and IPA letters. The Kelvin and Angstrom signs render as "K" and "A with ring above", so a lookalike of "Kelvin@example.com" validated. With re.ASCII, case-insensitive matching applies to ASCII letters only. Every character inside the listed ranges still matches itself, so nothing inside them is lost; comparing both patterns over all code points drops exactly those 31 characters and adds none.
1 parent 22786cc commit 4b013af

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

‎src/validators/email.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ def email(
9595
+ r"!#-\[\]-\177]|\\[\011.]"
9696
+ r')*")$',
9797
username_part,
98-
re.IGNORECASE,
98+
# re.ASCII: Unicode case-insensitive matching would also admit characters
99+
# outside these ranges, such as the Kelvin sign as a "k"
100+
re.IGNORECASE | re.ASCII,
99101
)
100102
else False
101103
)

‎tests/test_email.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,29 @@ def test_rejects_newline_after_local_part(local_part: str):
7272
def test_returns_failed_validation_on_idna_mapped_lookalike_domain(domain_part: str):
7373
"""Test returns failed validation on email domains that only IDNA 2003 mapping makes valid."""
7474
assert isinstance(email(f"user@{domain_part}"), ValidationError)
75+
76+
77+
@pytest.mark.parametrize(
78+
"local_part",
79+
[
80+
"\u212aelvin", # Kelvin sign, lowercase is k
81+
"\u212belvin", # Angstrom sign, lowercase is U+00E5
82+
"\u1e9eelvin", # capital sharp s, lowercase is U+00DF
83+
"\u039celvin", # Greek capital mu, re equates it with U+00B5
84+
"\u2c65elvin", # a with stroke, uppercase is U+023A
85+
],
86+
)
87+
@pytest.mark.parametrize("quoted", [False, True])
88+
def test_returns_failed_validation_on_case_folded_local_part(local_part: str, quoted: bool):
89+
"""Test returns failed validation on characters allowed only through Unicode case folding."""
90+
if quoted:
91+
local_part = f'"{local_part}"'
92+
assert isinstance(email(f"{local_part}@example.com"), ValidationError)
93+
94+
95+
@pytest.mark.parametrize(
96+
"local_part", ["KELVIN", "Kelvin", "\u0141\u00d3KA\u015a\u0106", "\u00ff\u0178", '"Kelvin"']
97+
)
98+
def test_returns_true_on_mixed_case_local_part(local_part: str):
99+
"""Test returns true on upper and lower case letters inside the allowed ranges."""
100+
assert email(f"{local_part}@example.com")

0 commit comments

Comments
 (0)