disposable_emails/lib.rs
1//! Disposable / temporary email domain detection.
2//!
3//! Tells you whether an email address (or just its domain) belongs to a
4//! disposable / temp-mail provider — the throwaway inboxes used to farm free
5//! trials, dodge per-account limits, and spam sign-up flows.
6//!
7//! ```
8//! use disposable_emails::{is_disposable_email, is_disposable_domain};
9//!
10//! assert!(is_disposable_email("someone@mailinator.com"));
11//! assert!(is_disposable_domain("guerrillamail.com"));
12//! assert!(!is_disposable_email("jeff@gmail.com"));
13//! ```
14//!
15//! # Why it's cheap
16//!
17//! The ~72,000-domain blocklist is compiled into a **sorted
18//! `&'static [&'static str]`** by the build script. That means:
19//!
20//! - **zero heap allocation** — the list lives in the binary's read-only
21//! segment; there is no `HashSet` to build,
22//! - **zero startup cost** — nothing is parsed or loaded at runtime,
23//! - lookups are an `O(log n)` binary search (~17 comparisons).
24//!
25//! The only allocation per call is a small lowercase copy of the *input*
26//! domain (tens of bytes).
27//!
28//! # The list
29//!
30//! `domains.txt` is a vendored copy of the community-maintained
31//! [`disposable`](https://github.com/disposable/disposable-email-domains)
32//! aggregate. Refresh it by replacing that file and rebuilding.
33//!
34//! # Licence
35//!
36//! Dual-licensed under Apache-2.0 OR MIT.
37
38#![forbid(unsafe_code)]
39
40include!(concat!(env!("OUT_DIR"), "/domains_generated.rs"));
41
42/// The number of disposable domains in the embedded blocklist.
43#[inline]
44pub fn domain_count() -> usize {
45 DOMAINS.len()
46}
47
48/// Returns `true` if `domain` is a known disposable / temp-mail domain.
49///
50/// Input is matched case-insensitively; a single trailing `.` (a
51/// fully-qualified domain) is tolerated. Subdomains are **not** walked — pass
52/// the registrable domain.
53///
54/// ```
55/// assert!(disposable_emails::is_disposable_domain("Mailinator.com"));
56/// assert!(!disposable_emails::is_disposable_domain("spider.cloud"));
57/// ```
58pub fn is_disposable_domain(domain: &str) -> bool {
59 let d = domain.trim().trim_end_matches('.').to_ascii_lowercase();
60 if d.is_empty() {
61 return false;
62 }
63 DOMAINS.binary_search(&d.as_str()).is_ok()
64}
65
66/// Returns `true` if the email address's domain is disposable.
67///
68/// Malformed input (no `@`, empty domain) returns `false` rather than erroring
69/// — callers that need strict validation should check the address shape first.
70///
71/// ```
72/// assert!(disposable_emails::is_disposable_email("x@10minutemail.com"));
73/// assert!(!disposable_emails::is_disposable_email("not-an-email"));
74/// ```
75pub fn is_disposable_email(email: &str) -> bool {
76 match email.rsplit_once('@') {
77 Some((_, domain)) if !domain.is_empty() => is_disposable_domain(domain),
78 _ => false,
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn list_is_substantial_and_sorted() {
88 assert!(domain_count() > 50_000, "blocklist looks too small");
89 assert!(
90 DOMAINS.windows(2).all(|w| w[0] < w[1]),
91 "DOMAINS must be sorted + deduped for binary_search"
92 );
93 }
94
95 #[test]
96 fn flags_known_disposable() {
97 for d in ["mailinator.com", "guerrillamail.com", "10minutemail.com"] {
98 assert!(is_disposable_domain(d), "{d} should be disposable");
99 }
100 }
101
102 #[test]
103 fn allows_real_domains() {
104 for d in ["gmail.com", "spider.cloud", "gottem.dev", "outlook.com"] {
105 assert!(!is_disposable_domain(d), "{d} should not be disposable");
106 }
107 }
108
109 #[test]
110 fn case_and_trailing_dot_insensitive() {
111 assert!(is_disposable_domain("MAILINATOR.COM"));
112 assert!(is_disposable_domain("mailinator.com."));
113 }
114
115 #[test]
116 fn email_form() {
117 assert!(is_disposable_email("a@mailinator.com"));
118 assert!(is_disposable_email("weird+tag@guerrillamail.com"));
119 assert!(!is_disposable_email("jeff@gmail.com"));
120 assert!(!is_disposable_email("garbage"));
121 assert!(!is_disposable_email("trailing@"));
122 }
123}