#[must_use]
pub fn is_valid_email(s: &str) -> bool {
let Some((local, domain)) = s.split_once('@') else {
return false;
};
!domain.contains('@') && is_valid_local(local) && is_valid_domain(domain)
}
fn is_valid_local(local: &str) -> bool {
!local.is_empty()
&& !local.starts_with('.')
&& !local.ends_with('.')
&& !local.contains("..")
&& local
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b".+-_%".contains(&b))
}
fn is_valid_domain(domain: &str) -> bool {
let labels: Vec<&str> = domain.split('.').collect();
if labels.len() < 2 {
return false;
}
let (rest, tld) = labels.split_at(labels.len() - 1);
let tld = tld[0];
tld.len() >= 2
&& tld.bytes().all(|b| b.is_ascii_alphabetic())
&& rest.iter().all(|label| is_valid_label(label))
}
fn is_valid_label(label: &str) -> bool {
!label.is_empty()
&& label.len() <= 63
&& !label.starts_with('-')
&& !label.ends_with('-')
&& label
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-')
}
#[cfg(test)]
#[path = "is_valid_email.test.rs"]
mod tests;
#[cfg(test)]
#[path = "is_valid_email.spec.rs"]
mod spec;