From 2a78adaf66838c5048687664baaa14a6daf2342a Mon Sep 17 00:00:00 2001 From: Fares Rafat <214850337+faresrafat3@users.noreply.github.com> Date: Mon, 2 Nov 2026 11:00:45 +0200 Subject: [PATCH 1/2] Fix ascii_local_part/smtputf8 after NFC normalization of local part When an NFC-normalized local part differs from the original (e.g. the Kelvin sign U+212A normalizes to ASCII K), validate_email revalidated the normalized part but kept the pre-normalization ascii_local_part and smtputf8 values. Update those fields from the second validation result so an ASCII normalized address reports ascii_local_part/ascii_email and smtputf8=False. Fixes #185. --- email_validator/validate_email.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/email_validator/validate_email.py b/email_validator/validate_email.py index 07b398a..e8a07d3 100644 --- a/email_validator/validate_email.py +++ b/email_validator/validate_email.py @@ -121,14 +121,18 @@ def validate_email( normalized_local_part = unicodedata.normalize("NFC", ret.local_part) if normalized_local_part != ret.local_part: try: - validate_email_local_part(normalized_local_part, - allow_smtputf8=allow_smtputf8, - allow_empty_local=allow_empty_local, - quoted_local_part=is_quoted_local_part, - strict=strict) + normalized_local_part_info = validate_email_local_part( + normalized_local_part, + allow_smtputf8=allow_smtputf8, + allow_empty_local=allow_empty_local, + quoted_local_part=is_quoted_local_part, + strict=strict, + ) except EmailSyntaxError as e: raise EmailSyntaxError("After Unicode normalization: " + str(e)) from e ret.local_part = normalized_local_part + ret.ascii_local_part = normalized_local_part_info["ascii_local_part"] + ret.smtputf8 = normalized_local_part_info["smtputf8"] # If a quoted local part isn't allowed but is present, now raise an exception. # This is done after any exceptions raised by validate_email_local_part so From 500465aee251ad0cf3ce410dcbeb8e5897cc1edd Mon Sep 17 00:00:00 2001 From: Fares Rafat <214850337+faresrafat3@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:36:12 +0200 Subject: [PATCH 2/2] Fix quoted local parts after NFC normalization (review finding) Unquote the NFC-normalized local part before re-validation so that validate_email_local_part (which expects an unquoted value and re-quotes it when quoted_local_part is True) does not double-escape. Also use the re-validated local_part value instead of the raw NFC string. Addresses review finding on PR #186. --- email_validator/validate_email.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/email_validator/validate_email.py b/email_validator/validate_email.py index e8a07d3..ffb5d8e 100644 --- a/email_validator/validate_email.py +++ b/email_validator/validate_email.py @@ -120,17 +120,37 @@ def validate_email( # data, and I don't know whether the rules really apply in the wild.) normalized_local_part = unicodedata.normalize("NFC", ret.local_part) if normalized_local_part != ret.local_part: + # If the original local part was quoted, NFC normalization keeps + # the surrounding quotes. Unquote before re-validating so that + # validate_email_local_part (which expects an unquoted value and + # re-quotes it when quoted_local_part is True) does not double-escape. + if (is_quoted_local_part and len(normalized_local_part) >= 2 + and normalized_local_part[0] == '"' and normalized_local_part[-1] == '"'): + to_validate = "" + escaped = False + for c in normalized_local_part[1:-1]: + if escaped: + to_validate += c + escaped = False + elif c == "\\": + escaped = True + else: + to_validate += c + revalidate_quoted = True + else: + to_validate = normalized_local_part + revalidate_quoted = is_quoted_local_part try: normalized_local_part_info = validate_email_local_part( - normalized_local_part, + to_validate, allow_smtputf8=allow_smtputf8, allow_empty_local=allow_empty_local, - quoted_local_part=is_quoted_local_part, + quoted_local_part=revalidate_quoted, strict=strict, ) except EmailSyntaxError as e: raise EmailSyntaxError("After Unicode normalization: " + str(e)) from e - ret.local_part = normalized_local_part + ret.local_part = normalized_local_part_info["local_part"] ret.ascii_local_part = normalized_local_part_info["ascii_local_part"] ret.smtputf8 = normalized_local_part_info["smtputf8"]