use idna::domain_to_ascii;
use regex::Regex;
use std::borrow::Cow;
use crate::validation::ip::validate_ip;
lazy_static! {
static ref EMAIL_USER_RE: Regex = Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap();
static ref EMAIL_DOMAIN_RE: Regex = Regex::new(
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$"
).unwrap();
static ref EMAIL_LITERAL_RE: Regex = Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap();
}
pub fn validate_email<'a, T>(val: T) -> bool
where
T: Into<Cow<'a, str>>,
{
let val = val.into();
if val.is_empty() || !val.contains('@') {
return false;
}
let parts: Vec<&str> = val.rsplitn(2, '@').collect();
let user_part = parts[1];
let domain_part = parts[0];
if !EMAIL_USER_RE.is_match(user_part) {
return false;
}
if !validate_domain_part(domain_part) {
return match domain_to_ascii(domain_part) {
Ok(d) => validate_domain_part(&d),
Err(_) => false,
};
}
true
}
fn validate_domain_part(domain_part: &str) -> bool {
if EMAIL_DOMAIN_RE.is_match(domain_part) {
return true;
}
match EMAIL_LITERAL_RE.captures(domain_part) {
Some(caps) => match caps.get(1) {
Some(c) => validate_ip(c.as_str()),
None => false,
},
None => false,
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::validate_email;
#[test]
fn test_validate_email() {
let tests = vec![
("email@here.com", true),
("weirder-email@here.and.there.com", true),
(r#"!def!xyz%abc@example.com"#, true),
("email@[127.0.0.1]", true),
("email@[2001:dB8::1]", true),
("email@[2001:dB8:0:0:0:0:0:1]", true),
("email@[::fffF:127.0.0.1]", true),
("example@valid-----hyphens.com", true),
("example@valid-with-hyphens.com", true),
("test@domain.with.idn.tld.उदाहरण.परीक्षा", true),
(r#""test@test"@example.com"#, false),
("a@atm.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true),
("a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.atm", true),
(
"a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbb.atm",
true,
),
("a@atm.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true),
("", false),
("abc", false),
("abc@", false),
("abc@bar", true),
("a @x.cz", false),
("something@@somewhere.com", false),
("email@127.0.0.1", true),
("email@[127.0.0.256]", false),
("email@[2001:db8::12345]", false),
("email@[2001:db8:0:0:0:0:1]", false),
("email@[::ffff:127.0.0.256]", false),
("example@invalid-.com", false),
("example@-invalid.com", false),
("example@invalid.com-", false),
("example@inv-.alid-.com", false),
("example@inv-.-alid.com", false),
(r#"test@example.com\n\n<script src="x.js">"#, false),
(r#""\\\011"@here.com"#, false),
(r#""\\\012"@here.com"#, false),
("trailingdot@shouldfail.com.", false),
("a@b.com\n", false),
("a\n@b.com", false),
(r#""test@test"\n@example.com"#, false),
("a@[127.0.0.1]\n", false),
];
for (input, expected) in tests {
println!("{} - {}", input, expected);
assert_eq!(validate_email(input), expected);
}
}
#[test]
fn test_validate_email_cow() {
let test: Cow<'static, str> = "email@here.com".into();
assert_eq!(validate_email(test), true);
let test: Cow<'static, str> = String::from("email@here.com").into();
assert_eq!(validate_email(test), true);
let test: Cow<'static, str> = "a@[127.0.0.1]\n".into();
assert_eq!(validate_email(test), false);
let test: Cow<'static, str> = String::from("a@[127.0.0.1]\n").into();
assert_eq!(validate_email(test), false);
}
}