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