mirror of
https://github.com/rustmailer/rustmailer.git
synced 2026-08-22 08:01:23 +00:00
222 lines
7.5 KiB
Rust
222 lines
7.5 KiB
Rust
// Copyright © 2025 rustmailer.com
|
|
// Licensed under RustMailer License Agreement v1.0
|
|
// Unauthorized copying, modification, or distribution is prohibited.
|
|
|
|
use dashmap::DashMap;
|
|
use http::header::{AUTHORIZATION, CONTENT_TYPE};
|
|
use http::StatusCode;
|
|
use serde::Serialize;
|
|
use tracing::error;
|
|
|
|
use crate::modules::error::code::ErrorCode;
|
|
use crate::modules::hook::entity::HttpMethod;
|
|
use crate::modules::settings::proxy::Proxy;
|
|
use crate::raise_error;
|
|
use crate::{modules::error::RustMailerResult, rustmailer_version};
|
|
use std::collections::HashMap;
|
|
use std::sync::LazyLock;
|
|
use std::time::Duration;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
// This will cache clients per proxy configuration.
|
|
static HTTP_CLIENTS_CACHE: LazyLock<DashMap<u64, reqwest::Client>> = LazyLock::new(DashMap::new);
|
|
|
|
pub struct HttpClient {
|
|
client: reqwest::Client,
|
|
}
|
|
|
|
impl HttpClient {
|
|
pub fn create(client: reqwest::Client) -> HttpClient {
|
|
Self { client }
|
|
}
|
|
|
|
fn base_builder() -> reqwest::ClientBuilder {
|
|
reqwest::ClientBuilder::new()
|
|
.user_agent(rustmailer_version!())
|
|
.timeout(Duration::from_secs(30))
|
|
.connect_timeout(Duration::from_secs(10))
|
|
}
|
|
|
|
pub async fn new(use_proxy: Option<u64>) -> RustMailerResult<HttpClient> {
|
|
// Use proxy_id or 0 as the key for the cache
|
|
let proxy_id = use_proxy.unwrap_or(0);
|
|
// First, check if the HttpClient is already cached
|
|
if let Some(client) = HTTP_CLIENTS_CACHE.get(&proxy_id) {
|
|
return Ok(HttpClient::create(client.clone())); // Client is already cloneable, so clone the Arc here
|
|
}
|
|
// If not found in the cache, build a new HttpClient
|
|
let mut builder = Self::base_builder();
|
|
if proxy_id != 0 {
|
|
// Only set the proxy if we have a valid proxy_id
|
|
let proxy = Proxy::get(proxy_id).await?;
|
|
let proxy_obj = reqwest::Proxy::all(&proxy.url).map_err(|e| {
|
|
raise_error!(
|
|
format!(
|
|
"Failed to configure SOCKS5 proxy ({}): {:#?}. Please check",
|
|
&proxy.url, e
|
|
),
|
|
ErrorCode::InternalError
|
|
)
|
|
})?;
|
|
builder = builder
|
|
.redirect(reqwest::redirect::Policy::none())
|
|
.proxy(proxy_obj);
|
|
}
|
|
// Build the HttpClient
|
|
let client = builder.build().map_err(|e| {
|
|
raise_error!(
|
|
format!("Failed to build HTTP client: {:#?}", e),
|
|
ErrorCode::InternalError
|
|
)
|
|
})?;
|
|
// Cache the newly created HttpClient
|
|
HTTP_CLIENTS_CACHE.insert(proxy_id, client.clone());
|
|
Ok(HttpClient::create(client))
|
|
}
|
|
|
|
pub async fn send_json_request(
|
|
&self,
|
|
task_info: Option<HashMap<String, String>>,
|
|
method: HttpMethod,
|
|
url: &str,
|
|
payload: &serde_json::Value,
|
|
headers: Option<HashMap<String, String>>,
|
|
) -> RustMailerResult<reqwest::Response> {
|
|
let mut request_builder = match method {
|
|
HttpMethod::Post => self.client.post(url),
|
|
HttpMethod::Put => self.client.put(url),
|
|
};
|
|
|
|
if let Some(headers) = task_info {
|
|
for (key, value) in headers {
|
|
request_builder = request_builder.header(&key, &value);
|
|
}
|
|
}
|
|
|
|
// Set headers if provided
|
|
if let Some(headers) = headers {
|
|
for (key, value) in headers {
|
|
request_builder = request_builder.header(&key, &value);
|
|
}
|
|
}
|
|
|
|
// Send the request with JSON payload
|
|
let response = request_builder
|
|
.json(payload) // Serialize the payload to JSON
|
|
.send()
|
|
.await
|
|
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
|
Ok(response)
|
|
}
|
|
|
|
/// Wrapper around the Gmail API `GET` request to fetch data.
|
|
pub async fn get(&self, url: &str, access_token: &str) -> RustMailerResult<serde_json::Value> {
|
|
let res = self
|
|
.client
|
|
.get(url)
|
|
.header(AUTHORIZATION, format!("Bearer {}", access_token))
|
|
.header(CONTENT_TYPE, "application/json")
|
|
.send()
|
|
.await
|
|
.map_err(|e| {
|
|
raise_error!(
|
|
format!("Request failed: {:#?}", e),
|
|
ErrorCode::InternalError
|
|
)
|
|
})?;
|
|
|
|
if res.status().is_success() {
|
|
let json: serde_json::Value = res.json().await.map_err(|e| {
|
|
raise_error!(
|
|
format!("Failed to parse response: {:#?}", e),
|
|
ErrorCode::InternalError
|
|
)
|
|
})?;
|
|
Ok(json)
|
|
} else {
|
|
let status = res.status();
|
|
let text = res.text().await.map_err(|e| {
|
|
raise_error!(
|
|
format!("Failed to read error response: {:#?}", e),
|
|
ErrorCode::InternalError
|
|
)
|
|
})?;
|
|
if matches!(status, StatusCode::NOT_FOUND) || matches!(status, StatusCode::BAD_REQUEST)
|
|
{
|
|
error!(
|
|
status = ?status,
|
|
url = %url,
|
|
response = %text,
|
|
"Gmail API client error"
|
|
);
|
|
return Err(raise_error!(
|
|
format!(
|
|
"Gmail API returned client error (status {}) for {}. Response: {}",
|
|
status, url, text
|
|
),
|
|
ErrorCode::GmailApiInvalidHistoryId
|
|
));
|
|
}
|
|
// Return the error with status and response text for more context
|
|
Err(raise_error!(
|
|
format!(
|
|
"Gmail API call to {} failed with status {}: {}",
|
|
url, status, text
|
|
),
|
|
ErrorCode::GmailApiCallFailed
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Wrapper around the Gmail API `POST` request.
|
|
pub async fn post<T: Serialize + ?Sized>(
|
|
&self,
|
|
url: &str,
|
|
access_token: &str,
|
|
body: &T,
|
|
) -> RustMailerResult<serde_json::Value> {
|
|
let res = self
|
|
.client
|
|
.post(url)
|
|
.header(AUTHORIZATION, format!("Bearer {}", access_token))
|
|
.header(CONTENT_TYPE, "application/json")
|
|
.json(body)
|
|
.send()
|
|
.await
|
|
.map_err(|e| {
|
|
raise_error!(
|
|
format!("Request failed: {:#?}", e),
|
|
ErrorCode::InternalError
|
|
)
|
|
})?;
|
|
|
|
if res.status().is_success() {
|
|
let json: serde_json::Value = res.json().await.map_err(|e| {
|
|
raise_error!(
|
|
format!("Failed to parse response: {:#?}", e),
|
|
ErrorCode::InternalError
|
|
)
|
|
})?;
|
|
Ok(json)
|
|
} else {
|
|
let status = res.status();
|
|
let text = res.text().await.map_err(|e| {
|
|
raise_error!(
|
|
format!("Failed to read error response: {:#?}", e),
|
|
ErrorCode::InternalError
|
|
)
|
|
})?;
|
|
// Return the error with status and response text for more context
|
|
Err(raise_error!(
|
|
format!(
|
|
"Gmail API call to {} failed with status {}: {}",
|
|
url, status, text
|
|
),
|
|
ErrorCode::GmailApiCallFailed
|
|
))
|
|
}
|
|
}
|
|
}
|