Skip to content

fix: preserve original email string for quoted local parts with backslash escapes#169

Open
gaoflow wants to merge 1 commit into
JoshData:mainfrom
gaoflow:fix/original-field-escaped-quoted-local
Open

fix: preserve original email string for quoted local parts with backslash escapes#169
gaoflow wants to merge 1 commit into
JoshData:mainfrom
gaoflow:fix/original-field-escaped-quoted-local

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 25, 2026

Copy link
Copy Markdown

Bug

When validate_email is called with a quoted local part that contains backslash-escaped characters — for example "test\"foo"@example.com — the original field on the returned ValidatedEmail object is built incorrectly.

The code reconstructs ret.original by re-quoting the already-unescaped local_part. Since split_email() returns the local part with escapes removed (i.e. test"foo), re-wrapping it in quotes produces "test"foo"@example.com — an invalid, malformed string that is also one character shorter than the actual input.

from email_validator import validate_email

email = '"test\\"foo"@example.com'
r = validate_email(email, check_deliverability=False, allow_quoted_local=True)

# Before fix:
# r.original == '"test"foo"@example.com'  ← wrong (invalid, missing backslash)
# r.normalized == '"test\\"foo"@example.com'  ← correct

# After fix:
# r.original == '"test\\"foo"@example.com'  ← correct (same as input)
# r.normalized == '"test\\"foo"@example.com'  ← correct

The documented contract (ValidatedEmail.original: "The email address that was passed to validate_email.") is violated. Additionally, the incorrect shorter string is used for email length checks (validate_email_length), which could allow emails that are actually over the 254-byte limit to pass when they contain backslash-escaped characters in a quoted local part.

Fix

When no display name is present, use the original email argument directly — no reconstruction needed. When a display name is present (e.g. "John" <addr@domain>), extract the address from inside the angle brackets to preserve the existing behavior of stripping the display name for email length checks.

All 288 existing syntax tests pass.


This pull request was prepared with the assistance of AI, under my direction and review.

When a quoted local part contains backslash-escaped characters (e.g.
"test\"foo"@example.com), the `original` field on the returned
ValidatedEmail object was being reconstructed by re-quoting the
already-unescaped local part. This produced an invalid string
('"test"foo"@example.com') that differed from the actual input and
was shorter because the backslash escape was lost.

Fix by using the original `email` argument directly when no display
name is present. When a display name is present (e.g. "John"
<addr@domain>), extract just the address from inside the angle
brackets so that email length checks still operate on the
address-only form, as before.

The documented contract for ValidatedEmail.original is: "The email
address that was passed to validate_email." This commit makes that
accurate for all inputs, including quoted local parts with escapes.
@JoshData

Copy link
Copy Markdown
Owner

Thanks for the bug report.

Your regex solution is I think wildly incorrect because you haven't actually parsed the email address. For example, it would fail on "<Hello>" <x@y.com>. I am going to chalk that up to your use of AI.

I've fixed it with a test here. Linking before merging in case you see any issues:

main...refs/heads/original-local-part-unescape

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.

2 participants