use alloc::borrow::Cow;
use alloc::string::String;
#[derive(Debug, Clone)]
pub struct ProviderRule {
domains: Cow<'static, [Cow<'static, 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: Cow::Owned(
domains
.into_iter()
.map(|d| Cow::Owned(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: Cow<'static, [ProviderRule]>,
}
macro_rules! domains {
($($d:literal),+ $(,)?) => {
&[$(Cow::Borrowed($d)),+]
};
}
const fn builtin(domains: &'static [Cow<'static, str>], strip_dots: bool) -> ProviderRule {
ProviderRule {
domains: Cow::Borrowed(domains),
strip_dots,
lowercase_local: true,
subaddress_sep: Some('+'),
is_freemail: true,
}
}
static BUILTIN: ProviderRegistry = ProviderRegistry {
rules: Cow::Borrowed(&[
builtin(domains!["gmail.com", "googlemail.com"], true),
builtin(
domains!["outlook.com", "hotmail.com", "live.com", "msn.com"],
false,
),
builtin(domains!["yahoo.com", "yahoo.co.uk", "yahoo.co.jp"], false),
builtin(domains!["protonmail.com", "proton.me"], false),
builtin(domains!["icloud.com", "me.com", "mac.com"], false),
builtin(domains!["yandex.ru", "yandex.com"], false),
builtin(domains!["mail.ru"], false),
builtin(
domains![
"aol.com",
"mail.com",
"zoho.com",
"gmx.com",
"gmx.de",
"web.de",
"tutanota.com",
"tuta.io",
"fastmail.com",
],
false,
),
]),
};
pub(crate) fn builtin_ref() -> &'static ProviderRegistry {
&BUILTIN
}
impl ProviderRegistry {
pub fn empty() -> Self {
Self {
rules: Cow::Borrowed(&[]),
}
}
pub fn builtin() -> Self {
builtin_ref().clone()
}
pub fn add(&mut self, rule: ProviderRule) {
self.rules.to_mut().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) -> String {
idna::domain_to_ascii(domain).unwrap_or_else(|_| domain.to_ascii_lowercase())
}
#[cfg(test)]
mod tests;