From aaff4ac90d9c73072bde82bdb5cb79815e395b64 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Thu, 25 Jun 2026 08:58:13 +0200 Subject: [PATCH] fix: preserve original email string for quoted local parts with escapes 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" ), 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. --- email_validator/validate_email.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/email_validator/validate_email.py b/email_validator/validate_email.py index 5837ed2..cb21e60 100644 --- a/email_validator/validate_email.py +++ b/email_validator/validate_email.py @@ -1,4 +1,5 @@ from typing import Optional, TYPE_CHECKING +import re import unicodedata from .exceptions import EmailSyntaxError @@ -90,9 +91,16 @@ def validate_email( # Collect return values in this instance. ret = ValidatedEmail() - ret.original = ((local_part if not is_quoted_local_part - else ('"' + local_part + '"')) - + "@" + domain_part) # drop the display name, if any, for email length tests at the end + # Store the original email address without any display name, preserving + # the original form (including backslash escapes in quoted local parts). + # When no display name is present, use the original email string directly. + # When a display name is present, extract just the address from the angle + # brackets so that email length checks use the address-only form. + if display_name is None: + ret.original = email + else: + m = re.search(r'<(.+)>', email) + ret.original = m.group(1) if m else email ret.display_name = display_name # Validate the email address's local part syntax and get a normalized form.