feat: bring back email domain blacklist

This commit is contained in:
Michael H.
2026-08-31 15:08:13 +02:00
parent 306bde7155
commit 3fdcf73d11
2 changed files with 50 additions and 9 deletions
+3 -1
View File
@@ -314,7 +314,7 @@ vars! {
SENDY_LIST_ID: String = "none"; SENDY_LIST_ID: String = "none";
SENDY_API_KEY: String = "none"; SENDY_API_KEY: String = "none";
SKIP_EMAIL_CHECK_DOMAINS: String = ""; SKIP_EMAIL_CHECK_DOMAINS: StringCsv = StringCsv(vec![]);
NEVERBOUNCE_API_KEY: String = ""; NEVERBOUNCE_API_KEY: String = "";
NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL; NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL;
@@ -323,6 +323,8 @@ vars! {
USERCHECK_GATE_ID: String = ""; USERCHECK_GATE_ID: String = "";
USERCHECK_BASE_URL: String = crate::util::usercheck::DEFAULT_API_URL; USERCHECK_BASE_URL: String = crate::util::usercheck::DEFAULT_API_URL;
EMAIL_DOMAIN_BLACKLIST: StringCsv = StringCsv(vec![]);
CLICKHOUSE_REPLICATED: bool = false; CLICKHOUSE_REPLICATED: bool = false;
CLICKHOUSE_URL: String = "http://localhost:8123"; CLICKHOUSE_URL: String = "http://localhost:8123";
CLICKHOUSE_USER: String = "default"; CLICKHOUSE_USER: String = "default";
+47 -8
View File
@@ -1901,17 +1901,54 @@ impl From<NewAccount> for AccountRegisterFlow {
} }
} }
/// Domains listed in `SKIP_EMAIL_CHECK_DOMAINS` bypass every email check. /// 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 domain_of(email: &str) -> Option<String> {
email
.rsplit_once('@')
.map(|(_, domain)| domain.to_ascii_lowercase())
}
fn domain_matches_list(domain: &str, list: &[String]) -> bool {
list.iter().any(|entry| matches_domain_entry(domain, entry))
}
/// Domains listed in `SKIP_EMAIL_CHECK_DOMAINS` bypass every email check,
/// including the blacklist.
fn email_checks_skipped(email: &str) -> bool { fn email_checks_skipped(email: &str) -> bool {
let Some((_, domain)) = email.rsplit_once('@') else { domain_of(email).is_some_and(|domain| {
return false; domain_matches_list(&domain, &ENV.SKIP_EMAIL_CHECK_DOMAINS)
})
}
fn ensure_email_domain_is_not_blacklisted(email: &str) -> Result<(), ApiError> {
let Some(domain) = domain_of(email) else {
return Ok(());
}; };
ENV.SKIP_EMAIL_CHECK_DOMAINS if !domain_matches_list(&domain, &ENV.EMAIL_DOMAIN_BLACKLIST) {
.split(',') return Ok(());
.map(str::trim) }
.filter(|skipped| !skipped.is_empty())
.any(|skipped| skipped.eq_ignore_ascii_case(domain)) info!(
email.domain = domain.as_str(),
"blacklisted email domain, denying",
);
Err(ApiError::Request(eyre!(
"This domain name may not be used ({domain})!"
)))
} }
/// Runs the UserCheck gate, which covers both password and OAuth signups. /// Runs the UserCheck gate, which covers both password and OAuth signups.
@@ -1923,6 +1960,8 @@ async fn ensure_email_passes_gate(
return Ok(()); return Ok(());
} }
ensure_email_domain_is_not_blacklisted(email)?;
let action = check_email_gate(redis, email) let action = check_email_gate(redis, email)
.await .await
.wrap_request_err("checking email address")?; .wrap_request_err("checking email address")?;