Skip to content

Match the email local part without Unicode case-insensitivity - #536

Open
nandgator wants to merge 1 commit into
masterfrom
fix-email-local-part-casefold
Open

nandgator wants to merge 1 commit into
masterfrom
fix-email-local-part-casefold

Conversation

@nandgator

@nandgator nandgator commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

email() matches the local part against an explicit allowlist: ASCII letters and digits, the RFC 5322 specials, and U+00A0 to U+024F. The pattern is compiled with re.IGNORECASE but not re.ASCII, so a character also matches when re treats one of its other-case forms as equal to a listed character (Python docs). That admits 31 characters outside the listed ranges, in both dot-atom and quoted local parts. Among them are the Kelvin sign (U+212A), which renders as K, and the Angstrom sign (U+212B), which renders as Å:

>>> validators.email(chr(0x212A) + "elvin@example.com")   # renders as Kelvin@example.com
True

The others are Greek mu (U+039C, U+03BC), capital sharp s (U+1E9E), 21 IPA letters (U+0253 to U+0292) and 5 Latin Extended-C and -D letters.

Change

Add re.ASCII, so that case-insensitive matching applies to ASCII letters only. Every character inside the listed ranges still matches itself, so upper, lower and mixed-case local parts like ŁÓKAŚĆ still validate. I compared the old and new patterns over every code point: exactly those 31 characters are dropped and none are gained.

Tests

The new rejection cases (five of the characters, each unquoted and quoted) fail on master and pass here. Positive cases cover upper, lower and mixed case inside the allowed ranges. The full suite (1589 tests), ruff check, ruff format, pyright and bandit pass, and the opengrep Unicode rule no longer reports email.py. A fuzz comparison against master over 60,000 random local parts found nothing newly accepted. Every newly rejected value contains a character outside ASCII and U+00A0 to U+024F.

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.
@nandgator
nandgator force-pushed the fix-email-local-part-casefold branch from e1263df to 4b013af Compare October 22, 2026 16:04
@nandgator nandgator changed the title Match the email local part without Unicode case folding Match the email local part without Unicode case-insensitivity Oct 22, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant