use crate::config::{Config, DomainCheck};
use crate::error::{Error, ErrorKind};
use crate::normalize::Normalized;
use crate::parser::Parsed;
const MAX_LOCAL_PART_LEN: usize = 64;
const MAX_ADDRESS_LEN: usize = 254;
const MAX_LABEL_LEN: usize = 63;
pub(crate) fn validate(
parsed: &Parsed<'_>,
normalized: &Normalized,
config: &Config,
) -> Result<(), Error> {
let domain = &normalized.domain;
let local = parsed.local_part_str();
let domain_str = parsed.domain_str();
if local.len() > MAX_LOCAL_PART_LEN {
return Err(Error::new(
ErrorKind::LocalPartTooLong { len: local.len() },
parsed.local_part.start,
));
}
let total = local.len() + 1 + domain_str.len();
if total > MAX_ADDRESS_LEN {
return Err(Error::new(
ErrorKind::AddressTooLong { len: total },
parsed.local_part.start,
));
}
let is_domain_literal = domain_str.starts_with('[');
if config.require_tld_dot && !domain.contains('.') && !is_domain_literal {
return Err(Error::new(ErrorKind::DomainNoDot, parsed.domain.start));
}
if !is_domain_literal {
for label in domain.split('.') {
if label.len() > MAX_LABEL_LEN {
return Err(Error::new(
ErrorKind::DomainLabelTooLong {
label: label.to_string(),
len: label.len(),
},
parsed.domain.start,
));
}
}
match config.domain_check {
DomainCheck::Syntax => {}
DomainCheck::Tld => validate_tld(domain, parsed.domain.start)?,
DomainCheck::Psl => validate_psl(domain, parsed.domain.start)?,
}
}
Ok(())
}
fn validate_tld(domain: &str, pos: usize) -> Result<(), Error> {
let tld = domain.rsplit('.').next().unwrap_or(domain);
if tld.starts_with("xn--") && tld.len() > 4 {
return Ok(());
}
if tld.len() < 2 || !tld.chars().all(|c| c.is_ascii_alphabetic()) {
return Err(Error::new(ErrorKind::UnknownTld(tld.to_string()), pos));
}
Ok(())
}
#[cfg(feature = "psl")]
fn validate_psl(domain: &str, pos: usize) -> Result<(), Error> {
match psl::suffix(domain.as_bytes()) {
Some(suffix) if suffix.is_known() => Ok(()),
_ => {
let tld = domain.rsplit('.').next().unwrap_or(domain);
Err(Error::new(ErrorKind::UnknownTld(tld.to_string()), pos))
}
}
}
#[cfg(not(feature = "psl"))]
fn validate_psl(domain: &str, pos: usize) -> Result<(), Error> {
validate_tld(domain, pos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tld_valid() {
assert!(validate_tld("example.com", 0).is_ok());
assert!(validate_tld("example.co.uk", 0).is_ok());
assert!(validate_tld("example.xn--p1ai", 0).is_ok()); }
#[test]
fn tld_invalid() {
assert!(validate_tld("example.x", 0).is_err()); assert!(validate_tld("example.123", 0).is_err()); }
#[test]
fn rejects_local_part_too_long() {
let long_local = "a".repeat(65);
let input = format!("{long_local}@example.com");
let result: Result<crate::EmailAddress, _> = input.parse();
assert!(matches!(
result.unwrap_err().kind(),
crate::ErrorKind::LocalPartTooLong { .. }
));
}
#[test]
fn rejects_address_too_long() {
let long_domain = format!("{}.com", "a".repeat(250));
let input = format!("u@{long_domain}");
let result: Result<crate::EmailAddress, _> = input.parse();
let kind = result.unwrap_err().kind().clone();
assert!(
matches!(
kind,
crate::ErrorKind::AddressTooLong { .. }
| crate::ErrorKind::DomainLabelTooLong { .. }
| crate::ErrorKind::IdnaError(_)
),
"expected length or IDNA error, got {kind:?}"
);
}
#[test]
fn rejects_domain_label_too_long() {
let long_label = "a".repeat(64);
let input = format!("user@{long_label}.com");
let result: Result<crate::EmailAddress, _> = input.parse();
let kind = result.unwrap_err().kind().clone();
assert!(
matches!(
kind,
crate::ErrorKind::DomainLabelTooLong { .. } | crate::ErrorKind::IdnaError(_)
),
"expected label-too-long or IDNA error, got {kind:?}"
);
}
#[test]
fn domain_literal_skips_label_check() {
let config = crate::Config::builder()
.allow_domain_literal()
.allow_single_label_domain()
.build();
let result = crate::EmailAddress::parse_with("user@[192.168.1.1]", &config);
assert!(result.is_ok());
}
}