feat: email whitelist

This commit is contained in:
Michael H.
2026-08-14 16:00:05 +02:00
parent 4ca48e4589
commit cf50e28dda
2 changed files with 47 additions and 16 deletions
+1
View File
@@ -318,6 +318,7 @@ vars! {
NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL;
EMAIL_DOMAIN_BLACKLIST: StringCsv = StringCsv(vec![]);
EMAIL_DOMAIN_WHITELIST: StringCsv = StringCsv(vec![]);
CLICKHOUSE_REPLICATED: bool = false;
CLICKHOUSE_URL: String = "http://localhost:8123";
+46 -16
View File
@@ -1847,9 +1847,35 @@ impl From<NewAccount> for AccountRegisterFlow {
}
}
/// The bundled disposable domain list is checked first. Environment blacklist
/// entries are matched literally, unless they begin with `*.`, in which case
/// they match any subdomain of the remaining suffix.
#[derive(PartialEq, Eq)]
enum EmailDomainStatus {
Whitelisted,
Neutral,
}
/// Environment list entries are matched literally, unless they begin with `*.`,
/// in which case they match any subdomain of the remaining suffix.
fn matches_domain_entry(domain: &str, entry: &str) -> bool {
let entry = entry.trim().to_ascii_lowercase();
match entry.strip_prefix("*.") {
Some(suffix) => domain
.strip_suffix(suffix)
.is_some_and(|subdomain| subdomain.ends_with('.')),
None => entry == domain,
}
}
fn is_whitelisted_domain(domain: &str) -> bool {
let domain = domain.to_ascii_lowercase();
ENV.EMAIL_DOMAIN_WHITELIST
.iter()
.any(|entry| matches_domain_entry(&domain, entry))
}
/// The bundled disposable domain list is checked first, then the environment
/// blacklist.
fn is_blacklisted_domain(domain: &str) -> bool {
let domain = domain.to_ascii_lowercase();
@@ -1863,35 +1889,39 @@ fn is_blacklisted_domain(domain: &str) -> bool {
return true;
}
ENV.EMAIL_DOMAIN_BLACKLIST.iter().any(|entry| {
let entry = entry.trim().to_ascii_lowercase();
match entry.strip_prefix("*.") {
Some(suffix) => domain
.strip_suffix(suffix)
.is_some_and(|subdomain| subdomain.ends_with('.')),
None => entry == domain,
}
})
ENV.EMAIL_DOMAIN_BLACKLIST
.iter()
.any(|entry| matches_domain_entry(&domain, entry))
}
fn ensure_email_domain_is_allowed(email: &str) -> Result<(), ApiError> {
fn ensure_email_domain_is_allowed(
email: &str,
) -> Result<EmailDomainStatus, ApiError> {
let Some((_, domain)) = email.rsplit_once('@') else {
return Err(ApiError::Request(email_check_error_generic()));
};
if is_whitelisted_domain(domain) {
info!(email.domain = domain, "whitelisted email domain, allowing");
return Ok(EmailDomainStatus::Whitelisted);
}
if is_blacklisted_domain(domain) {
info!(email.domain = domain, "blacklisted email domain, denying");
return Err(ApiError::Request(email_check_error_generic()));
}
Ok(())
Ok(EmailDomainStatus::Neutral)
}
async fn ensure_email_is_usable(email: &str) -> Result<(), ApiError> {
ensure_email_domain_is_allowed(email)
let status = ensure_email_domain_is_allowed(email)
.wrap_api_err("validating email domain is allowed")?;
if status == EmailDomainStatus::Whitelisted {
return Ok(());
}
let result = check_email(email)
.await
.wrap_request_err("checking email address")?;