From 3340bf368eff239749d039bd82f26c9952caf711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Sun, 19 Jul 2026 16:35:59 -0400 Subject: [PATCH 1/7] feat: neverbounce API client --- Cargo.lock | 9 ++ Cargo.toml | 2 + packages/neverbounce/Cargo.toml | 20 +++ packages/neverbounce/src/lib.rs | 221 ++++++++++++++++++++++++++++++++ 4 files changed, 252 insertions(+) create mode 100644 packages/neverbounce/Cargo.toml create mode 100644 packages/neverbounce/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1c3493e8e0..ffedd6b820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6247,6 +6247,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "neverbounce" +version = "0.1.0" +dependencies = [ + "reqwest 0.12.24", + "serde", + "serde_json", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index a5aed6cf76..00e193ed6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "packages/modrinth-log", "packages/modrinth-maxmind", "packages/modrinth-util", + "packages/neverbounce", "packages/path-util", ] @@ -132,6 +133,7 @@ modrinth-util = { path = "packages/modrinth-util" } muralpay = { path = "packages/muralpay" } murmur2 = "0.1.0" native-dialog = "0.9.2" +neverbounce = { path = "packages/neverbounce" } notify = { version = "8.2.0", default-features = false } notify-debouncer-mini = { version = "0.7.0", default-features = false } objc2-app-kit = { version = "0.3.2", default-features = false } diff --git a/packages/neverbounce/Cargo.toml b/packages/neverbounce/Cargo.toml new file mode 100644 index 0000000000..cf87700be7 --- /dev/null +++ b/packages/neverbounce/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "neverbounce" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +description = "NeverBounce API client" +license = "MIT" +keywords = [] +categories = ["api-bindings"] + +[dependencies] +reqwest = { workspace = true, features = ["json"] } +serde = { workspace = true, features = ["derive"] } + +[dev-dependencies] +serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/packages/neverbounce/src/lib.rs b/packages/neverbounce/src/lib.rs new file mode 100644 index 0000000000..23e16bbb74 --- /dev/null +++ b/packages/neverbounce/src/lib.rs @@ -0,0 +1,221 @@ +use reqwest::Client; +use serde::{Deserialize, Deserializer, Serialize}; +use std::time::Duration; + +pub const API_URL: &str = "https://api.neverbounce.com"; +pub const SINGLE_CHECK_PATH: &str = "/v4/single/check"; +pub const SINGLE_CHECK_URL: &str = + "https://api.neverbounce.com/v4/single/check"; +pub const TIMEOUT: Duration = Duration::from_secs(10); + +/// Authentication and email parameters for a single verification. +/// +/// The API key is sent in the JSON request body. +#[derive(Clone, Copy)] +pub struct SingleCheckParams<'a> { + pub api_url: &'a str, + pub api_key: &'a str, + pub email: &'a str, +} + +impl<'a> SingleCheckParams<'a> { + #[must_use] + pub const fn new(api_key: &'a str, email: &'a str) -> Self { + Self { + api_url: API_URL, + api_key, + email, + } + } + + #[must_use] + pub const fn with_api_url(mut self, api_url: &'a str) -> Self { + self.api_url = api_url; + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[non_exhaustive] +pub struct SingleCheckResponse { + pub status: ResponseStatus, + #[serde(default)] + pub result: Option, + #[serde(default)] + pub flags: Vec, + #[serde(default)] + pub suggested_correction: Option, + #[serde(default)] + pub retry_token: Option, + #[serde(default)] + pub message: Option, + #[serde(default)] + pub execution_time: Option, +} + +impl SingleCheckResponse { + #[must_use] + pub fn is_safe_to_send(&self) -> bool { + matches!(self.status, ResponseStatus::Success) + && matches!(self.result.as_ref(), Some(VerificationResult::Valid)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum ResponseStatus { + Success, + GeneralFailure, + AuthFailure, + TemporarilyUnavailable, + ThrottleTriggered, + BadReferrer, + Unrecognized(String), +} + +impl<'de> Deserialize<'de> for ResponseStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(match String::deserialize(deserializer)?.as_str() { + "success" => Self::Success, + "general_failure" => Self::GeneralFailure, + "auth_failure" => Self::AuthFailure, + "temp_unavail" => Self::TemporarilyUnavailable, + "throttle_triggered" => Self::ThrottleTriggered, + "bad_referrer" => Self::BadReferrer, + value => Self::Unrecognized(value.to_owned()), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum VerificationResult { + Valid, + Invalid, + Disposable, + CatchAll, + Unknown, + Unrecognized(String), +} + +impl<'de> Deserialize<'de> for VerificationResult { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(match String::deserialize(deserializer)?.as_str() { + "valid" => Self::Valid, + "invalid" => Self::Invalid, + "disposable" => Self::Disposable, + "catchall" => Self::CatchAll, + "unknown" => Self::Unknown, + value => Self::Unrecognized(value.to_owned()), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum VerificationFlag { + HasDns, + HasDnsMx, + BadSyntax, + FreeEmailHost, + Profanity, + RoleAccount, + DisposableEmail, + GovernmentHost, + AcademicHost, + MilitaryHost, + InternationalHost, + SquatterHost, + SpellingMistake, + BadDns, + TemporaryDnsError, + ConnectFails, + AcceptsAll, + ContainsAlias, + ContainsSubdomain, + SmtpConnectable, + SpamtrapNetwork, + HistoricalResponse, + Unrecognized(String), +} + +impl<'de> Deserialize<'de> for VerificationFlag { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(match String::deserialize(deserializer)?.as_str() { + "has_dns" => Self::HasDns, + "has_dns_mx" => Self::HasDnsMx, + "bad_syntax" => Self::BadSyntax, + "free_email_host" => Self::FreeEmailHost, + "profanity" => Self::Profanity, + "role_account" => Self::RoleAccount, + "disposable_email" => Self::DisposableEmail, + "government_host" => Self::GovernmentHost, + "acedemic_host" | "academic_host" => Self::AcademicHost, + "military_host" => Self::MilitaryHost, + "international_host" => Self::InternationalHost, + "squatter_host" => Self::SquatterHost, + "spelling_mistake" => Self::SpellingMistake, + "bad_dns" => Self::BadDns, + "temporary_dns_error" => Self::TemporaryDnsError, + "connect_fails" => Self::ConnectFails, + "accepts_all" => Self::AcceptsAll, + "contains_alias" => Self::ContainsAlias, + "contains_subdomain" => Self::ContainsSubdomain, + "smtp_connectable" => Self::SmtpConnectable, + "spamtrap_network" => Self::SpamtrapNetwork, + "historical_response" => Self::HistoricalResponse, + value => Self::Unrecognized(value.to_owned()), + }) + } +} + +#[derive(Serialize)] +struct SingleCheckRequest<'a> { + key: &'a str, + email: &'a str, + timeout: u64, +} + +/// Verifies one email address using NeverBounce's single-check endpoint. +/// +/// This endpoint should only be called in response to an action such as a form +/// submission. Existing lists and databases must use NeverBounce's bulk API. +/// Both the server verification timeout and the complete HTTP request timeout +/// are ten seconds. +pub async fn single_check( + client: &Client, + params: &SingleCheckParams<'_>, +) -> reqwest::Result { + single_check_request(client, params) + .send() + .await? + .error_for_status()? + .json() + .await +} + +fn single_check_request( + client: &Client, + params: &SingleCheckParams<'_>, +) -> reqwest::RequestBuilder { + client + .post(format!( + "{}{SINGLE_CHECK_PATH}", + params.api_url.trim_end_matches('/') + )) + .timeout(TIMEOUT) + .json(&SingleCheckRequest { + key: params.api_key, + email: params.email, + timeout: TIMEOUT.as_secs(), + }) +} From fca4b2135ac771dc324066d31a741c50d2b3a0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Sun, 19 Jul 2026 17:02:59 -0400 Subject: [PATCH 2/7] feat(labrinth): integrate neverbounce --- Cargo.lock | 1 + apps/labrinth/Cargo.toml | 1 + apps/labrinth/src/env.rs | 3 + apps/labrinth/src/routes/internal/flows.rs | 71 ++++++++++------ apps/labrinth/src/util/mod.rs | 1 + apps/labrinth/src/util/neverbounce.rs | 94 ++++++++++++++++++++++ packages/neverbounce/src/lib.rs | 13 +++ 7 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 apps/labrinth/src/util/neverbounce.rs diff --git a/Cargo.lock b/Cargo.lock index ffedd6b820..de32941ac5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5467,6 +5467,7 @@ dependencies = [ "modrinth-util", "muralpay", "murmur2", + "neverbounce", "paste", "path-util", "postcard", diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 3da97e6fa8..a840303665 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -79,6 +79,7 @@ modrinth-content-management = { workspace = true } modrinth-util = { workspace = true, features = ["decimal", "sentry", "utoipa"] } muralpay = { workspace = true, features = ["client", "mock", "utoipa"] } murmur2 = { workspace = true } +neverbounce = { workspace = true } paste = { workspace = true } path-util = { workspace = true } postcard = { workspace = true } diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 2fc836e7ce..11d26d4572 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -235,6 +235,9 @@ vars! { SENDY_LIST_ID: String = "none"; SENDY_API_KEY: String = "none"; + NEVERBOUNCE_API_KEY: String = ""; + NEVERBOUNCE_BASE_URL: String = neverbounce::API_URL; + 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 c81c66a32c..0011a38fe1 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -24,6 +24,7 @@ use crate::util::captcha::check_hcaptcha; use crate::util::error::Context; use crate::util::ext::get_image_ext; use crate::util::img::upload_image_optimized; +use crate::util::neverbounce::{check_email, email_check_error_generic}; use crate::util::validate::validation_errors_to_string; use actix_http::header::LOCATION; use actix_web::http::StatusCode; @@ -1440,7 +1441,7 @@ struct NewOAuthAccount { pub sign_up_newsletter: bool, } -/// Create account with OAuth. +/// Create account with OAuth. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -1481,6 +1482,10 @@ pub async fn create_oauth_account( return Err(ApiError::Internal(eyre!("invalid flow kind"))); }; + if let Some(email) = &user.email { + ensure_email_is_usable(email).await?; + } + let mut txn = db .begin() .await @@ -1527,7 +1532,7 @@ struct DiscordCommunityHandoffPayload { nonce: String, } -/// Link Discord community. +/// Link Discord community. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -1604,7 +1609,7 @@ pub async fn discord_community_link( Ok(web::Json(DiscordCommunityLinkResponse { url })) } -/// Remove an auth provider. +/// Remove an auth provider. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -1794,6 +1799,20 @@ impl From for AccountRegisterFlow { } } +async fn ensure_email_is_usable(email: &str) -> Result<(), ApiError> { + let result = check_email(email).await.map_err(ApiError::Request)?; + + if matches!( + result, + neverbounce::VerificationResult::Invalid + | neverbounce::VerificationResult::Disposable + ) { + return Err(ApiError::Request(email_check_error_generic())); + } + + Ok(()) +} + impl AccountRegisterFlow { async fn validate( self, @@ -1947,7 +1966,7 @@ impl ReadyAccountRegisterFlow { } } -/// Validate password account creation. +/// Validate password account creation. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -1975,7 +1994,7 @@ pub async fn validate_create_account_with_password( Ok(()) } -/// Create account with a password. +/// Create account with a password. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2002,6 +2021,8 @@ pub async fn create_account_with_password( return Err(ApiError::Turnstile); } + ensure_email_is_usable(&new_account.email).await?; + let mut transaction = pool.begin().await?; let ready_flow = AccountRegisterFlow::from(new_account) @@ -2024,7 +2045,7 @@ pub struct Login { pub challenge: String, } -/// Log in with a password. +/// Log in with a password. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2185,7 +2206,7 @@ async fn validate_2fa_code( } } -/// Complete login with 2FA. +/// Complete login with 2FA. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2245,7 +2266,7 @@ pub async fn login_2fa( } } -/// Start 2FA setup. +/// Start 2FA setup. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2296,7 +2317,7 @@ pub async fn begin_2fa_flow( } } -/// Finish 2FA setup. +/// Finish 2FA setup. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2431,7 +2452,7 @@ pub struct Remove2FA { pub code: String, } -/// Remove 2FA. +/// Remove 2FA. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2531,7 +2552,7 @@ pub struct ResetPassword { pub challenge: String, } -/// Start password reset. +/// Start password reset. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2637,7 +2658,7 @@ pub struct ChangePassword { pub new_password: Option, } -/// Change password. +/// Change password. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2803,7 +2824,7 @@ pub struct SetEmail { pub email: String, } -/// Set email address. +/// Set email address. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -2856,6 +2877,8 @@ pub async fn set_email( )); } + ensure_email_is_usable(&email_address.email).await?; + let mut transaction = pool.begin().await?; sqlx::query!( @@ -2925,7 +2948,7 @@ pub async fn set_email( Ok(HttpResponse::Ok().finish()) } -/// Resend verification email. +/// Resend verification email. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3000,7 +3023,7 @@ pub struct VerifyEmail { pub flow: String, } -/// Verify email address. +/// Verify email address. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3066,7 +3089,7 @@ pub async fn verify_email( } } -/// Subscribe to the newsletter. +/// Subscribe to the newsletter. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3115,7 +3138,7 @@ pub async fn subscribe_newsletter( Ok(HttpResponse::NoContent().finish()) } -/// Get newsletter subscription status. +/// Get newsletter subscription status. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3165,7 +3188,7 @@ pub struct RegisterPasskeyResponse { pub flow: String, } -/// Start passkey registration. +/// Start passkey registration. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3265,7 +3288,7 @@ pub struct PasskeyResponse { pub last_used: Option>, } -/// Finish passkey registration. +/// Finish passkey registration. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3374,7 +3397,7 @@ pub struct AuthenticatePasskeyResponse { pub flow: String, } -/// Start passkey authentication. +/// Start passkey authentication. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3417,7 +3440,7 @@ pub struct AuthenticatePasskeyFinish { pub credential: PublicKeyCredential, } -/// Finish passkey authentication. +/// Finish passkey authentication. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3534,7 +3557,7 @@ pub async fn authenticate_passkey_finish( } } -/// List passkeys. +/// List passkeys. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3584,7 +3607,7 @@ pub struct RenamePasskey { pub name: String, } -/// Rename a passkey. +/// Rename a passkey. #[utoipa::path( context_path = "/auth", tag = "auth", @@ -3639,7 +3662,7 @@ pub async fn rename_passkey( Ok(HttpResponse::NoContent().finish()) } -/// Delete a passkey. +/// Delete a passkey. #[utoipa::path( context_path = "/auth", tag = "auth", diff --git a/apps/labrinth/src/util/mod.rs b/apps/labrinth/src/util/mod.rs index 87676d448f..8c04d596de 100644 --- a/apps/labrinth/src/util/mod.rs +++ b/apps/labrinth/src/util/mod.rs @@ -14,6 +14,7 @@ pub mod http; pub mod img; pub mod ip; pub mod kafka; +pub mod neverbounce; pub mod ratelimit; pub mod redis; pub mod routes; diff --git a/apps/labrinth/src/util/neverbounce.rs b/apps/labrinth/src/util/neverbounce.rs new file mode 100644 index 0000000000..f104deb649 --- /dev/null +++ b/apps/labrinth/src/util/neverbounce.rs @@ -0,0 +1,94 @@ +use std::time::Instant; + +use eyre::{WrapErr, eyre}; +use neverbounce::{ + ResponseStatus, SingleCheckParams, SingleCheckResponse, VerificationResult, +}; +use tracing::{debug, error}; + +use crate::env::ENV; +use crate::util::http::HTTP_CLIENT; + +pub async fn check_email(email: &str) -> eyre::Result { + if ENV.NEVERBOUNCE_API_KEY.is_empty() { + debug!( + result = "unknown", + "NeverBounce email check skipped because API key is not set", + ); + return Ok(VerificationResult::Unknown); + } + + let params = SingleCheckParams::new(&ENV.NEVERBOUNCE_API_KEY, email) + .with_api_url(&ENV.NEVERBOUNCE_BASE_URL); + + let check_time_start = Instant::now(); + + let response = match neverbounce::single_check(&HTTP_CLIENT, ¶ms).await + { + Ok(response) => response, + Err(source) => { + error!( + result = "unknown", + error = ?source, + "NeverBounce email check failed", + ); + return Err(eyre!(source)).wrap_err("failed to check email"); + } + }; + + let SingleCheckResponse { status, result, .. } = response; + + let check_time = check_time_start.elapsed(); + + match status { + ResponseStatus::Success => { + let result = result.ok_or_else(|| { + error!(result = "unknown", "NeverBounce email check failed",); + eyre!("") + })?; + + if matches!(result, VerificationResult::Unrecognized(_)) { + error!( + result = result.as_str(), + request.time_ms = check_time.as_millis(), + "NeverBounce email check failed", + ); + return Err(email_check_error_generic()); + } + + debug!( + result = result.as_str(), + request.time_ms = check_time.as_millis(), + "NeverBounce email check succeeded", + ); + Ok(result) + } + failure_type => { + let result = result.unwrap_or(VerificationResult::Unknown); + error!( + failure_type = response_failure_type(&failure_type), + result = result.as_str(), + request.time_ms = check_time.as_millis(), + "NeverBounce email check failed", + ); + Err(email_check_error_generic()) + } + } +} + +pub fn email_check_error_generic() -> eyre::Error { + eyre!("Please try a different email address!") +} + +fn response_failure_type(status: &ResponseStatus) -> &str { + match status { + ResponseStatus::Success => "success", + ResponseStatus::GeneralFailure => "general_failure", + ResponseStatus::AuthFailure => "auth_failure", + ResponseStatus::TemporarilyUnavailable => "temp_unavail", + ResponseStatus::ThrottleTriggered => "throttle_triggered", + ResponseStatus::BadReferrer => "bad_referrer", + ResponseStatus::Unrecognized(status) => status, + _ => "unrecognized", + } +} diff --git a/packages/neverbounce/src/lib.rs b/packages/neverbounce/src/lib.rs index 23e16bbb74..fdc26ee6b9 100644 --- a/packages/neverbounce/src/lib.rs +++ b/packages/neverbounce/src/lib.rs @@ -101,6 +101,19 @@ pub enum VerificationResult { Unrecognized(String), } +impl VerificationResult { + pub fn as_str(&self) -> &str { + match self { + VerificationResult::Valid => "valid", + VerificationResult::Invalid => "invalid", + VerificationResult::Disposable => "disposable", + VerificationResult::CatchAll => "catchall", + VerificationResult::Unknown => "unknown", + VerificationResult::Unrecognized(other) => &other, + } + } +} + impl<'de> Deserialize<'de> for VerificationResult { fn deserialize(deserializer: D) -> Result where From 47e31db01ff7e4b6f677fee751c374b9f40e4abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Sun, 19 Jul 2026 17:04:27 -0400 Subject: [PATCH 3/7] chore: tombi --- packages/neverbounce/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neverbounce/Cargo.toml b/packages/neverbounce/Cargo.toml index cf87700be7..965b7ccd12 100644 --- a/packages/neverbounce/Cargo.toml +++ b/packages/neverbounce/Cargo.toml @@ -3,8 +3,8 @@ name = "neverbounce" version = "0.1.0" edition.workspace = true rust-version.workspace = true -repository.workspace = true description = "NeverBounce API client" +repository.workspace = true license = "MIT" keywords = [] categories = ["api-bindings"] From b82811843aefd307534c9a7317c51779392498e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Sun, 19 Jul 2026 17:06:21 -0400 Subject: [PATCH 4/7] chore: typo --- packages/neverbounce/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neverbounce/src/lib.rs b/packages/neverbounce/src/lib.rs index fdc26ee6b9..30f5b0d431 100644 --- a/packages/neverbounce/src/lib.rs +++ b/packages/neverbounce/src/lib.rs @@ -172,7 +172,7 @@ impl<'de> Deserialize<'de> for VerificationFlag { "role_account" => Self::RoleAccount, "disposable_email" => Self::DisposableEmail, "government_host" => Self::GovernmentHost, - "acedemic_host" | "academic_host" => Self::AcademicHost, + "academic_host" => Self::AcademicHost, "military_host" => Self::MilitaryHost, "international_host" => Self::InternationalHost, "squatter_host" => Self::SquatterHost, From 796456e6bdbffb6f9cca048d0693d54afb2f4565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Sun, 19 Jul 2026 17:06:42 -0400 Subject: [PATCH 5/7] chore: clippy --- packages/neverbounce/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neverbounce/src/lib.rs b/packages/neverbounce/src/lib.rs index 30f5b0d431..429bef3ec9 100644 --- a/packages/neverbounce/src/lib.rs +++ b/packages/neverbounce/src/lib.rs @@ -109,7 +109,7 @@ impl VerificationResult { VerificationResult::Disposable => "disposable", VerificationResult::CatchAll => "catchall", VerificationResult::Unknown => "unknown", - VerificationResult::Unrecognized(other) => &other, + VerificationResult::Unrecognized(other) => other, } } } From 4612b8ddae7b2715d2a9c5dd9721d8a74035d547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Sun, 19 Jul 2026 17:32:11 -0400 Subject: [PATCH 6/7] chore: remove unused dep --- packages/neverbounce/Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/neverbounce/Cargo.toml b/packages/neverbounce/Cargo.toml index 965b7ccd12..6a1b4fbda6 100644 --- a/packages/neverbounce/Cargo.toml +++ b/packages/neverbounce/Cargo.toml @@ -13,8 +13,5 @@ categories = ["api-bindings"] reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } -[dev-dependencies] -serde_json = { workspace = true } - [lints] workspace = true From 9f4b433bd5a430d02acbe88585d97640d21d9b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Sun, 19 Jul 2026 18:58:12 -0400 Subject: [PATCH 7/7] chore: cleanup --- Cargo.lock | 1 - apps/labrinth/src/env.rs | 2 +- packages/neverbounce/src/lib.rs | 6 ++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de32941ac5..5ac8a874a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6254,7 +6254,6 @@ version = "0.1.0" dependencies = [ "reqwest 0.12.24", "serde", - "serde_json", ] [[package]] diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 11d26d4572..8cd0984819 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -236,7 +236,7 @@ vars! { SENDY_API_KEY: String = "none"; NEVERBOUNCE_API_KEY: String = ""; - NEVERBOUNCE_BASE_URL: String = neverbounce::API_URL; + NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL; CLICKHOUSE_REPLICATED: bool = false; CLICKHOUSE_URL: String = "http://localhost:8123"; diff --git a/packages/neverbounce/src/lib.rs b/packages/neverbounce/src/lib.rs index 429bef3ec9..74c6af5b1b 100644 --- a/packages/neverbounce/src/lib.rs +++ b/packages/neverbounce/src/lib.rs @@ -2,10 +2,8 @@ use reqwest::Client; use serde::{Deserialize, Deserializer, Serialize}; use std::time::Duration; -pub const API_URL: &str = "https://api.neverbounce.com"; +pub const DEFAULT_API_URL: &str = "https://api.neverbounce.com"; pub const SINGLE_CHECK_PATH: &str = "/v4/single/check"; -pub const SINGLE_CHECK_URL: &str = - "https://api.neverbounce.com/v4/single/check"; pub const TIMEOUT: Duration = Duration::from_secs(10); /// Authentication and email parameters for a single verification. @@ -22,7 +20,7 @@ impl<'a> SingleCheckParams<'a> { #[must_use] pub const fn new(api_key: &'a str, email: &'a str) -> Self { Self { - api_url: API_URL, + api_url: DEFAULT_API_URL, api_key, email, }