Skip to content

Commit

Permalink
Merge pull request #9 from denehoffman/rust-crate
Browse files Browse the repository at this point in the history
feat: expose Rust API
  • Loading branch information
bnkc authored Dec 5, 2024
2 parents f014235 + f31b7ee commit 49df9e9
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 37 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Release-plz

permissions:
pull-requests: write
contents: write

on:
push:
branches:
- main

jobs:

# Release unpublished packages.
release-plz-release:
name: Release-plz release
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: release-plz/[email protected]
with:
command: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

# Create a PR with the new versions and changelog, preparing the next release.
release-plz-pr:
name: Release-plz PR
runs-on: ubuntu-latest
concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: release-plz/[email protected]
with:
command: release-pr
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ name = "emval"
version = "0.1.4"
description = "emval is a blazingly fast email validator"
edition = "2021"
license-file = "LICENSE"
homepage = "https://github.com/bnkc/emval"
repository = "https://github.com/bnkc/emval"
readme = "README.md"
keywords = ["email", "validation"]
categories = ["email"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "_emval"
crate-type = ["cdylib"]
name = "emval"
crate-type = ["cdylib", "rlib"]

[dependencies]
pyo3 = "0.23.0"
Expand Down
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 📬 emval

`emval` is a blazingly fast Python email validator written in Rust, offering performance improvements of 100-1000x over traditional validators.
`emval` is a blazingly fast email validator written in Rust with Python bindings, offering performance improvements of 100-1000x over traditional validators.

![performance image](https://raw.githubusercontent.com/bnkc/emval/b90cc4a0ae24e329702872c4fb1cccf212d556a6/perf.svg)

Expand All @@ -23,11 +23,16 @@ Install `emval` from PyPI:
pip install emval
```

or use `emval` in a Rust project:
```sh
cargo add emval
```

## Usage

### Quick Start

To validate an email address:
To validate an email address in Python:

```python
from emval import validate_email, EmailValidator
Expand All @@ -44,6 +49,18 @@ except Exception as e:
print(str(e))
```

The same code in Rust:
```rust
use emval::{validate_email, ValidationError};

fn main() -> Result<(), ValidationError> {
let email = "[email protected]";
let val_email = validate_email(email)?;
let normalized_email = val_email.normalized;
Ok(())
}
```

### Configurations

Customize email validation behavior using the `EmailValidator` class:
Expand All @@ -68,6 +85,25 @@ except Exception as e:
print(str(e))
```

The same code in Rust:
```rust
use emval::{EmailValidator, ValidationError};

fn main() -> Result<(), ValidationError> {
let emval = EmailValidator {
allow_smtputf8: false,
allow_empty_local: true,
allow_quoted_local: true,
allow_domain_literal: true,
deliverable_address: false,
};

let email = "[email protected]";
let validated_email = emval.validate_email(email)?;
Ok(())
}
```

### Options

- `allow_smtputf8`: Allows internationalized email addresses.
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ classifiers = [
]
dynamic = ["version"]

[project.optional-dependencies]
tests = ["pytest"]

[tool.maturin]
module-name = "emval._emval"
features = ["pyo3/extension-module"]
Expand Down
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use pyo3::exceptions::{PySyntaxError, PyValueError};
use pyo3::prelude::*;

/// An error enum for email validation.
#[derive(Debug)]
pub enum ValidationError {
/// A syntax error.
SyntaxError(String),
/// An error involving some input value.
ValueError(String),
}

Expand Down
165 changes: 164 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,174 @@
//! # 📬 emval
//!
//! `emval` is a blazingly fast email validator written in Rust with Python bindings, offering performance improvements of 100-1000x over traditional validators.
//!
//! ![performance image](https://raw.githubusercontent.com/bnkc/emval/b90cc4a0ae24e329702872c4fb1cccf212d556a6/perf.svg)
//! ## Features
//! - Drop-in replacement for popular email validators like `python-email-validator`, `verify-email`, and `pyIsEmail`.
//! - 100-1000x faster than [python-email-validator](https://github.com/JoshData/python-email-validator).
//! - Validates email address syntax according to [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322.html) and [RFC 6531](https://www.rfc-editor.org/rfc/rfc6531.html).
//! - Checks domain deliverability (coming soon).
//! - Supports internationalized domain names (IDN) and local parts.
//! - Provides user-friendly syntax errors.
//! - Normalizes addresses.
//! - Rejects invalid and unsafe Unicode characters.
//!
//! ## Getting Started
//!
//! Install `emval` from PyPI:
//!
//! ```sh
//! pip install emval
//! ```
//!
//! or use `emval` in a Rust project:
//! ```sh
//! cargo add emval
//! ```
//!
//! ## Usage
//!
//! ### Quick Start
//!
//! To validate an email address in Python:
//!
//! ```python
//! from emval import validate_email, EmailValidator
//!
//! email = "[email protected]"
//!
//! try:
//! # Check if the email is valid.
//! val_email = validate_email(email)
//! # Utilize the normalized form for storage.
//! normalized_email = val_email.normalized
//! except Exception as e:
//! # Example: "Invalid Local Part: Quoting the local part before the '@' sign is not permitted in this context."
//! print(str(e))
//! ```
//!
//! The same code in Rust:
//! ```rust
//! use emval::{validate_email, ValidationError};
//!
//! fn main() -> Result<(), ValidationError> {
//! let email = "[email protected]";
//! let val_email = validate_email(email)?;
//! let normalized_email = val_email.normalized;
//! Ok(())
//! }
//! ```
//!
//! ### Configurations
//!
//! Customize email validation behavior using the `EmailValidator` class:
//!
//! ```python
//! from emval import EmailValidator
//!
//! emval = EmailValidator(
//! allow_smtputf8=False,
//! allow_empty_local=True,
//! allow_quoted_local=True,
//! allow_domain_literal=True,
//! deliverable_address=False,
//! )
//!
//! email = "user@[192.168.1.1]"
//!
//! try:
//! validated_email = emval.validate_email(email)
//! print(validated_email)
//! except Exception as e:
//! print(str(e))
//! ```
//!
//! The same code in Rust:
//! ```rust
//! use emval::{EmailValidator, ValidationError};
//!
//! fn main() -> Result<(), ValidationError> {
//! let emval = EmailValidator {
//! allow_smtputf8: false,
//! allow_empty_local: true,
//! allow_quoted_local: true,
//! allow_domain_literal: true,
//! deliverable_address: false,
//! };
//!
//! let email = "[email protected]";
//! let validated_email = emval.validate_email(email)?;
//! Ok(())
//! }
//! ```
//!
//! ### Options
//!
//! - `allow_smtputf8`: Allows internationalized email addresses.
//! - `allow_empty_local`: Allows an empty local part (e.g., `@domain.com`).
//! - `allow_quoted_local`: Allows quoted local parts (e.g., `"user name"@domain.com`).
//! - `allow_domain_literal`: Allows domain literals (e.g., `[192.168.0.1]`).
//! - `deliverable_address`: Checks if the email address is deliverable by verifying the domain's MX records.
//!
//! ## Technical Details
//!
//! ### Email Address Syntax
//!
//! emval adheres to the syntax rules defined in [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322.html) and [RFC 6531](https://www.rfc-editor.org/rfc/rfc6531.html). It supports both ASCII and internationalized characters.
//!
//! ### Internationalized Email Addresses
//!
//! #### Domain Names
//!
//! emval converts non-ASCII domain names into their ASCII "Punycode" form according to [IDNA 2008](https://www.rfc-editor.org/rfc/rfc5891.html). This ensures compatibility with systems that do not support Unicode.
//!
//! #### Local Parts
//!
//! emval allows international characters in the local part of email addresses, following [RFC 6531](https://www.rfc-editor.org/rfc/rfc6531.html). It offers options to handle environments without SMTPUTF8 support.
//!
//! ### Unsafe Unicode Characters
//!
//! emval rejects unsafe Unicode characters to enhance security, preventing display and interpretation issues.
//!
//! ### Normalization
//!
//! emval normalizes email addresses to ensure consistency:
//!
//! - **Lowercasing domains:** Domain names are standardized to lowercase.
//! - **Unicode NFC normalization:** Characters are transformed into their precomposed forms.
//! - **Removing unnecessary characters:** Quotes and backslashes in the local part are removed.
//!
//! ## Acknowledgements
//!
//! This project draws inspiration from [python-email-validator](https://github.com/JoshData/python-email-validator). While `python-email-validator` is more comprehensive, `emval` aims to provide a faster solution.
//!
//! ## Getting Help
//!
//! For questions and issues, please open an issue in the [GitHub issue tracker](https://github.com/bnkc/emval/issues).
//!
//! ## License
//!
//! emval is licensed under the [MIT License](https://opensource.org/licenses/MIT). See the [LICENSE](https://github.com/bnkc/emval/blob/main/LICENSE) file for more details.
#![feature(ip)]
#[macro_use]
extern crate lazy_static;
mod consts;
mod errors;
pub mod errors;
mod models;
mod validators;

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

/// Validate an email with default validator settings.
pub fn validate_email<T: AsRef<str>>(email: T) -> Result<ValidatedEmail, ValidationError> {
let validator = EmailValidator::default();
validator.validate_email(email.as_ref())
}

use pyo3::prelude::*;

#[pymodule]
Expand Down
26 changes: 25 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,52 @@
use pyo3::prelude::*;
use std::net::IpAddr;

/// A structure representing a validated email address with various components and normalized forms.
#[pyclass]
pub struct ValidatedEmail {
/// The email address provided to validate_email.
#[pyo3(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)]
pub normalized: String,
/// The local part of the email address (the part before the '@' sign) after it has been Unicode normalized.
#[pyo3(get)]
pub local_part: String,
/// If the domain part is a domain literal, it will be an IPv4Address or IPv6Address object.
#[pyo3(get)]
pub domain_address: Option<IpAddr>,
/// The domain part of the email address (the part after the '@' sign) after Unicode normalization.
#[pyo3(get)]
pub domain_name: String,
/// Whether the email address is deliverable.
#[pyo3(get)]
pub is_deliverable: bool,
}

#[derive(Default)]
/// A structure for customizing email validation.
#[pyclass]
pub struct EmailValidator {
/// Whether to allow SMTPUTF8. [Default: true]
pub allow_smtputf8: bool,
/// Whether to allow empty local part. [Default: false]
pub allow_empty_local: bool,
/// Whether to allow quoted local part. [Default: false]
pub allow_quoted_local: bool,
/// Whether to allow domain literals. [Default: false]
pub allow_domain_literal: bool,
/// Whether to check if the email address is deliverable. [Default: true]
pub deliverable_address: bool,
}

impl Default for EmailValidator {
fn default() -> Self {
Self {
allow_smtputf8: true,
allow_empty_local: false,
allow_quoted_local: false,
allow_domain_literal: false,
deliverable_address: true,
}
}
}
Loading

0 comments on commit 49df9e9

Please sign in to comment.