#[derive(Debug, Clone)]
pub struct ProviderRule {
domains: Vec<Box<str>>,
strip_dots: bool,
lowercase_local: bool,
subaddress_sep: Option<char>,
is_freemail: bool,
}
impl ProviderRule {
pub fn new<I, S>(domains: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
domains: domains
.into_iter()
.map(|d| canonical_domain(&d.into()))
.collect(),
strip_dots: false,
lowercase_local: false,
subaddress_sep: Some('+'),
is_freemail: false,
}
}
#[must_use]
pub fn strip_dots(mut self, yes: bool) -> Self {
self.strip_dots = yes;
self
}
#[must_use]
pub fn lowercase_local(mut self, yes: bool) -> Self {
self.lowercase_local = yes;
self
}
#[must_use]
pub fn subaddress_separator(mut self, sep: Option<char>) -> Self {
self.subaddress_sep = sep;
self
}
#[must_use]
pub fn freemail(mut self, yes: bool) -> Self {
self.is_freemail = yes;
self
}
pub fn matches(&self, domain: &str) -> bool {
self.matches_canonical(&canonical_domain(domain))
}
fn matches_canonical(&self, canonical: &str) -> bool {
self.domains.iter().any(|d| &**d == canonical)
}
pub fn strips_dots(&self) -> bool {
self.strip_dots
}
pub fn folds_case(&self) -> bool {
self.lowercase_local
}
pub fn separator(&self) -> Option<char> {
self.subaddress_sep
}
pub fn is_freemail(&self) -> bool {
self.is_freemail
}
}
#[derive(Debug, Clone)]
pub struct ProviderRegistry {
rules: Vec<ProviderRule>,
}
static BUILTIN: std::sync::LazyLock<ProviderRegistry> = std::sync::LazyLock::new(|| {
let p = |domains: &[&str]| {
ProviderRule::new(domains.iter().copied())
.lowercase_local(true)
.freemail(true)
};
ProviderRegistry {
rules: vec![
p(&["gmail.com", "googlemail.com"]).strip_dots(true),
p(&["outlook.com", "hotmail.com", "live.com", "msn.com"]),
p(&["yahoo.com", "yahoo.co.uk", "yahoo.co.jp"]),
p(&["protonmail.com", "proton.me"]),
p(&["icloud.com", "me.com", "mac.com"]),
p(&["yandex.ru", "yandex.com"]),
p(&["mail.ru"]),
p(&[
"aol.com",
"mail.com",
"zoho.com",
"gmx.com",
"gmx.de",
"web.de",
"tutanota.com",
"tuta.io",
"fastmail.com",
]),
],
}
});
pub(crate) fn builtin_ref() -> &'static ProviderRegistry {
&BUILTIN
}
impl ProviderRegistry {
pub fn empty() -> Self {
Self { rules: Vec::new() }
}
pub fn builtin() -> Self {
builtin_ref().clone()
}
pub fn add(&mut self, rule: ProviderRule) {
self.rules.push(rule);
}
#[must_use]
pub fn with(mut self, rule: ProviderRule) -> Self {
self.add(rule);
self
}
pub fn lookup(&self, domain: &str) -> Option<&ProviderRule> {
let canonical = canonical_domain(domain);
self.rules
.iter()
.rev()
.find(|r| r.matches_canonical(&canonical))
}
}
impl Default for ProviderRegistry {
fn default() -> Self {
Self::builtin()
}
}
fn canonical_domain(domain: &str) -> Box<str> {
idna::domain_to_ascii(domain)
.unwrap_or_else(|_| domain.to_ascii_lowercase())
.into_boxed_str()
}
#[cfg(test)]
mod tests;