use regex::Regex; use std::sync::LazyLock; /// Basic email format regex — covers the vast majority of valid email addresses. /// Does NOT attempt to match the full RFC 5321 grammar (which is impractical via regex). static EMAIL_REGEX: LazyLock = LazyLock::new(|| { Regex::new( r"(?i)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$", ) .expect("invalid email regex") }); /// Check whether an email address has valid syntax. pub fn is_valid_email_format(email: &str) -> bool { let email = email.trim(); if email.is_empty() || email.len() > 254 { return false; } let parts: Vec<&str> = email.rsplitn(2, '@').collect(); if parts.len() != 2 { return false; } let local = parts[1]; let domain = parts[0]; // Local part max 64 chars if local.is_empty() || local.len() > 64 { return false; } // Domain must have at least one dot (TLD only not valid for email) if !domain.contains('.') { return false; } EMAIL_REGEX.is_match(email) } /// Email validation result with scoring. #[derive(Debug, Clone)] pub struct EmailValidationResult { pub is_valid: bool, pub format_valid: bool, pub score: f64, pub error_message: Option, } /// Validate an email address (synchronous, format-only). /// DNS-based validation (MX records, disposable domains) would require async and is /// intended for the N-API bridge layer where the TypeScript side already has DNS access. pub fn validate_email(email: &str) -> EmailValidationResult { let format_valid = is_valid_email_format(email); if !format_valid { return EmailValidationResult { is_valid: false, format_valid: false, score: 0.0, error_message: Some(format!("Invalid email format: {}", email)), }; } // Role account detection (weight 0.1 penalty) let local = email.split('@').next().unwrap_or(""); let is_role = is_role_account(local); // Score: format (0.4) + assumed-mx (0.3) + assumed-not-disposable (0.2) + role (0.1) let mut score = 0.4 + 0.3 + 0.2; // format + mx + not-disposable if !is_role { score += 0.1; } EmailValidationResult { is_valid: score >= 0.7, format_valid: true, score, error_message: None, } } /// Check if a local part is a common role account. fn is_role_account(local: &str) -> bool { const ROLE_ACCOUNTS: &[&str] = &[ "abuse", "admin", "administrator", "billing", "compliance", "devnull", "dns", "ftp", "hostmaster", "info", "inoc", "ispfeedback", "ispsupport", "list", "list-request", "maildaemon", "mailer-daemon", "mailerdaemon", "marketing", "noc", "no-reply", "noreply", "null", "phish", "phishing", "postmaster", "privacy", "registrar", "root", "sales", "security", "spam", "support", "sysadmin", "tech", "undisclosed-recipients", "unsubscribe", "usenet", "uucp", "webmaster", "www", ]; let lower = local.to_lowercase(); ROLE_ACCOUNTS.contains(&lower.as_str()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_valid_emails() { assert!(is_valid_email_format("user@example.com")); assert!(is_valid_email_format("first.last@example.com")); assert!(is_valid_email_format("user+tag@example.com")); assert!(is_valid_email_format("user@sub.domain.example.com")); } #[test] fn test_invalid_emails() { assert!(!is_valid_email_format("")); assert!(!is_valid_email_format("@")); assert!(!is_valid_email_format("user@")); assert!(!is_valid_email_format("@domain.com")); assert!(!is_valid_email_format("user@domain")); // no TLD assert!(!is_valid_email_format("user @domain.com")); // space assert!(!is_valid_email_format("user@.com")); // leading dot } #[test] fn test_validate_email_scoring() { let result = validate_email("user@example.com"); assert!(result.is_valid); assert!(result.score >= 0.9); let result = validate_email("postmaster@example.com"); assert!(result.is_valid); assert!(result.score >= 0.7); assert!(result.score < 1.0); // role account penalty let result = validate_email("not-an-email"); assert!(!result.is_valid); assert_eq!(result.score, 0.0); } #[test] fn test_role_accounts() { assert!(is_role_account("postmaster")); assert!(is_role_account("abuse")); assert!(is_role_account("noreply")); assert!(!is_role_account("john")); assert!(!is_role_account("alice")); } }