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