use crate::errors::{SpaceError, SpaceResult}; const MIN_EMAIL_LEN: usize = 3; const MAX_EMAIL_LEN: usize = 254; pub fn validate_email(raw: &str) -> SpaceResult { let email = raw.trim().to_lowercase(); if email.len() < MIN_EMAIL_LEN || email.len() > MAX_EMAIL_LEN { return Err(SpaceError::InvalidEmail); } if email.chars().any(char::is_whitespace) { return Err(SpaceError::InvalidEmail); } let mut parts = email.split('@'); let (Some(local), Some(domain), None) = (parts.next(), parts.next(), parts.next()) else { return Err(SpaceError::InvalidEmail); }; if !valid_local_part(local) || !valid_domain(domain) { return Err(SpaceError::InvalidEmail); } Ok(email) } fn valid_local_part(local: &str) -> bool { const MAX_LOCAL_LEN: usize = 64; if local.is_empty() || local.len() > MAX_LOCAL_LEN { return false; } if local.starts_with('.') || local.ends_with('.') || local.contains("..") { return false; } local.chars().all(|c| { c.is_ascii_alphanumeric() || matches!( c, '.' | '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_' | '`' | '{' | '|' | '}' | '~' ) }) } fn valid_domain(domain: &str) -> bool { const MAX_LABEL_LEN: usize = 63; let labels: Vec<&str> = domain.split('.').collect(); if labels.len() < 2 { return false; } for label in &labels { if label.is_empty() || label.len() > MAX_LABEL_LEN { return false; } if label.starts_with('-') || label.ends_with('-') { return false; } if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { return false; } } let Some(tld) = labels.last() else { return false; }; tld.len() >= 2 && tld.chars().all(|c| c.is_ascii_alphabetic()) }