codoseo_web/auth/
email.rs1pub fn parse(raw: &str) -> Option<&str> {
9 let email = raw.trim();
10 if email.len() > 254 || email.chars().any(|c| c.is_whitespace() || c.is_control()) {
11 return None;
12 }
13 let (local, domain) = email.rsplit_once('@')?;
14 let domain_ok = domain.contains('.')
15 && !domain.starts_with('.')
16 && !domain.ends_with('.')
17 && !domain.contains("..")
18 && domain
19 .chars()
20 .all(|c| c.is_alphanumeric() || c == '.' || c == '-');
21 (!local.is_empty() && !local.contains('@') && domain_ok).then_some(email)
22}
23
24pub fn canonical(email: &str) -> String {
26 let lower = email.trim().to_lowercase();
27 let Some((local, domain)) = lower.rsplit_once('@') else {
28 return lower;
29 };
30 let local = local.split('+').next().unwrap_or(local);
31 let (local, domain) = match domain {
32 "gmail.com" | "googlemail.com" => (local.replace('.', ""), "gmail.com"),
33 _ => (local.to_owned(), domain),
34 };
35 format!("{local}@{domain}")
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41
42 #[test]
43 fn gmail_variants_share_one_canonical_form() {
44 let a = canonical("Ana@Gmail.com");
45 assert_eq!(a, "ana@gmail.com");
46 assert_eq!(canonical("a.na+seo@gmail.com"), a);
47 assert_eq!(canonical("A.N.A@googlemail.com"), a);
48 }
49
50 #[test]
51 fn other_domains_keep_dots_but_drop_tags() {
52 assert_eq!(
53 canonical("First.Last+x@Example.com"),
54 "first.last@example.com"
55 );
56 assert_ne!(canonical("a.na@example.com"), canonical("ana@example.com"));
57 }
58
59 #[test]
60 fn parse_accepts_and_rejects() {
61 assert_eq!(parse(" ana@example.com "), Some("ana@example.com"));
62 for bad in [
63 "",
64 "ana",
65 "ana@",
66 "@example.com",
67 "ana@example",
68 "a b@example.com",
69 "ana@ex..com",
70 ] {
71 assert_eq!(parse(bad), None, "{bad:?}");
72 }
73 }
74}