Skip to main content

aster_forge_validation/
email.rs

1//! Email validation and normalization helpers.
2//!
3//! The helpers implement Aster's lightweight email contract: trim and lowercase values, reject
4//! obviously malformed addresses, and keep the behavior independent from any account or identity
5//! provider model.
6
7use crate::{Result, ValidationError};
8
9/// Validates a normalized email address using Aster's lightweight email rules.
10pub fn validate_email(email: &str) -> Result<()> {
11    if email.len() > 254 {
12        return Err(ValidationError::new("email is too long"));
13    }
14    if email.matches('@').count() != 1 {
15        return Err(ValidationError::new("invalid email format"));
16    }
17    let Some((local, domain)) = email.split_once('@') else {
18        return Err(ValidationError::new("invalid email format"));
19    };
20    if local.is_empty() || domain.is_empty() {
21        return Err(ValidationError::new("invalid email format"));
22    }
23    if !domain.contains('.') {
24        return Err(ValidationError::new("invalid email format"));
25    }
26    Ok(())
27}
28
29/// Trims and lowercases an email address, then validates it.
30pub fn normalize_email(email: &str) -> Result<String> {
31    let normalized = email.trim().to_ascii_lowercase();
32    validate_email(&normalized)?;
33    Ok(normalized)
34}
35
36/// Returns the lowercased domain portion of an email address.
37pub fn email_domain(email: &str) -> Result<String> {
38    let normalized = normalize_email(email)?;
39    normalized
40        .rsplit_once('@')
41        .map(|(_, domain)| domain.to_ascii_lowercase())
42        .ok_or_else(|| ValidationError::new("invalid email format"))
43}
44
45#[cfg(test)]
46mod tests {
47    use super::{email_domain, normalize_email, validate_email};
48
49    #[test]
50    fn validate_email_requires_exactly_one_at_separator() {
51        assert!(validate_email("alice@example.com").is_ok());
52        assert!(validate_email("alice@@example.com").is_err());
53        assert!(validate_email("alice@example@com").is_err());
54        assert!(validate_email("alice.example.com").is_err());
55        assert!(validate_email("@example.com").is_err());
56        assert!(validate_email("alice@").is_err());
57    }
58
59    #[test]
60    fn email_helpers_keep_existing_normalization_contract() {
61        assert_eq!(
62            normalize_email(" Alice@Example.COM ").unwrap(),
63            "alice@example.com"
64        );
65        assert_eq!(email_domain("alice@Example.COM").unwrap(), "example.com");
66    }
67
68    #[test]
69    fn validate_email_rejects_missing_domain_dot_and_overlong_values() {
70        assert!(validate_email("alice@example").is_err());
71        assert!(validate_email("alice@.").is_ok());
72
73        let long_local = "a".repeat(245);
74        let too_long = format!("{long_local}@example.com");
75        assert!(too_long.len() > 254);
76        assert!(validate_email(&too_long).is_err());
77    }
78}