// Copyright (c) 2026 colmugx
// SPDX-License-Identifier: Apache-2.0
///|
/// RFC 5321 4.5.3.1 fixes the local part at 64 octets: every implementation
/// must accept that length and must not exceed it.
let format_local_octet_limit : Int32 = 64
///|
/// `email` (RFC 5321 `Mailbox`): one ASCII `Local-part "@" Domain`, where the
/// local part is an RFC 5322 `dot-atom` or `quoted-string`. A display name, a
/// comment, an unquoted space or a comma-separated list fails, because all of
/// those are header syntax rather than a mailbox.
fn format_email(value : String) -> Bool {
format_mailbox(value[:].to_array(), ascii_only=true)
}
///|
/// `idn-email` (RFC 6531 internationalized `Mailbox`): the same grammar with
/// `atext`, `qtext` and quoted pairs extended by `UTF8-non-ascii`, so a Unicode
/// local part and a U-label domain are valid. Labels are checked in U-label
/// space, so IDNA2008 derived properties, ContextJ and the Bidi rule are not
/// asserted here.
fn format_idn_email(value : String) -> Bool {
format_mailbox(value[:].to_array(), ascii_only=false)
}
///|
fn format_mailbox(chars : Array[Char], ascii_only~ : Bool) -> Bool {
guard format_mailbox_separator(chars) is Some(separator) else { return false }
if separator == 0 || separator == chars.length() - 1 {
return false
}
if format_octets(chars, 0, separator) > format_local_octet_limit {
return false
}
if !format_local_part(chars, 0, separator, ascii_only~) {
return false
}
format_mailbox_domain(chars, separator + 1, chars.length(), ascii_only~)
}
///|
/// Position of the `@` that separates the local part from the domain. A quoted
/// local part may contain `@`, so the scan tracks quotes and quoted pairs; a
/// second separator, or a quote left open, yields `None`.
fn format_mailbox_separator(chars : Array[Char]) -> Int32? {
let mut separator : Int32? = None
let mut index = 0
let mut quoted = false
let mut escaped = false
while index < chars.length() {
let ch = chars[index]
if escaped {
escaped = false
} else if quoted && ch == '\\' {
escaped = true
} else if ch == '"' {
quoted = !quoted
} else if ch == '@' && !quoted {
if separator is Some(_) {
return None
}
separator = Some(index)
}
index = index + 1
}
guard !quoted && !escaped else { return None }
separator
}
///|
fn format_local_part(
chars : Array[Char],
start : Int32,
end : Int32,
ascii_only~ : Bool,
) -> Bool {
if chars[start] == '"' {
format_quoted_local(chars, start, end, ascii_only~)
} else {
format_dot_atom(chars, start, end, ascii_only~)
}
}
///|
/// RFC 5322 3.2.4 `quoted-string`, including both quotes: the closing quote has
/// to be the last character of the local part, so trailing text fails.
fn format_quoted_local(
chars : Array[Char],
start : Int32,
end : Int32,
ascii_only~ : Bool,
) -> Bool {
if end - start < 2 {
return false
}
let mut index = start + 1
while index < end {
let ch = chars[index]
if ch == '"' {
return index == end - 1
}
if ch == '\\' {
if index + 1 >= end || !format_quoted_pair(chars[index + 1], ascii_only~) {
return false
}
index = index + 2
} else if format_qtext(ch, ascii_only~) {
index = index + 1
} else {
return false
}
}
false
}
///|
/// RFC 5322 3.2.1 `quoted-pair`: a backslash followed by one printable
/// character. RFC 6531 extends the operand to `UTF8-non-ascii`.
fn format_quoted_pair(ch : Char, ascii_only~ : Bool) -> Bool {
if ch is (' '..<='~') {
return true
}
!ascii_only && ch.to_int() >= 0x80
}
///|
/// RFC 5322 3.2.4 `qtext`: printable ASCII without the quote and the backslash,
/// which the grammar reaches through dedicated productions.
fn format_qtext(ch : Char, ascii_only~ : Bool) -> Bool {
if ch is (' '..<='!' | '#'..<='[' | ']'..<='~') {
return true
}
!ascii_only && ch.to_int() >= 0x80
}
///|
/// RFC 5322 3.2.3 `atext`, extended by RFC 6531 `UTF8-non-ascii` outside ASCII
/// mode. Every remaining printable ASCII character is a `special`, so an
/// unquoted local part can never carry one.
fn format_atext(ch : Char, ascii_only~ : Bool) -> Bool {
if format_is_alnum(ch) {
return true
}
if !ascii_only && ch.to_int() >= 0x80 {
return true
}
ch
is ('!'
| '#'
| '$'
| '%'
| '&'
| '\''
| '*'
| '+'
| '-'
| '/'
| '='
| '?'
| '^'
| '_'
| '`'
| '{'
| '|'
| '}'
| '~')
}
///|
/// RFC 5322 3.2.3 `dot-atom`: `atext` runs separated by single dots, so a
/// leading, trailing or doubled dot fails.
fn format_dot_atom(
chars : Array[Char],
start : Int32,
end : Int32,
ascii_only~ : Bool,
) -> Bool {
let mut expect_atext = true
for index in start.. Bool {
if end <= start {
return false
}
if chars[start] == '[' {
format_address_literal(chars, start, end)
} else {
format_domain_labels(chars, start, end, ascii_only~)
}
}
///|
/// RFC 5321 4.1.3 `address-literal`: `[` IPv4 `]` or `[` `IPv6:` IPv6 `]`.
/// The tag is case-insensitive, and a bare bracketed string that is neither an
/// IPv4 nor a tagged IPv6 literal fails.
fn format_address_literal(
chars : Array[Char],
start : Int32,
end : Int32,
) -> Bool {
if end - start < 4 || chars[end - 1] != ']' {
return false
}
let text = format_text(chars, start + 1, end - 1)
if text[:5].to_lower() == "ipv6:" {
format_ipv6(text[5:].to_owned())
} else {
format_ipv4(text)
}
}
///|
/// Octet length of `[start, end)`, which is what the RFC 5321 size limits are
/// stated in; a U-label cannot be measured in characters.
fn format_octets(chars : Array[Char], start : Int32, end : Int32) -> Int32 {
let mut octets = 0
for index in start.. String {
let out : Array[Char] = []
for index in start..