mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4b433bd5 | ||
|
|
4612b8ddae | ||
|
|
796456e6bd | ||
|
|
b82811843a | ||
|
|
47e31db01f | ||
|
|
fca4b2135a | ||
|
|
3340bf368e |
Generated
+9
@@ -5467,6 +5467,7 @@ dependencies = [
|
||||
"modrinth-util",
|
||||
"muralpay",
|
||||
"murmur2",
|
||||
"neverbounce",
|
||||
"paste",
|
||||
"path-util",
|
||||
"postcard",
|
||||
@@ -6247,6 +6248,14 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neverbounce"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.24",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -235,6 +235,9 @@ vars! {
|
||||
SENDY_LIST_ID: String = "none";
|
||||
SENDY_API_KEY: String = "none";
|
||||
|
||||
NEVERBOUNCE_API_KEY: String = "";
|
||||
NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL;
|
||||
|
||||
CLICKHOUSE_REPLICATED: bool = false;
|
||||
CLICKHOUSE_URL: String = "http://localhost:8123";
|
||||
CLICKHOUSE_USER: String = "default";
|
||||
|
||||
@@ -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<NewAccount> 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<String>,
|
||||
}
|
||||
|
||||
/// 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<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// 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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<VerificationResult> {
|
||||
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",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "neverbounce"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "NeverBounce API client"
|
||||
repository.workspace = true
|
||||
license = "MIT"
|
||||
keywords = []
|
||||
categories = ["api-bindings"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,232 @@
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
pub const DEFAULT_API_URL: &str = "https://api.neverbounce.com";
|
||||
pub const SINGLE_CHECK_PATH: &str = "/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: DEFAULT_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<VerificationResult>,
|
||||
#[serde(default)]
|
||||
pub flags: Vec<VerificationFlag>,
|
||||
#[serde(default)]
|
||||
pub suggested_correction: Option<String>,
|
||||
#[serde(default)]
|
||||
pub retry_token: Option<String>,
|
||||
#[serde(default)]
|
||||
pub message: Option<String>,
|
||||
#[serde(default)]
|
||||
pub execution_time: Option<u64>,
|
||||
}
|
||||
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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 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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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,
|
||||
"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<SingleCheckResponse> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user