use crate::errors::ValidationError;
use crate::models::EmailValidator;
use idna::uts46::Uts46;
use idna::uts46::{AsciiDenyList, DnsLength, Hyphens};
use std::net::IpAddr;
use std::str::FromStr;
use trust_dns_resolver::config::*;
use trust_dns_resolver::Resolver;
use crate::util::ip_addr_ext::IpAddrExt;
pub fn validate_domain(
validator: &EmailValidator,
domain: &str,
) -> Result<(String, String, Option<IpAddr>, bool), ValidationError> {
if domain.is_empty() {
return Err(ValidationError::SyntaxError(
"Invalid Domain: The part after the '@' sign cannot be empty.".to_string(),
));
}
if domain.starts_with('[') && domain.ends_with(']') {
if !validator.allow_domain_literal {
return Err(ValidationError::ValueError(
"Invalid Domain: A bracketed IP address after the '@' sign is not permitted."
.to_string(),
));
}
let domain_literal = &domain[1..domain.len() - 1];
if domain_literal.starts_with("IPv6:") {
let ipv6_literal = &domain_literal[5..];
let addr = IpAddr::from_str(ipv6_literal).map_err(|_| {
ValidationError::SyntaxError(
"Invalid Domain: The IPv6 address in brackets following the '@' symbol is not valid."
.to_string(),
)
})?;
if let IpAddr::V6(addr) = addr {
let name = format!("[IPv6:{}]", addr);
return Ok((name.clone(), name, Some(IpAddr::V6(addr)), false));
}
}
let addr = IpAddr::from_str(domain_literal).map_err(|_| {
ValidationError::SyntaxError(
"Invalid Domain: The address in brackets following the '@' sign is not a valid IP address."
.to_string(),
)
})?;
let name = match addr {
IpAddr::V4(_) => format!("[{}]", addr),
IpAddr::V6(_) => format!("[IPv6:{}]", addr),
};
return Ok((name.clone(), name, Some(addr), false));
}
if !crate::consts::ATEXT_HOSTNAME_INTL.is_match(domain.as_bytes()) {
return Err(ValidationError::SyntaxError(
"Invalid Domain: Contains invalid characters after '@' sign.".to_string(),
));
}
crate::validators::validate_chars(domain, false)?;
let ascii_domain = Uts46::new()
.to_ascii(
domain.as_bytes(),
AsciiDenyList::URL,
Hyphens::Allow,
DnsLength::Verify,
)
.map_err(|_| {
ValidationError::SyntaxError(
"Invalid Domain: Contains invalid characters after '@' sign post Unicode normalization."
.to_string(),
)
})?;
if !crate::consts::ATEXT_HOSTNAME_INTL.is_match(ascii_domain.as_bytes()) {
return Err(ValidationError::SyntaxError(
"Invalid Domain: Contains invalid characters after Unicode normalization.".to_string(),
));
}
crate::validators::validate_email_label(
&ascii_domain,
"Invalid Domain: A {} cannot immediately follow the '@' symbol.",
"Invalid Domain: A {} cannot appear at the end of the domain.",
true,
)?;
if ascii_domain.len() > crate::consts::MAX_DOMAIN_LENGTH {
return Err(ValidationError::ValueError(
"Invalid Domain: Exceeds the maximum length (253 chars).".to_string(),
));
}
for label in ascii_domain.split('.') {
if label.len() > crate::consts::MAX_DNS_LABEL_LENGTH {
return Err(ValidationError::ValueError(
"Invalid Label: Exceeds the maximum length (63 chars).".to_string(),
));
}
if label.is_empty() {
return Err(ValidationError::SyntaxError(
"Invalid Label: The Label cannot be empty.".to_string(),
));
}
if crate::consts::DNS_LABEL_REGEX.is_match(label.as_bytes())
&& !label.to_lowercase().starts_with("xn--")
{
return Err(ValidationError::SyntaxError(
"Invalid Domain: Two letters followed by two dashes ('--') are not allowed immediately after the '@' sign or a period.".to_string(),
));
}
}
if validator.deliverable_address {
if !ascii_domain.contains(".") {
return Err(ValidationError::SyntaxError(
"Invalid Domain: Must contain a period ('.') to be considered valid.".to_string(),
));
}
if !crate::consts::DOMAIN_NAME_REGEX.is_match(ascii_domain.as_bytes()) {
return Err(ValidationError::SyntaxError(
"Invalid domain: The part after the '@' sign does not belong to a valid top-level domain (TLD).".to_string(),
));
}
}
let (unicode_domain, result) =
Uts46::new().to_unicode(ascii_domain.as_bytes(), AsciiDenyList::URL, Hyphens::Allow);
result.map_err(|_| {
ValidationError::SyntaxError("Invalid Domain: Contains invalid characters after '@' sign post Unicode normalization.".to_string())
})?;
let maybe_special_domain =
crate::consts::SPECIAL_USE_DOMAIN_NAMES
.iter()
.find(|special_domain| {
ascii_domain == **special_domain
|| ascii_domain.ends_with(&format!(".{}", special_domain))
});
if let Some(special) = maybe_special_domain {
let is_allowed = validator
.allowed_special_domains
.iter()
.any(|allowed| allowed == special);
if !is_allowed {
Err(ValidationError::SyntaxError(
"Invalid Domain: The part after the '@' sign is a reserved or special-use domain that cannot be used.".to_string(),
))
} else {
Ok((
unicode_domain.to_string(),
ascii_domain.to_string(),
None,
true,
))
}
} else {
Ok((
unicode_domain.to_string(),
ascii_domain.to_string(),
None,
false,
))
}
}
pub fn validate_deliverability(domain: &str) -> Result<(), ValidationError> {
let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default())
.map_err(|e| ValidationError::SyntaxError(e.to_string()))?;
if let Ok(mx_records) = resolver.mx_lookup(domain) {
for mx in mx_records.iter() {
let exchange = mx.exchange().to_string();
if exchange == "." {
return Err(ValidationError::SyntaxError(
"Invalid Domain: The domain does not accept email due to a null MX record, indicating it is not configured to receive emails.".to_string(),
));
}
}
if mx_records
.iter()
.any(|mx| !mx.exchange().to_string().is_empty())
{
return Ok(());
}
}
if let Ok(a_records) = resolver.ipv4_lookup(domain) {
if a_records.iter().any(|ip| IpAddrExt::is_global(&ip.0)) {
return Ok(());
}
}
if let Ok(aaaa_records) = resolver.ipv6_lookup(domain) {
if aaaa_records.iter().any(|ip| IpAddrExt::is_global(&ip.0)) {
return Ok(());
}
}
if let Ok(txt_records) = resolver.txt_lookup(domain) {
for record in txt_records.iter() {
let txt = record.to_string();
if txt.starts_with("v=spf1 ") && txt.contains("-all") {
return Err(ValidationError::SyntaxError(
"Invalid Domain: The domain does not send email due to an SPF record that rejects all emails.".to_string(),
));
}
}
}
Err(ValidationError::SyntaxError(
"Invalid Domain: No MX, A, or AAAA records found for domain.".to_string(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case("domain.com")]
#[case("a.com")]
#[case("sub.domain.com")] #[case("example.co.uk")] #[case("xn--d1acufc.xn--p1ai")] #[case("123.com")] #[case("example.museum")] #[case("example.travel")] #[case("e.com")] #[case("a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.com")] fn test_validate_domain_valid(#[case] domain: &str) {
let emval = EmailValidator::default();
let result = validate_domain(&emval, domain);
assert!(result.is_ok());
}
#[rstest]
fn test_validate_domain_idna() {
let emval = EmailValidator::default();
let result = validate_domain(&emval, "xn--fsqu00a.xn--4rr70v");
assert_eq!(
result.unwrap(),
(
"例子.广告".to_string(),
"xn--fsqu00a.xn--4rr70v".to_string(),
None,
false
)
);
let result = validate_domain(&emval, "例子.广告");
assert_eq!(
result.unwrap(),
(
"例子.广告".to_string(),
"xn--fsqu00a.xn--4rr70v".to_string(),
None,
false
)
);
}
#[rstest]
#[case("invali*d.com")]
#[case(&"a".repeat(64))]
#[case("a.com-")]
#[case("a-.com")]
#[case(&(String::from("a") + &".com".repeat(126)))]
#[case("example..com")] #[case("example-.com")] #[case("example-.com")] #[case("example..com")] #[case("example-.com")] #[case(".example.com")] #[case("example.com.")] #[case("example..com")]
#[case("example.com-")] #[case("example..com")] #[case("xn--d1acufc.xn--p1ai-")] #[case("ex_ample.com")] fn test_validate_domain_invalid(#[case] domain: &str) {
let emval = EmailValidator::default();
let result = validate_domain(&emval, domain);
assert!(result.is_err());
}
#[rstest]
#[case("me@anything.arpa", false)]
#[case("me@link.local", false)]
#[case("me@valid.invalid", false)]
#[case("me@host.localhost", false)]
#[case("me@onion.onion.onion", false)]
#[case("me@test.test.test", false)]
fn test_special_use_domains(#[case] domain: &str, #[case] expected: bool) {
let emval = EmailValidator::default();
let result = validate_domain(&emval, domain);
if expected {
assert!(result.is_ok());
} else {
assert!(result.is_err());
}
}
#[rstest]
#[case("null.example.com")]
#[case("nonexistentdomain.example")]
#[case("-invaliddomain.com")]
#[case("invalid_domain.com")]
#[case("例え.テスト")]
#[case("example..com")]
fn test_validate_deliverability_invalid(#[case] domain: &str) {
assert!(validate_deliverability(domain).is_err());
}
#[rstest]
#[case("google.com")]
#[case("gmail.com")]
#[case("yahoo.com")]
#[case("hotmail.com")]
#[case("outlook.com")]
#[case("aol.com")]
fn test_validate_deliverability_valid(#[case] domain: &str) {
assert!(validate_deliverability(domain).is_ok());
}
#[rstest]
#[case("blackhole.isi.edu")] fn test_validate_deliverability_null_mx(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err());
}
#[rstest]
#[case("www.cloudflare.com")]
#[case("osu.edu")] fn test_validate_deliverability_valid_a_no_mx(#[case] domain: &str) {
assert!(validate_deliverability(domain).is_ok());
}
#[rstest]
#[case("nonexistentdomain.example")]
#[case("invalid-domain-test-12345.com")]
fn test_validate_deliverability_no_records(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err());
}
#[rstest]
#[case("thisdomaindoesnotexist.tld")]
fn test_validate_deliverability_nxdomain(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err());
}
#[rstest]
#[case("example.com")]
#[case("example.org")]
fn test_validate_deliverability_spf_reject_all(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err());
}
#[rstest]
#[case("localhost")] #[case("example.internal")] fn test_validate_deliverability_private_ip(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err());
}
#[rstest]
#[case("-invaliddomain.com")]
#[case("invalid_domain.com")]
#[case("example..com")]
fn test_validate_deliverability_invalid_syntax(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err());
}
#[rstest]
#[case("例え.テスト")] #[case("مثال.إختبار")] fn test_validate_deliverability_idn(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err() || result.is_ok());
}
#[rstest]
#[case("no-ns.example.com")] fn test_validate_deliverability_no_nameservers(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err());
}
#[rstest]
#[case("timeout.example.com")]
fn test_validate_deliverability_timeout(#[case] domain: &str) {
let result = validate_deliverability(domain);
assert!(result.is_err());
}
}