Skip to content

Commit e1263df

Browse files
committed
Match the email local part without Unicode case folding
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 its case-folded form is in a class, which admitted 31 characters outside those ranges: the Kelvin sign (U+212A) through "k", the Angstrom sign (U+212B), Greek mu, U+1E9E and IPA letters. The Kelvin and Angstrom signs render as "K" and "A with ring above", so "Kelvin@example.com" could be registered next to a lookalike. With re.ASCII, case-insensitive matching applies to ASCII letters only. Both cases of the allowed Latin letters are already in the ranges, so nothing inside them is lost.
1 parent 22786cc commit e1263df

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 folding would also admit characters outside
99+
# 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, folds to k
81+
"\u212belvin", # Angstrom sign, folds to a with ring above
82+
"\u1e9eelvin", # capital sharp s, folds to sharp s
83+
"\u039celvin", # Greek capital mu, folds to micro sign
84+
"\u2c65elvin", # a with stroke, folds to capital a with stroke
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)