Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
// Every character class is hand-rolled rather than expressed as a regular
// expression. No regex crate is available here, and the only way to be sure
// three implementations agree on an edge case is for all three to make the
// same decision in the same place. The address is walked as a `Vec<char>` so
// that positions mean code points, exactly as they do in Python.
/// RFC 5321 caps the local part at 64 octets.
const MAX_LOCAL: usize = 64;
/// RFC 5321 caps a forward path at 256 octets including the angle brackets.
const MAX_TOTAL: usize = 254;
/// RFC 1035 caps a DNS label at 63 octets.
const MAX_LABEL: usize = 63;
/// The "atext" specials from RFC 5322, plus the dot handled separately below.
const LOCAL_SPECIALS: &str = "!#$%&'*+-/=?^_`{|}~";
fn is_digit(ch: char) -> bool {
('0'..='9').contains(&ch)
}
fn is_letter(ch: char) -> bool {
('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch)
}
fn is_letter_or_digit(ch: char) -> bool {
is_letter(ch) || is_digit(ch)
}
fn is_local_char(ch: char) -> bool {
is_letter_or_digit(ch) || LOCAL_SPECIALS.contains(ch)
}
/// Is this a plausible email address?
///
/// This is a deliberate, documented subset of RFC 5322, not an implementation
/// of it. The full grammar admits comments, folded whitespace, quoted strings
/// with embedded spaces and bracketed IP literals; almost nothing downstream
/// of a signup form can handle those, and accepting them would let addresses
/// through that the mail stack then rejects.
///
/// The only true validation of an email address is sending mail to it and
/// seeing the recipient act on it. Use this to catch typos at the keyboard,
/// then confirm by email. Never use it to decide that an address is real.
pub fn is_email(value: &str) -> bool {
let chars: Vec<char> = value.chars().collect();
if chars.is_empty() || chars.len() > MAX_TOTAL {
return false;
}
// Exactly one @: the last-@ split used by lenient parsers quietly accepts
// "a@b@c", which no MTA will route.
let mut at: Option<usize> = None;
for (i, ch) in chars.iter().enumerate() {
if *ch == '@' {
if at.is_some() {
return false;
}
at = Some(i);
}
}
let at = match at {
Some(i) => i,
None => return false,
};
if at == 0 || at == chars.len() - 1 {
return false;
}
is_local_part(&chars[..at]) && is_domain(&chars[at + 1..])
}
fn is_local_part(local: &[char]) -> bool {
if local.is_empty() || local.len() > MAX_LOCAL {
return false;
}
// A dot is a separator between atoms, so it cannot lead, trail or double up.
if local[0] == '.' || local[local.len() - 1] == '.' {
return false;
}
for (i, ch) in local.iter().enumerate() {
if *ch == '.' {
if local[i - 1] == '.' {
return false;
}
continue;
}
if !is_local_char(*ch) {
return false;
}
}
true
}
fn is_domain(domain: &[char]) -> bool {
// MAX_TOTAL already bounds this, but stating the domain limit separately
// keeps the rule readable and survives any future change to the total.
if domain.is_empty() || domain.len() > MAX_TOTAL - 2 {
return false;
}
let mut labels: Vec<&[char]> = Vec::new();
let mut start = 0usize;
for i in 0..domain.len() {
if domain[i] == '.' {
labels.push(&domain[start..i]);
start = i + 1;
}
}
labels.push(&domain[start..]);
// At least one dot. A bare "localhost" is a valid host but not an address
// anyone outside that machine can deliver to, and a signup form is asking
// for the latter.
if labels.len() < 2 {
return false;
}
for label in &labels {
if label.is_empty() || label.len() > MAX_LABEL {
return false;
}
if label[0] == '-' || label[label.len() - 1] == '-' {
return false;
}
for ch in label.iter() {
if !is_letter_or_digit(*ch) && *ch != '-' {
return false;
}
}
}
// The top-level label must be two or more letters. This is what rejects
// "user@example.123" and the bracketed-IP form, and it is the rule most
// likely to need revisiting: it also rejects punycode-free internationalised
// TLDs written in their native script.
let tld = labels[labels.len() - 1];
if tld.len() < 2 {
return false;
}
tld.iter().all(|ch| is_letter(*ch))
}
/// The domain half of an address, lowercased, or `None` if the address is not
/// one this capability accepts.
///
/// Domains are case-insensitive; local parts are not, so this deliberately
/// only normalises the half where doing so is safe.
pub fn email_domain(value: &str) -> Option<String> {
if !is_email(value) {
return None;
}
let chars: Vec<char> = value.chars().collect();
let at = chars.iter().position(|ch| *ch == '@').unwrap();
Some(chars[at + 1..].iter().collect::<String>().to_ascii_lowercase())
}
pub fn fune_vector(args: &[Value]) -> Value {
// A non-string argument arrives here as an empty string, which is exactly
// the answer TypeScript and Python give for a non-string: not an address.
Value::Bool(is_email(args[0].as_str()))
}