disposable-emails 0.1.1

Fast, zero-allocation disposable / temporary email domain detection with an embedded 72k-domain blocklist.
Documentation
//! Disposable / temporary email domain detection.
//!
//! Tells you whether an email address (or just its domain) belongs to a
//! disposable / temp-mail provider — the throwaway inboxes used to farm free
//! trials, dodge per-account limits, and spam sign-up flows.
//!
//! ```
//! use disposable_emails::{is_disposable_email, is_disposable_domain};
//!
//! assert!(is_disposable_email("someone@mailinator.com"));
//! assert!(is_disposable_domain("guerrillamail.com"));
//! assert!(!is_disposable_email("jeff@gmail.com"));
//! ```
//!
//! # Why it's cheap
//!
//! The ~72,000-domain blocklist is compiled into a **sorted
//! `&'static [&'static str]`** by the build script. That means:
//!
//! - **zero heap allocation** — the list lives in the binary's read-only
//!   segment; there is no `HashSet` to build,
//! - **zero startup cost** — nothing is parsed or loaded at runtime,
//! - lookups are an `O(log n)` binary search (~17 comparisons).
//!
//! The only allocation per call is a small lowercase copy of the *input*
//! domain (tens of bytes).
//!
//! # The list
//!
//! `domains.txt` is a vendored copy of the community-maintained
//! [`disposable`](https://github.com/disposable/disposable-email-domains)
//! aggregate. Refresh it by replacing that file and rebuilding.
//!
//! # Licence
//!
//! Dual-licensed under Apache-2.0 OR MIT.

#![forbid(unsafe_code)]

include!(concat!(env!("OUT_DIR"), "/domains_generated.rs"));

/// The number of disposable domains in the embedded blocklist.
#[inline]
pub fn domain_count() -> usize {
    DOMAINS.len()
}

/// Returns `true` if `domain` is a known disposable / temp-mail domain.
///
/// Input is matched case-insensitively; a single trailing `.` (a
/// fully-qualified domain) is tolerated. Subdomains are **not** walked — pass
/// the registrable domain.
///
/// ```
/// assert!(disposable_emails::is_disposable_domain("Mailinator.com"));
/// assert!(!disposable_emails::is_disposable_domain("spider.cloud"));
/// ```
pub fn is_disposable_domain(domain: &str) -> bool {
    let d = domain.trim().trim_end_matches('.').to_ascii_lowercase();
    if d.is_empty() {
        return false;
    }
    DOMAINS.binary_search(&d.as_str()).is_ok()
}

/// Returns `true` if the email address's domain is disposable.
///
/// Malformed input (no `@`, empty domain) returns `false` rather than erroring
/// — callers that need strict validation should check the address shape first.
///
/// ```
/// assert!(disposable_emails::is_disposable_email("x@10minutemail.com"));
/// assert!(!disposable_emails::is_disposable_email("not-an-email"));
/// ```
pub fn is_disposable_email(email: &str) -> bool {
    match email.rsplit_once('@') {
        Some((_, domain)) if !domain.is_empty() => is_disposable_domain(domain),
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn list_is_substantial_and_sorted() {
        assert!(domain_count() > 50_000, "blocklist looks too small");
        assert!(
            DOMAINS.windows(2).all(|w| w[0] < w[1]),
            "DOMAINS must be sorted + deduped for binary_search"
        );
    }

    #[test]
    fn flags_known_disposable() {
        for d in ["mailinator.com", "guerrillamail.com", "10minutemail.com"] {
            assert!(is_disposable_domain(d), "{d} should be disposable");
        }
    }

    #[test]
    fn allows_real_domains() {
        for d in ["gmail.com", "spider.cloud", "gottem.dev", "outlook.com"] {
            assert!(!is_disposable_domain(d), "{d} should not be disposable");
        }
    }

    #[test]
    fn case_and_trailing_dot_insensitive() {
        assert!(is_disposable_domain("MAILINATOR.COM"));
        assert!(is_disposable_domain("mailinator.com."));
    }

    #[test]
    fn email_form() {
        assert!(is_disposable_email("a@mailinator.com"));
        assert!(is_disposable_email("weird+tag@guerrillamail.com"));
        assert!(!is_disposable_email("jeff@gmail.com"));
        assert!(!is_disposable_email("garbage"));
        assert!(!is_disposable_email("trailing@"));
    }
}