Skip to content
Open
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
253 changes: 253 additions & 0 deletions test/email/idn_email_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,256 @@ TEST(valid_ulabel_local_and_alabel_domain) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("\xce\xb1@xn--nxasmq6b.com"));
EXPECT_FALSE(sourcemeta::core::is_email("\xce\xb1@xn--nxasmq6b.com"));
}

// --- RFC 5321 §4.1.3 address-literal: Snum ---------------------------------

// RFC 5321 §4.1.3: Snum = 1*3DIGIT "representing a decimal integer value in
// the range 0 through 255". The rule constrains the VALUE, not the digit
// count, so leading zeros are inside the grammar. This is the documented
// difference from the RFC 3986 dec-octet that backs is_ipv4, which forbids
// them - so an implementation that reuses its ipv4 checker here is wrong.
TEST(valid_address_literal_snum_leading_zero) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[01.0.0.1]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[01.0.0.1]"));
}

TEST(valid_address_literal_snum_leading_zero_last_octet) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[0.0.0.01]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[0.0.0.01]"));
}

TEST(valid_address_literal_snum_leading_zeros_every_octet) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[001.002.003.004]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[001.002.003.004]"));
}

TEST(valid_address_literal_snum_padded_zero_octets) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[000.000.000.000]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[000.000.000.000]"));
}

TEST(valid_address_literal_snum_maximum) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[255.255.255.255]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[255.255.255.255]"));
}

// One over the range the comment in the ABNF gives
TEST(invalid_address_literal_snum_over_maximum) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[256.0.0.1]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[256.0.0.1]"));
}

// 1*3DIGIT caps the digit count at three even when the value fits
TEST(invalid_address_literal_snum_four_digits) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[0255.0.0.1]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[0255.0.0.1]"));
}

// RFC 5321 §4.1.3: IPv4-address-literal = Snum 3("." Snum), so exactly four
TEST(invalid_address_literal_three_octets) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[1.2.3]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[1.2.3]"));
}

TEST(invalid_address_literal_five_octets) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[1.2.3.4.5]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[1.2.3.4.5]"));
}

TEST(invalid_address_literal_trailing_dot) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[1.2.3.]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[1.2.3.]"));
}

TEST(invalid_address_literal_leading_dot) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[.1.2.3]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[.1.2.3]"));
}

TEST(invalid_address_literal_empty_octet) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[1..2.3]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[1..2.3]"));
}

// --- RFC 5321 §4.1.3 General-address-literal -------------------------------

// General-address-literal = Standardized-tag ":" 1*dcontent
TEST(valid_general_address_literal) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[unknown-tag:abc]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[unknown-tag:abc]"));
}

TEST(valid_general_address_literal_shortest) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[a:b]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[a:b]"));
}

// RFC 5321 §4.1.2: Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig constrains only
// the final character, so unlike sub-domain (Let-dig [Ldh-str]) a
// Standardized-tag may begin with a hyphen
TEST(valid_general_address_literal_leading_hyphen_tag) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[-tag:abc]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[-tag:abc]"));
}

TEST(invalid_general_address_literal_trailing_hyphen_tag) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[tag-:abc]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[tag-:abc]"));
}

// 1*dcontent requires at least one octet
TEST(invalid_general_address_literal_empty_content) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[tag:]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[tag:]"));
}

TEST(invalid_general_address_literal_empty_tag) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[:abc]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[:abc]"));
}

// RFC 5321 §4.1.3: dcontent = %d33-90 / %d94-126, which excludes SP, "[",
// "\" and "]"
TEST(invalid_general_address_literal_space_in_content) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[tag:ab c]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[tag:ab c]"));
}

TEST(invalid_general_address_literal_open_bracket_in_content) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[tag:ab[c]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[tag:ab[c]"));
}

// --- RFC 5321 §4.1.3 IPv6-address-literal ----------------------------------

// IPv6-comp permits an empty prefix and suffix around "::"
TEST(valid_address_literal_ipv6_fully_compressed) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[IPv6:::1]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[IPv6:::1]"));
}

// RFC 5234 §2.3: ABNF literal strings are case-insensitive, so the tag matches
// in any case. The lowercase form is already covered; this pins the uppercase.
TEST(valid_address_literal_ipv6_uppercase_tag) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("user@[IPV6:2001:db8::1]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[IPV6:2001:db8::1]"));
}

// IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::" [...] IPv4-address-literal
TEST(valid_address_literal_ipv6v4_comp) {
EXPECT_TRUE(
sourcemeta::core::is_idn_email("user@[IPv6:2001:db8::192.0.2.1]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[IPv6:2001:db8::192.0.2.1]"));
}

// IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
TEST(valid_address_literal_ipv6v4_full) {
EXPECT_TRUE(
sourcemeta::core::is_idn_email("user@[IPv6:1:2:3:4:5:6:1.2.3.4]"));
EXPECT_TRUE(sourcemeta::core::is_email("user@[IPv6:1:2:3:4:5:6:1.2.3.4]"));
}

TEST(invalid_address_literal_ipv6_empty) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[IPv6:]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[IPv6:]"));
}

// --- address-literal shape -------------------------------------------------

TEST(invalid_address_literal_empty) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[]"));
}

TEST(invalid_address_literal_unterminated) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[192.0.2.1"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[192.0.2.1"));
}

TEST(invalid_address_literal_doubled_brackets) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[[192.0.2.1]]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[[192.0.2.1]]"));
}

TEST(invalid_address_literal_followed_by_label) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[192.0.2.1].com"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[192.0.2.1].com"));
}

TEST(invalid_address_literal_trailing_space) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user@[192.0.2.1 ]"));
EXPECT_FALSE(sourcemeta::core::is_email("user@[192.0.2.1 ]"));
}

// --- the "@" delimiter -----------------------------------------------------

// RFC 5321 §4.1.2: Mailbox splits on U+0040. A fullwidth (U+FF20, EF BC A0) or
// small (U+FE6B, EF B9 AB) commercial at is an ordinary character, so a string
// carrying one instead of "@" has no delimiter at all. Both codepoints map to
// U+0040 in the UTS #46 table, but that mapping applies to the domain the
// split produces, so it cannot manufacture the delimiter.
TEST(invalid_fullwidth_commercial_at) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user\xef\xbc\xa0"
"example.com"));
EXPECT_FALSE(sourcemeta::core::is_email("user\xef\xbc\xa0"
"example.com"));
}

TEST(invalid_small_commercial_at) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("user\xef\xb9\xab"
"example.com"));
EXPECT_FALSE(sourcemeta::core::is_email("user\xef\xb9\xab"
"example.com"));
}

// --- local-part codepoints the ABNF admits ---------------------------------

// RFC 6531 §3.3 extends atext with UTF8-non-ascii and RFC 6532 §3.1 defines
// that as UTF8-2 / UTF8-3 / UTF8-4, with no codepoint-class filter. So a C1
// control, a noncharacter and a supplementary private-use codepoint are all
// inside the local-part grammar even though they are unusual.
// U+0085 NEXT LINE (C2 85)
TEST(valid_local_c1_control) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("\xc2\x85@example.com"));
EXPECT_FALSE(sourcemeta::core::is_email("\xc2\x85@example.com"));
}

// U+FFFE noncharacter (EF BF BE)
TEST(valid_local_noncharacter) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("\xef\xbf\xbe@example.com"));
EXPECT_FALSE(sourcemeta::core::is_email("\xef\xbf\xbe@example.com"));
}

// U+FDD0, the start of the noncharacter block (EF B7 90)
TEST(valid_local_noncharacter_block) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("\xef\xb7\x90@example.com"));
EXPECT_FALSE(sourcemeta::core::is_email("\xef\xb7\x90@example.com"));
}

// U+F0000, a supplementary private-use codepoint (F3 B0 80 80)
TEST(valid_local_supplementary_private_use) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("\xf3\xb0\x80\x80@example.com"));
EXPECT_FALSE(sourcemeta::core::is_email("\xf3\xb0\x80\x80@example.com"));
}

// --- Quoted-string structure -----------------------------------------------

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove section comments?


// RFC 5321 §4.1.2: Quoted-string = DQUOTE *QcontentSMTP DQUOTE, so the closing
// DQUOTE is mandatory
TEST(invalid_quoted_local_unterminated) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("\"unterminated@example.com"));
EXPECT_FALSE(sourcemeta::core::is_email("\"unterminated@example.com"));
}

// Local-part = Dot-string / Quoted-string, so a quoted string cannot be
// followed by more local-part text
TEST(invalid_quoted_local_trailing_text) {
EXPECT_FALSE(sourcemeta::core::is_idn_email("\"a\"b@example.com"));
EXPECT_FALSE(sourcemeta::core::is_email("\"a\"b@example.com"));
}

// quoted-pairSMTP = %d92 %d32-126, so a backslash may escape the DQUOTE
TEST(valid_quoted_local_escaped_dquote) {
EXPECT_TRUE(sourcemeta::core::is_idn_email("\"\\\"\"@example.com"));
EXPECT_TRUE(sourcemeta::core::is_email("\"\\\"\"@example.com"));
}
Loading