diff --git a/src/validators/email.py b/src/validators/email.py index 46cc5946..d5c1e29d 100644 --- a/src/validators/email.py +++ b/src/validators/email.py @@ -5,6 +5,7 @@ # local from .hostname import hostname +from .ip_address import ipv6 from .utils import validator @@ -39,7 +40,8 @@ def email( value: eMail string to validate. ipv6_address: - When the domain part is an IPv6 address. + When the domain part is an IPv6 address, including an RFC 5321 + address literal such as `[IPv6:2001:db8::1]`. ipv4_address: When the domain part is an IPv4 address. simple_host: @@ -67,7 +69,12 @@ def email( if ipv6_address or ipv4_address: if domain_part.startswith("[") and domain_part.endswith("]"): # ref: RFC 5321 - domain_part = domain_part.lstrip("[").rstrip("]") + if domain_part[1:6].lower() == "ipv6:": + domain_part = domain_part[6:-1] + if not ipv6_address or "%" in domain_part or not ipv6(domain_part, cidr=False): + return False + else: + domain_part = domain_part.lstrip("[").rstrip("]") else: return False diff --git a/tests/test_email.py b/tests/test_email.py index 9b82eb46..f11f4452 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -72,3 +72,39 @@ def test_rejects_newline_after_local_part(local_part: str): def test_returns_failed_validation_on_idna_mapped_lookalike_domain(domain_part: str): """Test returns failed validation on email domains that only IDNA 2003 mapping makes valid.""" assert isinstance(email(f"user@{domain_part}"), ValidationError) + + +@pytest.mark.parametrize( + "address", + ["2001:db8::1", "2001:db8:0:0:0:0:0:1", "::ffff:192.0.2.1"], +) +def test_accepts_tagged_ipv6_literal(address: str): + """Accept RFC 5321 address literals when IPv6 addresses are enabled.""" + assert email(f"user@[IPv6:{address}]", ipv6_address=True) is True + assert email(f"user@[ipv6:{address}]", ipv6_address=True) is True + assert isinstance(email(f"user@[IPv6:{address}]"), ValidationError) + assert isinstance(email(f"user@[IPv6:{address}]", ipv4_address=True), ValidationError) + + +@pytest.mark.parametrize( + "address", ["", "example.com", "192.0.2.1", "2001:db8::xyz", "fe80::1%eth0"] +) +def test_rejects_invalid_tagged_ipv6_literal(address: str): + """An IPv6 tag must identify a valid IPv6 address, never a hostname or IPv4.""" + assert isinstance(email(f"user@[IPv6:{address}]", ipv6_address=True), ValidationError) + assert isinstance( + email(f"user@[IPv6:{address}]", ipv6_address=True, ipv4_address=True), ValidationError + ) + + +def test_rejects_extra_brackets_around_tagged_ipv6_literal(): + """Do not strip repeated brackets while interpreting an IPv6 tag.""" + assert isinstance(email("user@[[IPv6:2001:db8::1]]", ipv6_address=True), ValidationError) + + +def test_preserves_legacy_email_address_literals(): + """Keep bare IPv6 and IPv4 literal support compatible.""" + assert email("user@[2001:db8::1]", ipv6_address=True) is True + assert email("user@[192.0.2.1]", ipv4_address=True) is True + assert email("user@[IPv6:2001:db8::1]", ipv6_address=True, ipv4_address=True) is True + assert isinstance(email("invalid local@[IPv6:2001:db8::1]", ipv6_address=True), ValidationError)