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}