From 37ddfb2bd2f411fe2b1a03bb5b9d8088e3ed0f94 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Thu, 1 Oct 2026 14:01:31 +0900 Subject: [PATCH] Reject IPv6 domain literals with a zone index ipaddress.IPv6Address accepts '%scope' since Python 3.9, but RFC 5321's IPv6-addr has no zone index, so 'me@[IPv6:fe80::1%eth0]' was accepted. --- email_validator/syntax.py | 4 ++++ tests/test_syntax.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/email_validator/syntax.py b/email_validator/syntax.py index 0a06840..51c022a 100644 --- a/email_validator/syntax.py +++ b/email_validator/syntax.py @@ -791,6 +791,10 @@ def validate_email_domain_literal(domain_literal: str) -> DomainLiteralValidatio except ValueError as e: raise EmailSyntaxError(f"The IPv6 address in brackets after the @-sign is not valid ({e}).") from e + # Python accepts a zone index ("%eth0") but RFC 5321 4.1.3 does not. + if addr.scope_id is not None: + raise EmailSyntaxError("The IPv6 address in brackets after the @-sign is not valid (It has a zone index).") + # Return the IPv6Address object and construct a normalized # domain literal. return { diff --git a/tests/test_syntax.py b/tests/test_syntax.py index a322f4e..2082f2c 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -407,6 +407,16 @@ def test_domain_literal() -> None: assert validated.domain == "[IPv6:::1]" assert repr(validated.domain_address) == "IPv6Address('::1')" + # Check that an IPv4 address in an IPv6 address is accepted. + validated = validate_email("me@[IPv6:::ffff:1.2.3.4]", allow_domain_literal=True) + assert validated.domain == "[IPv6:::ffff:1.2.3.4]" + + # Check that IPv6 zone indexes are rejected. + for email_input in ("me@[IPv6:fe80::1%eth0]", "me@[IPv6:::1%]]"): + with pytest.raises(EmailSyntaxError) as exc_info: + validate_email(email_input, allow_domain_literal=True) + assert str(exc_info.value) == "The IPv6 address in brackets after the @-sign is not valid (It has a zone index)." + @pytest.mark.parametrize( 'email_input,error_msg',