Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/validators/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

# local
from .hostname import hostname
from .ip_address import ipv6
from .utils import validator


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
36 changes: 36 additions & 0 deletions tests/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)