From 32109ad7375654cd42713ae63e2afc50f11a81ad Mon Sep 17 00:00:00 2001 From: "Michael H." Date: Mon, 20 Jul 2026 11:33:18 +0200 Subject: [PATCH 1/2] feat: email blacklist --- apps/labrinth/src/env.rs | 2 ++ apps/labrinth/src/routes/internal/flows.rs | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 8cd0984819..e403be77ca 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -238,6 +238,8 @@ vars! { NEVERBOUNCE_API_KEY: String = ""; NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL; + EMAIL_DOMAIN_BLACKLIST: StringCsv = StringCsv(vec![]); + CLICKHOUSE_REPLICATED: bool = false; CLICKHOUSE_URL: String = "http://localhost:8123"; CLICKHOUSE_USER: String = "default"; diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 637a0af346..e7152be691 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1816,7 +1816,27 @@ impl From for AccountRegisterFlow { } } +fn ensure_email_domain_is_allowed(email: &str) -> Result<(), ApiError> { + let Some((_, domain)) = email.rsplit_once('@') else { + return Err(ApiError::Request(email_check_error_generic())); + }; + + let is_blacklisted = ENV + .EMAIL_DOMAIN_BLACKLIST + .iter() + .any(|blacklisted| blacklisted.trim().eq_ignore_ascii_case(domain)); + + if is_blacklisted { + info!(email.domain = domain, "blacklisted email domain, denying"); + return Err(ApiError::Request(email_check_error_generic())); + } + + Ok(()) +} + async fn ensure_email_is_usable(email: &str) -> Result<(), ApiError> { + ensure_email_domain_is_allowed(email)?; + let result = check_email(email).await.map_err(ApiError::Request)?; if matches!( From 96f06e961792929a856e735f99a78ab5c1ea32a1 Mon Sep 17 00:00:00 2001 From: "Michael H." Date: Mon, 20 Jul 2026 11:54:06 +0200 Subject: [PATCH 2/2] feat: wildcard support --- apps/labrinth/src/routes/internal/flows.rs | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index e7152be691..4ccd811089 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1816,17 +1816,29 @@ impl From for AccountRegisterFlow { } } +/// Blacklist entries are matched literally, unless they begin with `*.`, in +/// which case they match any subdomain of the remaining suffix. +fn is_blacklisted_domain(domain: &str) -> bool { + let domain = domain.to_ascii_lowercase(); + + 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, + } + }) +} + fn ensure_email_domain_is_allowed(email: &str) -> Result<(), ApiError> { let Some((_, domain)) = email.rsplit_once('@') else { return Err(ApiError::Request(email_check_error_generic())); }; - let is_blacklisted = ENV - .EMAIL_DOMAIN_BLACKLIST - .iter() - .any(|blacklisted| blacklisted.trim().eq_ignore_ascii_case(domain)); - - if is_blacklisted { + if is_blacklisted_domain(domain) { info!(email.domain = domain, "blacklisted email domain, denying"); return Err(ApiError::Request(email_check_error_generic())); }