1use idna::domain_to_ascii;
2use once_cell::sync::Lazy;
3use regex::Regex;
4use std::borrow::Cow;
5
6use crate::{ValidateIp};
7
8static EMAIL_USER_RE: Lazy<Regex> = Lazy::new(|| {
12    Regex::new(r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap()
13});
14static EMAIL_DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
15    Regex::new(
16        r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
17    ).unwrap()
18});
19static EMAIL_LITERAL_RE: Lazy<Regex> = Lazy::new(|| {
21    Regex::new(r"\[([a-fA-F0-9:\.]+)\]\z").unwrap()
22});
23
24#[must_use]
26fn validate_domain_part(domain_part: &str) -> bool {
27    if EMAIL_DOMAIN_RE.is_match(domain_part) {
28        return true;
29    }
30
31    match EMAIL_LITERAL_RE.captures(domain_part) {
33        Some(caps) => match caps.get(1) {
34            Some(c) => c.as_str().validate_ip(),
35            None => false,
36        },
37        None => false,
38    }
39}
40
41pub trait ValidateEmail {
45    fn validate_email(&self) -> bool {
46        let val = if let Some(v) = self.as_email_string() { v } else { return true; };
47
48        if val.is_empty() || !val.contains('@') {
49            return false;
50        }
51
52        let parts: Vec<&str> = val.rsplitn(2, '@').collect();
53        let user_part = parts[1];
54        let domain_part = parts[0];
55
56        if user_part.chars().count() > 64 || domain_part.chars().count() > 255 {
61            return false;
62        }
63
64        if !EMAIL_USER_RE.is_match(user_part) {
65            return false;
66        }
67
68        if !validate_domain_part(domain_part) {
69            return match domain_to_ascii(domain_part) {
71                Ok(d) => validate_domain_part(&d),
72                Err(_) => false,
73            };
74        }
75
76        true
77    }
78
79    fn as_email_string(&self) -> Option<Cow<str>>;
80}
81
82impl<T> ValidateEmail for &T
83    where T: ValidateEmail {
84    fn as_email_string(&self) -> Option<Cow<str>> {
85        T::as_email_string(self)
86    }
87}
88
89impl ValidateEmail for String {
90    fn as_email_string(&self) -> Option<Cow<str>> {
91        Some(Cow::from(self))
92    }
93}
94
95impl<T> ValidateEmail for Option<T>
96    where
97        T: ValidateEmail, {
98    fn as_email_string(&self) -> Option<Cow<str>> {
99        let Some(u) = self else {
100            return None;
101        };
102
103        T::as_email_string(u)
104    }
105}
106
107impl<'a> ValidateEmail for &'a str {
108    fn as_email_string(&self) -> Option<Cow<'_, str>> {
109        Some(Cow::from(*self))
110    }
111}
112
113impl ValidateEmail for Cow<'_, str> {
114    fn as_email_string(&self) -> Option<Cow<'_, str>> {
115        Some(self.clone())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::borrow::Cow;
122
123    use crate::ValidateEmail;
124
125    #[test]
126    fn test_validate_email() {
127        let tests = vec![
130            ("email@here.com", true),
131            ("weirder-email@here.and.there.com", true),
132            (r#"!def!xyz%abc@example.com"#, true),
133            ("email@[127.0.0.1]", true),
134            ("email@[2001:dB8::1]", true),
135            ("email@[2001:dB8:0:0:0:0:0:1]", true),
136            ("email@[::fffF:127.0.0.1]", true),
137            ("example@valid-----hyphens.com", true),
138            ("example@valid-with-hyphens.com", true),
139            ("test@domain.with.idn.tld.उदाहरण.परीक्षा", true),
140            (r#""test@test"@example.com"#, false),
141            ("a@atm.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true),
143            ("a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.atm", true),
144            (
145                "a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbb.atm",
146                true,
147            ),
148            ("a@atm.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false),
150            ("", false),
151            ("abc", false),
152            ("abc@", false),
153            ("abc@bar", true),
154            ("a @x.cz", false),
155            ("abc@.com", false),
156            ("something@@somewhere.com", false),
157            ("email@127.0.0.1", true),
158            ("email@[127.0.0.256]", false),
159            ("email@[2001:db8::12345]", false),
160            ("email@[2001:db8:0:0:0:0:1]", false),
161            ("email@[::ffff:127.0.0.256]", false),
162            ("example@invalid-.com", false),
163            ("example@-invalid.com", false),
164            ("example@invalid.com-", false),
165            ("example@inv-.alid-.com", false),
166            ("example@inv-.-alid.com", false),
167            (r#"test@example.com\n\n<script src="x.js">"#, false),
168            (r#""\\\011"@here.com"#, false),
169            (r#""\\\012"@here.com"#, false),
170            ("trailingdot@shouldfail.com.", false),
171            ("a@b.com\n", false),
173            ("a\n@b.com", false),
174            (r#""test@test"\n@example.com"#, false),
175            ("a@[127.0.0.1]\n", false),
176            ("John.Doe@exam_ple.com", false),
178        ];
179
180        for (input, expected) in tests {
181            assert_eq!(
183                input.validate_email(),
184                expected,
185                "Email `{}` was not classified correctly",
186                input
187            );
188        }
189    }
190
191    #[test]
192    fn test_validate_email_cow() {
193        let test: Cow<'static, str> = "email@here.com".into();
194        assert!(test.validate_email());
195        let test: Cow<'static, str> = String::from("email@here.com").into();
196        assert!(test.validate_email());
197        let test: Cow<'static, str> = "a@[127.0.0.1]\n".into();
198        assert!(!test.validate_email());
199        let test: Cow<'static, str> = String::from("a@[127.0.0.1]\n").into();
200        assert!(!test.validate_email());
201    }
202
203    #[test]
204    fn test_validate_email_rfc5321() {
205        let test = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@mail.com";
207        assert_eq!(test.validate_email(), false);
208        let test = "a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com";
210        assert_eq!(test.validate_email(), false);
211    }
212}