use idna::domain_to_ascii;
use once_cell::sync::Lazy;
use regex::Regex;
use std::borrow::Cow;
use crate::{ValidateIp};
static EMAIL_USER_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap()
});
static EMAIL_DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
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])?)*$"
).unwrap()
});
static EMAIL_LITERAL_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\[([a-fA-F0-9:\.]+)\]\z").unwrap()
});
#[must_use]
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) => c.as_str().validate_ip(),
None => false,
},
None => false,
}
}
pub trait ValidateEmail {
fn validate_email(&self) -> bool {
let val = if let Some(v) = self.as_email_string() { v } else { return true; };
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 user_part.chars().count() > 64 || domain_part.chars().count() > 255 {
return false;
}
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 as_email_string(&self) -> Option<Cow<str>>;
}
impl<T> ValidateEmail for &T
where T: ValidateEmail {
fn as_email_string(&self) -> Option<Cow<str>> {
T::as_email_string(self)
}
}
impl ValidateEmail for String {
fn as_email_string(&self) -> Option<Cow<str>> {
Some(Cow::from(self))
}
}
impl<T> ValidateEmail for Option<T>
where
T: ValidateEmail, {
fn as_email_string(&self) -> Option<Cow<str>> {
let Some(u) = self else {
return None;
};
T::as_email_string(u)
}
}
impl<'a> ValidateEmail for &'a str {
fn as_email_string(&self) -> Option<Cow<'_, str>> {
Some(Cow::from(*self))
}
}
impl ValidateEmail for Cow<'_, str> {
fn as_email_string(&self) -> Option<Cow<'_, str>> {
Some(self.clone())
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use crate::ValidateEmail;
#[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", false),
("", false),
("abc", false),
("abc@", false),
("abc@bar", true),
("a @x.cz", false),
("abc@.com", 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),
("John.Doe@exam_ple.com", false),
];
for (input, expected) in tests {
assert_eq!(
input.validate_email(),
expected,
"Email `{}` was not classified correctly",
input
);
}
}
#[test]
fn test_validate_email_cow() {
let test: Cow<'static, str> = "email@here.com".into();
assert!(test.validate_email());
let test: Cow<'static, str> = String::from("email@here.com").into();
assert!(test.validate_email());
let test: Cow<'static, str> = "a@[127.0.0.1]\n".into();
assert!(!test.validate_email());
let test: Cow<'static, str> = String::from("a@[127.0.0.1]\n").into();
assert!(!test.validate_email());
}
#[test]
fn test_validate_email_rfc5321() {
let test = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@mail.com";
assert_eq!(test.validate_email(), false);
let test = "a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com";
assert_eq!(test.validate_email(), false);
}
}