Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 16 additions & 29 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 23 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ name = "emval"
crate-type = ["cdylib", "rlib"]

[dependencies]
pyo3 = { version = "=0.25.1", features = ["extension-module", "abi3-py38"] }
pyo3 = { version = "=0.25.1", features = [
"extension-module",
"abi3-py38",
], optional = true }

# emval dependencies
icu_normalizer = "2.0.0"
Expand All @@ -26,14 +29,29 @@ regex = "1.10.5"
unicode-properties = "0.1.1"
rstest = "0.21.0"
unicode_names2 = "1.2.2"
trust-dns-resolver = "0.23.2"
trust-dns-resolver = { version = "0.23.2", optional = true }

# WASM dependencies
wasm-bindgen = { version = "0.2", optional = true }

# polars dependencies
pyo3-polars = { version = "0.22", features = ["derive", "dtype-struct"] }
polars = { version = "0.49.1", features = ["dtype-struct"], default-features = false }
polars-arrow = { version = "0.49.1", default-features = false }
pyo3-polars = { version = "0.22", features = [
"derive",
"dtype-struct",
], optional = true }
polars = { version = "0.49.1", features = [
"dtype-struct",
], default-features = false, optional = true }
polars-arrow = { version = "0.49.1", default-features = false, optional = true }
serde = { version = "1", features = ["derive"] }

[features]
default = ["python", "polars"]
python = ["dep:pyo3", "dep:pyo3-polars", "dep:polars", "dep:polars-arrow"]
polars = ["dep:pyo3-polars", "dep:polars", "dep:polars-arrow", "dns"]
dns = ["dep:trust-dns-resolver"]
wasm = ["dep:wasm-bindgen"]

[profile.release]
lto = true

18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "emval",
"version": "0.1.12",
"type": "module",
"main": "js/emval.js",
"types": "js/emval.d.ts",
"files": [
"js/"
],
"scripts": {
"cargo:debug": "cargo build --no-default-features --features wasm --target wasm32-unknown-unknown",
"cargo": "cargo build --no-default-features --features wasm --target wasm32-unknown-unknown --release",
"wasm": "wasm-bindgen target/wasm32-unknown-unknown/release/emval.wasm --out-dir js --target bundler",
"wasm:debug": "wasm-bindgen target/wasm32-unknown-unknown/debug/emval.wasm --out-dir js --target bundler",
"build:debug": "npm run cargo:debug && npm run wasm:debug",
"build": "npm run cargo && npm run wasm"
}
}
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#[cfg(feature = "python")]
use pyo3::exceptions::{PySyntaxError, PyValueError};
#[cfg(feature = "python")]
use pyo3::prelude::*;

/// An error enum for email validation.
Expand All @@ -10,6 +12,7 @@ pub enum ValidationError {
ValueError(String),
}

#[cfg(feature = "python")]
impl From<ValidationError> for PyErr {
fn from(err: ValidationError) -> Self {
match err {
Expand Down
17 changes: 17 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,16 @@
mod consts;
pub mod errors;
mod models;
#[cfg(feature = "polars")]
mod polars_plugin;
pub(crate) mod util;
mod validators;

#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;



pub use crate::errors::ValidationError;
pub use crate::models::{EmailValidator, ValidatedEmail};

Expand All @@ -169,8 +175,19 @@ pub fn validate_email<T: AsRef<str>>(email: T) -> Result<ValidatedEmail, Validat
validator.validate_email(email.as_ref())
}

#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn validate_email_wasm(email: String) -> Result<String, String> {
match validate_email(&email) {
Ok(validated) => Ok(validated.normalized),
Err(e) => Err(e.to_string()),
}
}

#[cfg(feature = "python")]
use pyo3::prelude::*;

#[cfg(feature = "python")]
#[pymodule]
fn _emval(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {
m.add_class::<models::EmailValidator>()?;
Expand Down
21 changes: 11 additions & 10 deletions src/models.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
#[cfg(feature = "python")]
use pyo3::prelude::*;
use std::net::IpAddr;

/// A structure representing a validated email address with various components and normalized forms.
#[pyclass]
#[cfg_attr(feature = "python", pyclass)]
pub struct ValidatedEmail {
/// The email address provided to validate_email.
#[pyo3(get)]
#[cfg_attr(feature = "python", get)]
pub original: String,
/// The normalized email address should be used instead of the original. It converts IDNA ASCII domain names to Unicode and normalizes both the local part and domain. The normalized address combines the local part and domain name with an '@' sign.
#[pyo3(get)]
#[cfg_attr(feature = "python", get)]
pub normalized: String,
/// The ASCII (Punycode-encoded) form of the email address, if one exists.
#[pyo3(get)]
#[cfg_attr(feature = "python", get)]
pub ascii_email: Option<String>,
/// The local part of the email address (the part before the '@' sign) after it has been Unicode normalized.
#[pyo3(get)]
#[cfg_attr(feature = "python", get)]
pub local_part: String,
/// If the domain part is a domain literal, it will be an IPv4Address or IPv6Address object.
#[pyo3(get)]
#[cfg_attr(feature = "python", get)]
pub domain_address: Option<IpAddr>,
/// The domain part of the email address (the part after the '@' sign) after Unicode normalization.
#[pyo3(get)]
#[cfg_attr(feature = "python", get)]
pub domain_name: String,
/// The ASCII (Punycode-encoded) form of the domain part of the email address.
#[pyo3(get)]
#[cfg_attr(feature = "python", get)]
pub ascii_domain: String,
/// Whether the email address is deliverable.
#[pyo3(get)]
#[cfg_attr(feature = "python", get)]
pub is_deliverable: bool,
}

/// A structure for customizing email validation.
#[pyclass]
#[cfg_attr(feature = "python", pyclass)]
pub struct EmailValidator {
/// Whether to allow SMTPUTF8. [Default: true]
pub allow_smtputf8: bool,
Expand Down
1 change: 1 addition & 0 deletions src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#[cfg(feature = "dns")]
pub mod ip_addr_ext;
10 changes: 10 additions & 0 deletions src/validators/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ use idna::uts46::Uts46;
use idna::uts46::{AsciiDenyList, DnsLength, Hyphens};
use std::net::IpAddr;
use std::str::FromStr;
#[cfg(feature = "dns")]
use trust_dns_resolver::config::*;
#[cfg(feature = "dns")]
use trust_dns_resolver::Resolver;
#[cfg(feature = "dns")]
use crate::util::ip_addr_ext::IpAddrExt;

pub fn validate_domain(
Expand Down Expand Up @@ -193,6 +196,7 @@ pub fn validate_domain(
}
}

#[cfg(feature = "dns")]
pub fn validate_deliverability(domain: &str) -> Result<(), ValidationError> {
let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default())
.map_err(|e| ValidationError::SyntaxError(e.to_string()))?;
Expand Down Expand Up @@ -244,6 +248,12 @@ pub fn validate_deliverability(domain: &str) -> Result<(), ValidationError> {
))
}

#[cfg(not(feature = "dns"))]
pub fn validate_deliverability(_domain: &str) -> Result<(), ValidationError> {
// DNS resolution is not available when dns feature is disabled
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 2 additions & 0 deletions src/validators/email.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::models::{EmailValidator, ValidatedEmail};
#[cfg(feature = "python")]
use pyo3::prelude::*;

impl EmailValidator {
Expand Down Expand Up @@ -55,6 +56,7 @@ impl EmailValidator {
}
}

#[cfg(feature = "python")]
#[pymethods]
impl EmailValidator {
/// Create a new email validator with the given settings.
Expand Down
Loading