import pytest
from emval import ValidatedEmail, validate_email
@pytest.mark.parametrize(
"email_input,output",
[
(
"Abc@example.tld",
ValidatedEmail(
local_part="Abc",
ascii_domain="example.tld",
domain_name="example.tld",
normalized="Abc@example.tld",
ascii_email="Abc@example.tld",
original="Abc@example.tld",
),
),
(
"Abc.123@test-example.com",
ValidatedEmail(
local_part="Abc.123",
ascii_domain="test-example.com",
domain_name="test-example.com",
normalized="Abc.123@test-example.com",
ascii_email="Abc.123@test-example.com",
original="Abc.123@test-example.com",
),
),
(
"user+mailbox/department=shipping@example.tld",
ValidatedEmail(
local_part="user+mailbox/department=shipping",
ascii_domain="example.tld",
domain_name="example.tld",
normalized="user+mailbox/department=shipping@example.tld",
ascii_email="user+mailbox/department=shipping@example.tld",
original="user+mailbox/department=shipping@example.tld",
),
),
(
"!#$%&'*+-/=?^_`.{|}~@example.tld",
ValidatedEmail(
local_part="!#$%&'*+-/=?^_`.{|}~",
ascii_domain="example.tld",
domain_name="example.tld",
normalized="!#$%&'*+-/=?^_`.{|}~@example.tld",
ascii_email="!#$%&'*+-/=?^_`.{|}~@example.tld",
original="!#$%&'*+-/=?^_`.{|}~@example.tld",
),
),
(
"jeff@臺網中心.tw",
ValidatedEmail(
local_part="jeff",
ascii_domain="xn--fiqq24b10vi0d.tw",
domain_name="臺網中心.tw",
normalized="jeff@臺網中心.tw",
ascii_email="jeff@xn--fiqq24b10vi0d.tw",
original="jeff@臺網中心.tw",
),
),
(
'"quoted local part"@example.org',
ValidatedEmail(
local_part='"quoted local part"',
ascii_domain="example.org",
domain_name="example.org",
normalized='"quoted local part"@example.org',
ascii_email='"quoted local part"@example.org',
original='"quoted local part"@example.org',
),
),
(
'"de-quoted.local.part"@example.org',
ValidatedEmail(
local_part="de-quoted.local.part",
ascii_domain="example.org",
domain_name="example.org",
normalized="de-quoted.local.part@example.org",
ascii_email="de-quoted.local.part@example.org",
original='"de-quoted.local.part"@example.org',
),
),
],
)
def test_email_valid(email_input: str, output: ValidatedEmail) -> None:
validated_email = validate_email(
email_input,
deliverable_address=False,
allow_smtputf8=False,
allow_quoted_local=True,
)
assert validated_email == output
assert (
validate_email(
email_input,
deliverable_address=False,
allow_smtputf8=True,
allow_quoted_local=True,
)
== output
)
@pytest.mark.parametrize(
"email_input,output",
[
(
"伊昭傑@郵件.商務",
ValidatedEmail(
local_part="伊昭傑",
ascii_domain="xn--5nqv22n.xn--lhr59c",
domain_name="郵件.商務",
normalized="伊昭傑@郵件.商務",
original="伊昭傑@郵件.商務",
),
),
(
"राम@मोहन.ईन्फो",
ValidatedEmail(
local_part="राम",
ascii_domain="xn--l2bl7a9d.xn--o1b8dj2ki",
domain_name="मोहन.ईन्फो",
normalized="राम@मोहन.ईन्फो",
original="राम@मोहन.ईन्फो",
),
),
(
"юзер@екзампл.ком",
ValidatedEmail(
local_part="юзер",
ascii_domain="xn--80ajglhfv.xn--j1aef",
domain_name="екзампл.ком",
normalized="юзер@екзампл.ком",
original="юзер@екзампл.ком",
),
),
(
"θσερ@εχαμπλε.ψομ",
ValidatedEmail(
local_part="θσερ",
ascii_domain="xn--mxahbxey0c.xn--xxaf0a",
domain_name="εχαμπλε.ψομ",
normalized="θσερ@εχαμπλε.ψομ",
original="θσερ@εχαμπλε.ψομ",
),
),
(
"葉士豪@臺網中心.tw",
ValidatedEmail(
local_part="葉士豪",
ascii_domain="xn--fiqq24b10vi0d.tw",
domain_name="臺網中心.tw",
normalized="葉士豪@臺網中心.tw",
original="葉士豪@臺網中心.tw",
),
),
(
"葉士豪@臺網中心.台灣",
ValidatedEmail(
local_part="葉士豪",
ascii_domain="xn--fiqq24b10vi0d.xn--kpry57d",
domain_name="臺網中心.台灣",
normalized="葉士豪@臺網中心.台灣",
original="葉士豪@臺網中心.台灣",
),
),
(
"jeff葉@臺網中心.tw",
ValidatedEmail(
local_part="jeff葉",
ascii_domain="xn--fiqq24b10vi0d.tw",
domain_name="臺網中心.tw",
normalized="jeff葉@臺網中心.tw",
original="jeff葉@臺網中心.tw",
),
),
(
"ñoñó@example.tld",
ValidatedEmail(
local_part="ñoñó",
ascii_domain="example.tld",
domain_name="example.tld",
normalized="ñoñó@example.tld",
original="ñoñó@example.tld",
),
),
(
"我買@example.tld",
ValidatedEmail(
local_part="我買",
ascii_domain="example.tld",
domain_name="example.tld",
normalized="我買@example.tld",
original="我買@example.tld",
),
),
(
"甲斐黒川日本@example.tld",
ValidatedEmail(
local_part="甲斐黒川日本",
ascii_domain="example.tld",
domain_name="example.tld",
normalized="甲斐黒川日本@example.tld",
original="甲斐黒川日本@example.tld",
),
),
(
"чебурашкаящик-с-апельсинами.рф@example.tld",
ValidatedEmail(
local_part="чебурашкаящик-с-апельсинами.рф",
ascii_domain="example.tld",
domain_name="example.tld",
normalized="чебурашкаящик-с-апельсинами.рф@example.tld",
original="чебурашкаящик-с-апельсинами.рф@example.tld",
),
),
(
"उदाहरण.परीक्ष@domain.with.idn.tld",
ValidatedEmail(
local_part="उदाहरण.परीक्ष",
ascii_domain="domain.with.idn.tld",
domain_name="domain.with.idn.tld",
normalized="उदाहरण.परीक्ष@domain.with.idn.tld",
original="उदाहरण.परीक्ष@domain.with.idn.tld",
),
),
(
"ιωάννης@εεττ.gr",
ValidatedEmail(
local_part="ιωάννης",
ascii_domain="xn--qxaa9ba.gr",
domain_name="εεττ.gr",
normalized="ιωάννης@εεττ.gr",
original="ιωάννης@εεττ.gr",
),
),
],
)
def test_email_valid_intl_local_part(email_input: str, output: ValidatedEmail) -> None:
assert (
validate_email(
email_input,
deliverable_address=False,
allow_smtputf8=True,
allow_quoted_local=True,
)
== output
)
with pytest.raises(SyntaxError) as exc_info:
validate_email(
email_input,
deliverable_address=False,
allow_smtputf8=False,
allow_quoted_local=True,
)
assert str(exc_info.value) == (
"Invalid Local Part: Internationalized characters before the '@' sign are not supported."
)
@pytest.mark.parametrize(
"email_input,normalized_local_part",
[
(
'"unnecessarily.quoted.local.part"@example.com',
"unnecessarily.quoted.local.part",
),
('"quoted..local.part"@example.com', '"quoted..local.part"'),
('"quoted.with.at@"@example.com', '"quoted.with.at@"'),
('"quoted with space"@example.com', '"quoted with space"'),
('"quoted.with.dquote\\""@example.com', '"quoted.with.dquote\\""'),
(
'"unnecessarily.quoted.with.unicode.λ"@example.com',
"unnecessarily.quoted.with.unicode.λ",
),
('"quoted.with..unicode.λ"@example.com', '"quoted.with..unicode.λ"'),
(
'"quoted.with.extraneous.\\escape"@example.com',
"quoted.with.extraneous.escape",
),
],
)
def test_email_valid_only_if_quoted_local_part(
email_input: str, normalized_local_part: str
) -> None:
with pytest.raises(SyntaxError) as exc_info:
validate_email(email_input)
assert (
str(exc_info.value)
== "Invalid Local Part: Quoting the local part before the '@' sign is not permitted in this context."
)
validated = validate_email(
email_input,
allow_quoted_local=True,
deliverable_address=False,
)
assert validated.local_part == normalized_local_part
def test_domain_literal() -> None:
validated = validate_email(
"me@[127.0.0.1]", allow_domain_literal=True, deliverable_address=False
)
assert validated.domain_name == "[127.0.0.1]"
assert repr(validated.domain_address) == "IPv4Address('127.0.0.1')"
validated = validate_email(
"me@[IPv6:::1]", allow_domain_literal=True, deliverable_address=False
)
assert validated.domain_name == "[IPv6:::1]"
assert repr(validated.domain_address) == "IPv6Address('::1')"
validated = validate_email(
"me@[IPv6:0000:0000:0000:0000:0000:0000:0000:0001]",
allow_domain_literal=True,
deliverable_address=False,
)
assert validated.domain_name == "[IPv6:::1]"
assert repr(validated.domain_address) == "IPv6Address('::1')"
@pytest.mark.parametrize(
"email_input,error_msg",
[
("hello.world", "Invalid Email Address: Missing an '@' sign."),
(
"my@localhost",
"Invalid Domain: Must contain a period ('.') to be considered valid.",
),
(
"me@-leadingdash",
"Invalid Domain: A hyphen ('-') cannot immediately follow the '@' symbol.",
),
(
"me@-leadingdashfw",
"Invalid Domain: A hyphen ('-') cannot immediately follow the '@' symbol.",
),
(
"me@trailingdash-",
"Invalid Domain: A hyphen ('-') cannot appear at the end of the domain.",
),
(
"me@trailingdashfw-",
"Invalid Domain: A hyphen ('-') cannot appear at the end of the domain.",
),
(
"my@baddash.-.com",
"Invalid Email Address: A period ('.') and a hyphen ('-') cannot be adjacent in the email address.",
),
(
"my@baddash.-a.com",
"Invalid Email Address: A period ('.') and a hyphen ('-') cannot be adjacent in the email address.",
),
(
"my@baddash.b-.com",
"Invalid Email Address: A period ('.') and a hyphen ('-') cannot be adjacent in the email address.",
),
(
"my@baddashfw.-.com",
"Invalid Email Address: A period ('.') and a hyphen ('-') cannot be adjacent in the email address.",
),
(
"my@baddashfw.-a.com",
"Invalid Email Address: A period ('.') and a hyphen ('-') cannot be adjacent in the email address.",
),
(
"my@baddashfw.b-.com",
"Invalid Email Address: A period ('.') and a hyphen ('-') cannot be adjacent in the email address.",
),
(
"my@example.com\n",
"Invalid Domain: Contains invalid characters after '@' sign.",
),
(
"my@example\n.com",
"Invalid Domain: Contains invalid characters after '@' sign.",
),
(
"me@x!",
"Invalid Domain: Contains invalid characters after '@' sign.",
),
(
"me@x ",
"Invalid Domain: Contains invalid characters after '@' sign.",
),
(".leadingdot@domain.com", "Invalid Local Part: Cannot start with a period."),
(
"twodots..here@domain.com",
"Invalid Email Address: Two periods ('.') cannot be adjacent in the email address.",
),
(
"trailingdot.@domain.email",
"Invalid Local Part: A period cannot immediately precede the '@' sign.",
),
(
"me@⒈wouldbeinvalid.com",
"Invalid Domain: Contains invalid characters after '@' sign post Unicode normalization.",
),
(
"me@\u037e.com",
"Invalid Domain: Contains invalid characters after Unicode normalization.",
),
(
"me@\u1fef.com",
"Invalid Domain: Contains invalid characters after Unicode normalization.",
),
(
"@example.com",
"Invalid Local Part: The part before the '@' sign cannot be empty.",
),
(
"white space@test",
"Invalid Local Part: contains invalid characters before the '@' sign.",
),
(
"test@white space",
"Invalid Domain: Contains invalid characters after '@' sign.",
),
(
"\nmy@example.com",
"Invalid Local Part: contains invalid characters before the '@' sign.",
),
(
"m\ny@example.com",
"Invalid Local Part: contains invalid characters before the '@' sign.",
),
(
"my\n@example.com",
"Invalid Local Part: contains invalid characters before the '@' sign.",
),
("test@\n", "Invalid Domain: Contains invalid characters after '@' sign."),
(
'bad"quotes"@example.com',
"Invalid Local Part: contains invalid characters before the '@' sign.",
),
(
'obsolete."quoted".atom@example.com',
"Invalid Local Part: contains invalid characters before the '@' sign.",
),
(
"11111111112222222222333333333344444444445555555555666666666677777@example.com",
"Invalid Local Part: The part before the '@' sign exceeds the maximum length (64 chars).",
),
(
"111111111122222222223333333333444444444455555555556666666666777777@example.com",
"Invalid Local Part: The part before the '@' sign exceeds the maximum length (64 chars).",
),
(
"\ufb2c111111122222222223333333333444444444455555555556666666666777777@example.com",
"Invalid Local Part: The part before the '@' sign exceeds the maximum length (64 chars).",
),
(
"me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444444445555555555.com",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444.com",
"Invalid Domain: Contains invalid characters after '@' sign post Unicode normalization.",
),
(
"me@\ufb2c1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444.com",
"Invalid Domain: Contains invalid characters after '@' sign post Unicode normalization.",
),
(
"meme@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.com",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"my.long.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"my.long.address@\ufb2c111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444.info",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"my.\u0073\u0323\u0307.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"my.\ufb2c.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info",
"Invalid Email Address: The email exceeds the maximum length (254 chars).",
),
(
"me@bad-tld-1",
"Invalid Domain: Must contain a period ('.') to be considered valid.",
),
(
"me@bad.tld-2",
"Invalid domain: The part after the '@' sign does not belong to a valid top-level domain (TLD).",
),
(
"me@xn--0.tld",
"Invalid Domain: Contains invalid characters after '@' sign post Unicode normalization.",
),
(
"me@yy--0.tld",
"Invalid Domain: Two letters followed by two dashes ('--') are not allowed immediately after the '@' sign or a period.",
),
(
"me@yy--0.tld",
"Invalid Domain: Two letters followed by two dashes ('--') are not allowed immediately after the '@' sign or a period.",
),
(
"me@[127.0.0.999]",
"Invalid Domain: The address in brackets following the '@' sign is not a valid IP address.",
),
(
"me@[IPv6:::G]",
"Invalid Domain: The IPv6 address in brackets following the '@' symbol is not valid.",
),
(
"me@[tag:text]",
"Invalid Domain: The address in brackets following the '@' sign is not a valid IP address.",
),
(
"me@[untaggedtext]",
"Invalid Domain: The address in brackets following the '@' sign is not a valid IP address.",
),
(
"me@[tag:invalid space]",
"Invalid Domain: The address in brackets following the '@' sign is not a valid IP address.",
),
],
)
def test_email_invalid_syntax(email_input: str, error_msg: str) -> None:
with pytest.raises((SyntaxError, ValueError)) as exc_info:
validate_email(email_input, allow_smtputf8=True, allow_domain_literal=True)
assert str(exc_info.value) == error_msg
@pytest.mark.parametrize(
"email_input",
[
("me@anything.arpa"),
("me@valid.invalid"),
("me@link.local"),
("me@host.localhost"),
("me@onion.onion.onion"),
("me@test.test.test"),
],
)
def test_email_invalid_reserved_domain(email_input: str) -> None:
with pytest.raises(SyntaxError) as exc_info:
validate_email(email_input)
assert (
"Invalid Domain: The part after the '@' sign is a reserved or special-use domain that cannot be used."
in str(exc_info.value)
)
@pytest.mark.parametrize(
("s", "expected_error"),
[
("\u2028", "LINE SEPARATOR"), ("\u2029", "PARAGRAPH SEPARATOR"), ("\u0300", "COMBINING GRAVE ACCENT"), ("\u009c", "U+009C"), ("\u200b", "ZERO WIDTH SPACE"), (
"\u202dforward-\u202ereversed",
"LEFT-TO-RIGHT OVERRIDE, RIGHT-TO-LEFT OVERRIDE",
), ("\ue000", "U+E000"), ("\U0010fdef", "U+0010FDEF"), ("\ufdef", "U+FDEF"), ],
)
def test_email_unsafe_character(s: str, expected_error: str) -> None:
with pytest.raises(SyntaxError) as exc_info:
validate_email(s + "@test")
assert (
str(exc_info.value)
== f"Invalid Email Address: contains invalid characters: {expected_error}."
)
with pytest.raises(SyntaxError) as exc_info:
validate_email("test@" + s)
assert "Invalid Email Address: contains invalid characters:" in str(exc_info.value)
@pytest.mark.parametrize(
("email_input", "expected_error"),
[
(
"λambdaツ@test",
"Invalid Local Part: Internationalized characters before the '@' sign are not supported.",
),
(
'"quoted.with..unicode.λ"@example.com',
"Invalid Local Part: Internationalized characters before the '@' sign are not supported.",
),
],
)
def test_email_invalid_character_smtputf8_off(
email_input: str, expected_error: str
) -> None:
with pytest.raises(SyntaxError) as exc_info:
validate_email(email_input, allow_smtputf8=False, allow_quoted_local=True)
assert str(exc_info.value) == expected_error
def test_email_empty_local() -> None:
validate_email("@example.com", allow_empty_local=True, deliverable_address=False)
validate_email(
'""@example.com',
allow_empty_local=True,
allow_quoted_local=True,
deliverable_address=False,
)
def test_case_insensitive_mailbox_name() -> None:
assert (
validate_email("POSTMASTER@example.com", deliverable_address=False).normalized
== "postmaster@example.com"
)
assert (
validate_email(
"NOT-POSTMASTER@example.com", deliverable_address=False
).normalized
== "NOT-POSTMASTER@example.com"
)
@pytest.mark.parametrize(
"domain,expected_response",
[
(
"test@gmail.com",
True,
),
(
"test@pages.github.com",
True,
),
],
)
def test_deliverability_found(domain: str, expected_response: bool) -> None:
response = validate_email(domain, deliverable_address=True)
assert response.is_deliverable == expected_response
@pytest.mark.parametrize(
("domain", "error"),
[
(
"test@xkxufoekjvjfjeodlfmdfjcu.com",
"Invalid Domain: No MX, A, or AAAA records found for domain.",
),
(
"test@example.com",
"Invalid Domain: The domain does not accept email due to a null MX record, indicating it is not configured to receive emails.",
),
(
"test@g.mail.com",
"Invalid Domain: No MX, A, or AAAA records found for domain.",
),
(
"test@justtxt.joshdata.me",
"Invalid Domain: No MX, A, or AAAA records found for domain.",
),
],
)
def test_deliverability_fails(domain: str, error: str) -> None:
with pytest.raises(SyntaxError, match=error):
validate_email(domain, deliverable_address=True)