use crate::models::{EmailValidator, ValidatedEmail};
use pyo3::prelude::*;
impl EmailValidator {
pub fn validate_email(
&self,
email: &str,
) -> Result<ValidatedEmail, crate::errors::ValidationError> {
let (unvalidated_local_part, unvalidated_domain) = crate::validators::split_email(email)?;
crate::validators::validate_email_length(&unvalidated_local_part, &unvalidated_domain)?;
let mut valid_local_part =
crate::validators::validate_local_part(self, &unvalidated_local_part)?;
if crate::consts::CASE_INSENSITIVE_MAILBOX_NAMES
.contains(&valid_local_part.to_lowercase().as_str())
{
valid_local_part = valid_local_part.to_lowercase();
}
let (domain_name, ascii_domain, domain_address, is_whitelisted_special_domain) =
crate::validators::validate_domain(self, &unvalidated_domain)?;
if self.deliverable_address && !is_whitelisted_special_domain {
crate::validators::validate_deliverability(&ascii_domain)?;
}
let ascii_email = valid_local_part
.is_ascii()
.then(|| format!("{}@{}", valid_local_part, ascii_domain));
let normalized = format!("{}@{}", valid_local_part, domain_name);
Ok(ValidatedEmail {
original: email.to_string(),
local_part: valid_local_part,
domain_name,
ascii_domain,
domain_address,
normalized,
ascii_email,
is_deliverable: true,
})
}
}
#[pymethods]
impl EmailValidator {
#[new]
#[pyo3(signature = (
allow_smtputf8 = true,
allow_empty_local = false,
allow_quoted_local = false,
allow_domain_literal = false,
deliverable_address = true,
allowed_special_domains = vec![],
))]
pub fn new(
allow_smtputf8: bool,
allow_empty_local: bool,
allow_quoted_local: bool,
allow_domain_literal: bool,
deliverable_address: bool,
allowed_special_domains: Vec<String>,
) -> Self {
EmailValidator {
allow_smtputf8,
allow_empty_local,
allow_quoted_local,
allow_domain_literal,
deliverable_address,
allowed_special_domains,
}
}
#[pyo3(name = "validate_email")]
fn py_validate_email(&self, email: &str) -> PyResult<ValidatedEmail> {
self.validate_email(email).map_err(|e| e.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use std::net::IpAddr;
use std::str::FromStr;
fn ipv4(octets: [u8; 4]) -> Option<IpAddr> {
Some(IpAddr::V4(std::net::Ipv4Addr::new(
octets[0], octets[1], octets[2], octets[3],
)))
}
fn ipv6(addr: &str) -> Option<IpAddr> {
Some(IpAddr::V6(std::net::Ipv6Addr::from_str(addr).unwrap()))
}
#[rstest]
#[case("example@domain.com", Some("example@domain.com"))]
#[case(
"user.name+tag+sorting@example.com",
Some("user.name+tag+sorting@example.com")
)]
#[case("x@example.com", Some("x@example.com"))]
#[case(
"example-indeed@strange-example.com",
Some("example-indeed@strange-example.com")
)]
fn test_validate_email_valid(#[case] email: &str, #[case] expected: Option<&str>) {
let emval = EmailValidator {
allow_smtputf8: false,
allow_empty_local: false,
allow_quoted_local: false,
allow_domain_literal: false,
deliverable_address: false,
allowed_special_domains: Vec::new(),
};
let result = emval.validate_email(email);
match expected {
Some(expected_normalized) => {
assert!(result.is_ok());
let validated_email = result.unwrap();
assert_eq!(validated_email.normalized, expected_normalized);
}
None => {
assert!(result.is_err());
}
}
}
#[rstest]
#[case("plainaddress", None)]
#[case("@missing-local.org", None)]
#[case("missing-domain@.com", None)]
#[case("missing-at-sign.com", None)]
#[case("missing-tld@domain.", None)]
#[case("invalid-char@domain.c*m", None)]
#[case("too..many..dots@domain.com", None)]
fn test_validate_email_invalid(#[case] email: &str, #[case] expected: Option<&str>) {
let emval = EmailValidator::default();
let result = emval.validate_email(email);
match expected {
Some(expected_normalized) => {
assert!(result.is_ok());
let validated_email = result.unwrap();
assert_eq!(validated_email.normalized, expected_normalized);
}
None => {
assert!(result.is_err());
}
}
}
#[rstest]
#[case("POSTMASTER@example.com", Some("postmaster@example.com"))]
#[case("NOT-POSTMASTER@example.com", Some("NOT-POSTMASTER@example.com"))]
fn test_validate_email_case_insensitive(#[case] email: &str, #[case] expected: Option<&str>) {
let emval = EmailValidator {
allow_smtputf8: false,
allow_empty_local: false,
allow_quoted_local: false,
allow_domain_literal: false,
deliverable_address: false,
allowed_special_domains: Vec::new(),
};
let result = emval.validate_email(email);
match expected {
Some(expected_normalized) => {
assert!(result.is_ok());
let validated_email = result.unwrap();
assert_eq!(validated_email.normalized, expected_normalized);
}
None => {
assert!(result.is_err());
}
}
}
#[rstest]
#[case("me@[127.0.0.1]", "[127.0.0.1]", ipv4([127, 0, 0, 1]))]
#[case("me@[192.168.0.1]", "[192.168.0.1]", ipv4([192, 168, 0, 1]))]
#[case("me@[IPv6:::1]", "[IPv6:::1]", ipv6("::1"))]
#[case(
"me@[IPv6:0000:0000:0000:0000:0000:0000:0000:0001]",
"[IPv6:::1]",
ipv6("::1")
)]
#[case("me@[IPv6:2001:db8::1]", "[IPv6:2001:db8::1]", ipv6("2001:db8::1"))]
#[case(
"me@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
"[IPv6:2001:db8:85a3::8a2e:370:7334]",
ipv6("2001:db8:85a3::8a2e:370:7334")
)]
#[case(
"me@[IPv6:2001:db8:1234:5678:9abc:def0:1234:5678]",
"[IPv6:2001:db8:1234:5678:9abc:def0:1234:5678]",
ipv6("2001:db8:1234:5678:9abc:def0:1234:5678")
)]
fn test_validate_domain_literal_valid(
#[case] email: &str,
#[case] expected_domain: &str,
#[case] expected_ip: Option<IpAddr>,
) {
let emval = EmailValidator {
allow_domain_literal: true,
allow_smtputf8: false,
allow_empty_local: false,
allow_quoted_local: false,
deliverable_address: false,
allowed_special_domains: Vec::new(),
};
let result = emval.validate_email(email);
assert!(result.is_ok());
let validated_email = result.unwrap();
assert_eq!(validated_email.domain_name, expected_domain);
assert_eq!(validated_email.domain_address, expected_ip);
}
#[rstest]
#[case("user@anon.com.test", vec!["test".to_string()])]
#[case("user@anon.com.invalid", vec!["invalid".to_string()])]
#[case("user@example.test", vec!["test".to_string()])]
#[case("user@example.invalid", vec!["invalid".to_string()])]
fn test_validate_allowed_special_domains(
#[case] email: &str,
#[case] allowed_domains: Vec<String>,
) {
let emval = EmailValidator {
allow_smtputf8: false,
allow_empty_local: false,
allow_quoted_local: false,
allow_domain_literal: false,
deliverable_address: true,
allowed_special_domains: allowed_domains,
};
let result = emval.validate_email(email);
assert!(result.is_ok());
}
#[rstest]
#[case("user@anon.com.test")]
#[case("user@anon.com.invalid")]
fn test_validate_blocked_special_domains_without_allowlist(#[case] email: &str) {
let emval = EmailValidator {
allow_smtputf8: false,
allow_empty_local: false,
allow_quoted_local: false,
allow_domain_literal: false,
deliverable_address: false,
allowed_special_domains: Vec::new(), };
let result = emval.validate_email(email);
assert!(result.is_err());
}
}