Files
rustmailer/src/modules/hook/http/mod.rs
T

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
))
}
}
}