Skip to content
Merged
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
70 changes: 13 additions & 57 deletions src/core/email/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,6 @@ constexpr auto is_qtext_smtp(const unsigned char character) -> bool {
(character >= 93 && character <= 126);
}

// RFC 5321 §4.1.3: dcontent = %d33-90 / %d94-126
constexpr auto is_dcontent(const unsigned char character) -> bool {
return (character >= 33 && character <= 90) ||
(character >= 94 && character <= 126);
}

// RFC 5321 §4.1.2: Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
// RFC 5321 §4.1.3: Standardized-tag = Ldh-str
constexpr auto is_ldh_str(const std::string_view value) -> bool {
if (value.empty() || !sourcemeta::core::is_alphanum(value.back())) {
return false;
}
for (std::string_view::size_type position{0}; position + 1 < value.size();
position += 1) {
const auto character{value[position]};
if (!sourcemeta::core::is_alphanum(character) && character != '-') {
return false;
}
}
return true;
}

// RFC 5321 §4.1.3: Snum = 1*3DIGIT ; representing a decimal integer
// value in the range 0 through 255. Leading zeros are permitted, unlike
// the RFC 3986 dec-octet that backs is_ipv4
Expand Down Expand Up @@ -122,30 +100,8 @@ constexpr auto matches_ipv6_tag(const std::string_view value) -> bool {
value[4] == ':';
}

// RFC 5321 §4.1.3: General-address-literal = Standardized-tag ":" 1*dcontent
constexpr auto is_general_address_literal(const std::string_view value)
-> bool {
const auto colon_position{value.find(':')};
if (colon_position == std::string_view::npos) {
return false;
}
if (!is_ldh_str(value.substr(0, colon_position))) {
return false;
}
const auto content{value.substr(colon_position + 1)};
if (content.empty()) {
return false;
}
for (const auto character : content) {
if (!is_dcontent(static_cast<unsigned char>(character))) {
return false;
}
}
return true;
}

// RFC 5321 §4.1.3: validate the address-literal payload (between "[" and "]")
// as IPv6, IPv4, or General-address-literal. Always ASCII; no IDNA applies
// as IPv6 or IPv4. Always ASCII; no IDNA applies
inline auto is_address_literal(const std::string_view domain) -> bool {
if (domain.back() != ']') {
return false;
Expand All @@ -155,18 +111,18 @@ inline auto is_address_literal(const std::string_view domain) -> bool {
return false;
}
const auto inner{domain.substr(1, domain.size() - 2)};
// RFC 5321 §4.1.3: IPv6-address-literal = "IPv6:" IPv6-addr
if (matches_ipv6_tag(inner) && sourcemeta::core::is_ipv6(inner.substr(5))) {
return true;
}
// RFC 5234 §3.2: ABNF alternatives are unordered. A failed IPv6 match
// falls through to IPv4 or General-address-literal.
// RFC 5321 §4.1.3: IPv4-address-literal has no ":";
// General-address-literal requires ":"
if (!inner.contains(':')) {
return is_ipv4_address_literal(inner);
}
return is_general_address_literal(inner);
// RFC 5321 §4.1.3: IPv6-address-literal = "IPv6:" IPv6-addr. The tag names
// the syntax that the rest of the literal follows, so a payload that is not
// an address is turned down rather than read as general content, which
// would otherwise leave the IPv6 form unable to ever fail
if (matches_ipv6_tag(inner)) {
return sourcemeta::core::is_ipv6(inner.substr(5));
}
// RFC 5321 §4.1.3: a Standardized-tag must be registered with IANA before
// being used, and the registry carries the IPv6 tag alone, so the general
// form admits nothing that the branch above does not already cover. What
// remains is the IPv4 form, which has no colon to begin with
return !inner.contains(':') && is_ipv4_address_literal(inner);
}

// RFC 3986 §2.1: "For consistency, URI producers and normalizers should use
Expand Down
13 changes: 7 additions & 6 deletions src/core/email/include/sourcemeta/core/email.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ namespace sourcemeta::core {
/// prose specifies the IPv6 syntax as that of RFC 4291, while the `IPv6-addr`
/// ABNF in the same section is stricter and conflicts with it, so the prose is
/// followed. A bracketed `[IPv6:...]` whose body is not a valid address is
/// still accepted, because Section 4.1.3 also permits any
/// General-address-literal (a registered tag, a colon, and content) and ABNF
/// alternatives are unordered.
/// turned down rather than read as a General-address-literal, since the tag
/// names the syntax that has to follow it. That general form is not accepted
/// under any other tag either, as Section 4.1.3 requires a Standardized-tag to
/// be registered with IANA before being used and that registry carries the
/// IPv6 tag alone.
///
/// For example:
///
Expand Down Expand Up @@ -108,9 +110,8 @@ auto is_idn_email_uts46(const std::string_view value) -> bool;
/// Produce the domain half of a valid RFC 5321 `Mailbox`, returning an empty
/// view when the input is not one, which no valid mailbox can otherwise yield
/// since a domain is never empty. The separator is located by parsing the
/// address, since neither the first nor the last at sign reliably marks it:
/// RFC 5321 Section 4.1.2 admits one inside a quoted local part, and Section
/// 4.1.3 admits one inside the content of a General-address-literal. The
/// address, since the first at sign does not reliably mark it: RFC 5321
/// Section 4.1.2 admits one inside a quoted local part. The
/// result borrows from the input and keeps its spelling, so a caller comparing
/// domains applies the RFC 5321 Section 2.4 case insensitivity itself. Only
/// the ASCII grammar is accepted, so an internationalized address per RFC 6531
Expand Down
8 changes: 4 additions & 4 deletions test/email/email_domain_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ TEST(email_domain_quoted_local_part_carrying_an_at_sign) {
"example.org");
}

// RFC 5321 §4.1.3: General-address-literal content is 1*dcontent, and
// dcontent = %d33-90 / %d94-126 admits the at sign, so the separator is not
// the last one in the string either
// RFC 5321 §4.1.3: no address-literal carries an at sign, as the only tag the
// registry holds is the IPv6 one and no address spells one, so a literal that
// does is not a Mailbox and reports no domain at all
TEST(email_domain_address_literal_carrying_an_at_sign) {
EXPECT_EQ(sourcemeta::core::email_domain("user@[tag:a@b]"), "[tag:a@b]");
EXPECT_TRUE(sourcemeta::core::email_domain("user@[tag:a@b]").empty());
}

TEST(email_domain_ipv4_address_literal) {
Expand Down
Loading
Loading