mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
initial commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use ring::aead::{Aad, BoundKey, Nonce, NonceSequence, OpeningKey, SealingKey, AES_256_GCM};
|
||||
use ring::pbkdf2::{self, derive};
|
||||
use ring::rand::{SecureRandom, SystemRandom};
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::error::BichonResult;
|
||||
use crate::modules::settings::cli::SETTINGS;
|
||||
use crate::raise_error;
|
||||
|
||||
struct SingleNonceSequence([u8; 12]);
|
||||
|
||||
impl SingleNonceSequence {
|
||||
fn new(nonce: [u8; 12]) -> Self {
|
||||
SingleNonceSequence(nonce)
|
||||
}
|
||||
}
|
||||
|
||||
impl NonceSequence for SingleNonceSequence {
|
||||
fn advance(&mut self) -> Result<Nonce, ring::error::Unspecified> {
|
||||
Ok(Nonce::assume_unique_for_key(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_string(plaintext: &str) -> BichonResult<String> {
|
||||
internal_encrypt_string(&SETTINGS.bichon_encrypt_password, plaintext)
|
||||
.map_err(|_| raise_error!("Failed to encrypt string.".into(), ErrorCode::InternalError))
|
||||
}
|
||||
|
||||
pub fn decrypt_string(data: &str) -> BichonResult<String> {
|
||||
internal_decrypt_string(&SETTINGS.bichon_encrypt_password, data).map_err(|_| {
|
||||
raise_error!(
|
||||
"Decryption failed, likely due to incorrect encryption key or corrupted data".into(),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn internal_encrypt_string(
|
||||
password: &str,
|
||||
plaintext: &str,
|
||||
) -> Result<String, ring::error::Unspecified> {
|
||||
let rng = SystemRandom::new();
|
||||
let mut salt = [0u8; 32];
|
||||
rng.fill(&mut salt)?;
|
||||
let mut key = [0u8; 32];
|
||||
derive(
|
||||
pbkdf2::PBKDF2_HMAC_SHA256,
|
||||
NonZeroU32::new(100_000).unwrap(),
|
||||
&salt,
|
||||
password.as_bytes(),
|
||||
&mut key,
|
||||
);
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
rng.fill(&mut nonce_bytes)?;
|
||||
let unbound_key = ring::aead::UnboundKey::new(&AES_256_GCM, &key)?;
|
||||
let nonce_sequence = SingleNonceSequence::new(nonce_bytes);
|
||||
let mut sealing_key = SealingKey::new(unbound_key, nonce_sequence);
|
||||
let mut in_out = plaintext.as_bytes().to_vec();
|
||||
let aad = Aad::empty();
|
||||
sealing_key.seal_in_place_append_tag(aad, &mut in_out)?;
|
||||
let mut result = Vec::with_capacity(32 + 12 + in_out.len());
|
||||
result.extend_from_slice(&salt);
|
||||
result.extend_from_slice(&nonce_bytes);
|
||||
result.extend_from_slice(&in_out);
|
||||
Ok(general_purpose::URL_SAFE.encode(&result))
|
||||
}
|
||||
|
||||
fn internal_decrypt_string(password: &str, data: &str) -> Result<String, ring::error::Unspecified> {
|
||||
let data = general_purpose::URL_SAFE
|
||||
.decode(data)
|
||||
.map_err(|_| ring::error::Unspecified)?;
|
||||
if data.len() < 32 + 12 {
|
||||
return Err(ring::error::Unspecified);
|
||||
}
|
||||
let salt = &data[0..32];
|
||||
let nonce_bytes: [u8; 12] = data[32..44]
|
||||
.try_into()
|
||||
.map_err(|_| ring::error::Unspecified)?;
|
||||
let ciphertext = &data[44..];
|
||||
let mut key = [0u8; 32];
|
||||
derive(
|
||||
pbkdf2::PBKDF2_HMAC_SHA256,
|
||||
NonZeroU32::new(100_000).unwrap(),
|
||||
salt,
|
||||
password.as_bytes(),
|
||||
&mut key,
|
||||
);
|
||||
let unbound_key = ring::aead::UnboundKey::new(&AES_256_GCM, &key)?;
|
||||
let nonce_sequence = SingleNonceSequence::new(nonce_bytes);
|
||||
let mut opening_key = OpeningKey::new(unbound_key, nonce_sequence);
|
||||
let mut in_out = ciphertext.to_vec();
|
||||
let aad = Aad::empty();
|
||||
let decrypted_bytes = opening_key.open_in_place(aad, &mut in_out)?;
|
||||
String::from_utf8(decrypted_bytes.to_vec()).map_err(|_| ring::error::Unspecified)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() {
|
||||
let password = "my_secure_passwasdasdasdasdasord";
|
||||
let plaintext = "Helloasdasdasdasdasd, World!";
|
||||
let encrypted = internal_encrypt_string(password, plaintext).unwrap();
|
||||
println!("{}", &encrypted);
|
||||
let decrypted = internal_decrypt_string(password, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
//
|
||||
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use rand::{rng, Rng};
|
||||
|
||||
use super::error::code::ErrorCode;
|
||||
|
||||
pub mod encrypt;
|
||||
pub mod net;
|
||||
pub mod rate_limit;
|
||||
pub mod shutdown;
|
||||
pub mod tls;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bichon_version {
|
||||
() => {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! utc_now {
|
||||
() => {{
|
||||
use chrono::Utc;
|
||||
Utc::now().timestamp_millis()
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! after_n_days_timestamp {
|
||||
($start_ts:expr, $days:expr) => {{
|
||||
const MILLIS_PER_DAY: i64 = 86_400_000; // 24 * 60 * 60 * 1000
|
||||
$start_ts + ($days as i64) * MILLIS_PER_DAY
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! base64_encode {
|
||||
($bytes:expr) => {{
|
||||
use base64::{engine::general_purpose::STANDARD, *};
|
||||
STANDARD.encode($bytes)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! base64_decode {
|
||||
($key:expr) => {{
|
||||
use base64::{engine::general_purpose::STANDARD, *};
|
||||
STANDARD.decode($key).unwrap()
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! base64_decode_url_safe {
|
||||
($key:expr) => {{
|
||||
use base64::{engine::general_purpose::URL_SAFE, *};
|
||||
URL_SAFE.decode($key)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! base64_encode_url_safe {
|
||||
($key:expr) => {{
|
||||
use base64::{engine::general_purpose::URL_SAFE, *};
|
||||
URL_SAFE.encode($key)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! product_public_key {
|
||||
() => {
|
||||
$crate::base64_decode!(r#"BNlT+WjdEls9VGfry+zKygx+UoypxSqsMBddMGxYgbhWOz7Xfh7YJXGMeby9jBtbz3rhSGrTuZCYA9uwwMMYkhI="#)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! license_header {
|
||||
() => {
|
||||
"{\"alg\":\"ES256\",\"typ\":\"JWT\"}"
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! raise_error {
|
||||
($msg:expr, $code:expr) => {
|
||||
$crate::modules::error::BichonError::Generic {
|
||||
message: $msg,
|
||||
location: snafu::Location::default(),
|
||||
code: $code,
|
||||
}
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! run_with_timeout {
|
||||
($duration:expr, $task:expr, $err_msg:expr) => {{
|
||||
match tokio::time::timeout($duration, $task).await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err($err_msg),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! free_memory {
|
||||
() => {{
|
||||
let mut sys = sysinfo::System::new_all();
|
||||
sys.refresh_memory();
|
||||
sys.free_memory()
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! generate_token {
|
||||
($bit_strength:expr) => {{
|
||||
$crate::modules::utils::generate_token_impl($bit_strength)
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) fn generate_token_impl(bit_strength: usize) -> String {
|
||||
let byte_length = (bit_strength + 23) / 24 * 3;
|
||||
let random_bytes: Vec<u8> = (0..byte_length).map(|_| rand::random::<u8>()).collect();
|
||||
let mut encoded = general_purpose::URL_SAFE.encode(&random_bytes);
|
||||
|
||||
encoded = encoded
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c == '/' || c == '+' || c == '-' || c == '_' {
|
||||
make_single_random_char()
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
encoded
|
||||
}
|
||||
|
||||
fn make_single_random_char() -> char {
|
||||
let random_bytes: [u8; 3] = rng().random();
|
||||
let encoded = general_purpose::URL_SAFE.encode(random_bytes);
|
||||
encoded
|
||||
.chars()
|
||||
.find(|&c| c != '-' && c != '_' && c != '+' && c != '/')
|
||||
.unwrap_or('a')
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure_access {
|
||||
($dir:expr) => {{
|
||||
$crate::modules::utils::ensure_dir_and_test_access($dir)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! decode_mailbox_name {
|
||||
($name:expr) => {{
|
||||
utf7_imap::decode_utf7_imap($name.to_string())
|
||||
}};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! encode_mailbox_name {
|
||||
($name:expr) => {{
|
||||
utf7_imap::encode_utf7_imap($name.to_string())
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_encoding {
|
||||
($label:expr) => {
|
||||
match encoding_rs::Encoding::for_label($label.as_bytes()) {
|
||||
None => None,
|
||||
Some(encoding) => Some(encoding),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! current_datetime {
|
||||
() => {{
|
||||
use chrono::Local;
|
||||
let now = Local::now();
|
||||
now.format("%Y%m%d%H%M").to_string()
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! validate_email {
|
||||
($email:expr) => {{
|
||||
$crate::modules::utils::validate_email($email)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! encrypt {
|
||||
($plaintext:expr) => {{
|
||||
$crate::modules::utils::encrypt::encrypt_string($plaintext)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! decrypt {
|
||||
($plaintext:expr) => {{
|
||||
$crate::modules::utils::encrypt::decrypt_string($plaintext)
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn validate_email(email: &str) -> crate::modules::error::BichonResult<()> {
|
||||
use std::str::FromStr;
|
||||
let email_address = email_address::EmailAddress::from_str(email).map_err(|_| {
|
||||
raise_error!(
|
||||
format!("Invalid email format : {}", email),
|
||||
ErrorCode::InvalidParameter
|
||||
)
|
||||
})?;
|
||||
if email != email_address.email() {
|
||||
return Err(raise_error!(
|
||||
format!("Invalid email format: {}", email),
|
||||
ErrorCode::InvalidParameter
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! calculate_hash {
|
||||
($name:expr) => {
|
||||
$crate::modules::utils::hash($name)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! id {
|
||||
($bit_strength:expr) => {{
|
||||
// Generate a token with the given bit strength
|
||||
let token = $crate::modules::utils::generate_token_impl($bit_strength);
|
||||
// Hash the generated token
|
||||
$crate::modules::utils::hash(&token)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! u64_to_str {
|
||||
($id:expr) => {{
|
||||
let mut buf = itoa::Buffer::new();
|
||||
buf.format($id)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Generates a 64-bit hash from a string, ensuring the output is within JavaScript's safe integer range (0 to 2^53 - 1).
|
||||
pub fn hash(s: &str) -> u64 {
|
||||
let mut cursor = Vec::new();
|
||||
cursor.extend_from_slice(s.as_bytes());
|
||||
let mut cursor = std::io::Cursor::new(cursor);
|
||||
let hash = murmur3::murmur3_x64_128(&mut cursor, 0).unwrap();
|
||||
(hash & 0x1F_FFFF_FFFF_FFFF) as u64
|
||||
}
|
||||
|
||||
pub fn create_hash(account_id: u64, field: &str) -> u64 {
|
||||
// Construct a buffer of bytes from account_id and mailbox_name
|
||||
let mut buffer = Vec::new();
|
||||
buffer.extend_from_slice(&account_id.to_le_bytes()); // Convert u64 to bytes
|
||||
buffer.push(b':'); // Separator
|
||||
buffer.extend_from_slice(field.as_bytes()); // Add mailbox name
|
||||
// Create a Cursor for the buffer
|
||||
let mut cursor = std::io::Cursor::new(buffer);
|
||||
// Compute the 128-bit Murmur3 hash and cast to u64
|
||||
let hash = murmur3::murmur3_x64_128(&mut cursor, 0).unwrap();
|
||||
(hash & 0x1F_FFFF_FFFF_FFFF) as u64
|
||||
}
|
||||
|
||||
pub fn get_total_size(path: &PathBuf) -> io::Result<u64> {
|
||||
if !path.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
return Ok(fs::metadata(path)?.len());
|
||||
}
|
||||
|
||||
let mut total_size = 0u64;
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let entry_path = entry.path();
|
||||
|
||||
if entry_path.is_file() {
|
||||
total_size += fs::metadata(&entry_path)?.len();
|
||||
} else if entry_path.is_dir() {
|
||||
total_size += get_total_size(&entry_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_size)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//
|
||||
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::settings::proxy::Proxy;
|
||||
use crate::modules::utils::tls::establish_tls_stream;
|
||||
use crate::modules::{error::BichonResult, imap::session::SessionStream};
|
||||
use crate::raise_error;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
use tokio_socks::tcp::Socks5Stream;
|
||||
use tracing::error;
|
||||
|
||||
pub(crate) const TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
pub(crate) async fn establish_tcp_connection_with_timeout(
|
||||
address: SocketAddr,
|
||||
use_proxy: Option<u64>,
|
||||
) -> BichonResult<Pin<Box<TimeoutStream<TcpStream>>>> {
|
||||
// Establish the TCP connection with a timeout
|
||||
let tcp_stream = connect_with_optional_proxy(use_proxy, address).await?;
|
||||
|
||||
// Disable Nagle's algorithm for more efficient network communication
|
||||
tcp_stream
|
||||
.set_nodelay(true)
|
||||
.map_err(|e| raise_error!(e.to_string(), ErrorCode::NetworkError))?;
|
||||
|
||||
// Wrap the TCP stream in a TimeoutStream for timeout management
|
||||
let mut timeout_stream = TimeoutStream::new(tcp_stream);
|
||||
|
||||
// Set read and write timeouts
|
||||
timeout_stream.set_write_timeout(Some(TIMEOUT));
|
||||
timeout_stream.set_read_timeout(Some(TIMEOUT));
|
||||
|
||||
// Return the timeout-wrapped TCP stream as a Pin
|
||||
Ok(Box::pin(timeout_stream))
|
||||
}
|
||||
|
||||
pub(crate) async fn establish_tls_connection(
|
||||
address: SocketAddr,
|
||||
server_hostname: &str,
|
||||
alpn_protocols: &[&str],
|
||||
use_proxy: Option<u64>,
|
||||
) -> BichonResult<impl SessionStream> {
|
||||
// Establish the TCP connection with timeout
|
||||
let tcp_stream = establish_tcp_connection_with_timeout(address, use_proxy).await?;
|
||||
|
||||
// Wrap the TCP stream with TLS encryption
|
||||
let tls_stream = establish_tls_stream(server_hostname, alpn_protocols, tcp_stream).await?;
|
||||
|
||||
// Return the TLS stream wrapped in a SessionStream
|
||||
Ok(tls_stream)
|
||||
}
|
||||
|
||||
pub fn parse_proxy_addr(input: &str) -> BichonResult<SocketAddr> {
|
||||
// Normalize and check protocol prefix
|
||||
let (scheme, stripped) = if let Some(rest) = input
|
||||
.strip_prefix("socks5://")
|
||||
.or_else(|| input.strip_prefix("SOCKS5://"))
|
||||
.or_else(|| input.strip_prefix("Socks5://"))
|
||||
{
|
||||
("socks5", rest)
|
||||
} else if let Some(rest) = input
|
||||
.strip_prefix("http://")
|
||||
.or_else(|| input.strip_prefix("HTTP://"))
|
||||
.or_else(|| input.strip_prefix("Http://"))
|
||||
{
|
||||
("http", rest)
|
||||
} else {
|
||||
return Err(raise_error!(
|
||||
format!(
|
||||
"Invalid proxy URL: must start with 'http://' or 'socks5://', got '{}'",
|
||||
input
|
||||
),
|
||||
ErrorCode::InvalidParameter
|
||||
));
|
||||
};
|
||||
|
||||
// Parse the remaining address
|
||||
let addr = stripped.parse::<SocketAddr>().map_err(|e| {
|
||||
raise_error!(
|
||||
format!(
|
||||
"Failed to parse {} proxy address '{}': {}",
|
||||
scheme, stripped, e
|
||||
),
|
||||
ErrorCode::InvalidParameter
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Try to connect via SOCKS5 proxy or TCP with timeout
|
||||
async fn connect_with_optional_proxy(
|
||||
use_proxy: Option<u64>,
|
||||
address: SocketAddr,
|
||||
) -> BichonResult<TcpStream> {
|
||||
// Try if proxy is enabled
|
||||
if let Some(proxy_id) = use_proxy {
|
||||
let proxy = Proxy::get(proxy_id).await?;
|
||||
let proxy = parse_proxy_addr(&proxy.url)?;
|
||||
return timeout(TIMEOUT, Socks5Stream::connect(proxy, address))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
error!(
|
||||
"SOCKS5 proxy connection to {} via {} timed out after {}s",
|
||||
address,
|
||||
proxy,
|
||||
TIMEOUT.as_secs()
|
||||
);
|
||||
raise_error!(
|
||||
format!(
|
||||
"SOCKS5 proxy connection to {} via {} timed out after {}s",
|
||||
address,
|
||||
proxy,
|
||||
TIMEOUT.as_secs()
|
||||
),
|
||||
ErrorCode::ConnectionTimeout
|
||||
)
|
||||
})?
|
||||
.map(|s| s.into_inner())
|
||||
.map_err(|e| raise_error!(e.to_string(), ErrorCode::NetworkError));
|
||||
}
|
||||
// Fallback to direct TCP connection
|
||||
timeout(TIMEOUT, TcpStream::connect(address))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
error!(
|
||||
"TCP connection to {} timed out after {}s",
|
||||
address,
|
||||
TIMEOUT.as_secs()
|
||||
);
|
||||
raise_error!(
|
||||
format!(
|
||||
"TCP connection to {} timed out after {}s",
|
||||
address,
|
||||
TIMEOUT.as_secs()
|
||||
),
|
||||
ErrorCode::ConnectionTimeout
|
||||
)
|
||||
})?
|
||||
.map_err(|e| raise_error!(e.to_string(), ErrorCode::NetworkError))
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use dashmap::DashMap;
|
||||
use governor::{
|
||||
clock::{QuantaClock, QuantaInstant},
|
||||
middleware::NoOpMiddleware,
|
||||
state::{InMemoryState, NotKeyed},
|
||||
NotUntil, Quota, RateLimiter,
|
||||
};
|
||||
use std::{
|
||||
num::NonZero,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::modules::token::RateLimit;
|
||||
|
||||
pub static RATE_LIMITER_MANAGER: LazyLock<TokenRateLimiter> = LazyLock::new(TokenRateLimiter::new);
|
||||
|
||||
pub struct TokenRateLimiter {
|
||||
limiters: Arc<
|
||||
DashMap<
|
||||
String,
|
||||
(
|
||||
Arc<RateLimiter<NotKeyed, InMemoryState, QuantaClock, NoOpMiddleware>>,
|
||||
RateLimit,
|
||||
),
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl TokenRateLimiter {
|
||||
pub fn new() -> Self {
|
||||
TokenRateLimiter {
|
||||
limiters: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check(
|
||||
&self,
|
||||
token: &str,
|
||||
limit: RateLimit,
|
||||
) -> Result<(), NotUntil<QuantaInstant>> {
|
||||
let limiter = self.get_or_update_limiter(token, limit).await;
|
||||
limiter.check()
|
||||
}
|
||||
|
||||
async fn get_or_update_limiter(
|
||||
&self,
|
||||
token: &str,
|
||||
limit: RateLimit,
|
||||
) -> Arc<RateLimiter<NotKeyed, InMemoryState, QuantaClock, NoOpMiddleware>> {
|
||||
self.limiters
|
||||
.entry(token.to_string())
|
||||
.and_modify(|(existing_limiter, current_limit)| {
|
||||
if current_limit.interval != limit.interval || current_limit.quota != limit.quota {
|
||||
let quota = Quota::with_period(Duration::from_secs(limit.interval))
|
||||
.unwrap()
|
||||
.allow_burst(NonZero::new(limit.quota).unwrap());
|
||||
*existing_limiter = Arc::new(RateLimiter::direct_with_clock(
|
||||
quota,
|
||||
QuantaClock::default(),
|
||||
));
|
||||
*current_limit = RateLimit {
|
||||
interval: limit.interval,
|
||||
quota: limit.quota,
|
||||
};
|
||||
}
|
||||
})
|
||||
.or_insert({
|
||||
let quota = Quota::with_period(Duration::from_secs(limit.interval))
|
||||
.unwrap()
|
||||
.allow_burst(NonZero::new(limit.quota).unwrap());
|
||||
(
|
||||
Arc::new(RateLimiter::direct_with_clock(
|
||||
quota,
|
||||
QuantaClock::default(),
|
||||
)),
|
||||
limit,
|
||||
)
|
||||
})
|
||||
.value()
|
||||
.0
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use tokio::signal;
|
||||
|
||||
pub(crate) async fn shutdown_signal() {
|
||||
let ctrl_c_signal = async {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("Error installing Ctrl+C signal handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate_signal = async {
|
||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
.expect("Error installing terminate signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate_signal = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c_signal => {},
|
||||
_ = terminate_signal => {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use crate::{
|
||||
modules::{
|
||||
error::{code::ErrorCode, BichonResult},
|
||||
imap::session::SessionStream,
|
||||
},
|
||||
raise_error,
|
||||
};
|
||||
use rustls::RootCertStore;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn establish_tls_stream(
|
||||
server_hostname: &str,
|
||||
alpn_protocols: &[&str],
|
||||
stream: impl SessionStream + 'static,
|
||||
) -> BichonResult<impl SessionStream> {
|
||||
let tls_stream = establish_rustls_stream(server_hostname, alpn_protocols, stream).await?;
|
||||
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
|
||||
Ok(boxed_stream)
|
||||
}
|
||||
|
||||
pub async fn establish_rustls_stream(
|
||||
server_hostname: &str,
|
||||
alpn_protocols: &[&str],
|
||||
stream: impl SessionStream,
|
||||
) -> BichonResult<impl SessionStream> {
|
||||
// Create a root certificate store and add default trusted roots
|
||||
let root_store = RootCertStore {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.into(),
|
||||
};
|
||||
|
||||
// Configure the Rustls client with the root certs and no client authentication
|
||||
let mut config = rustls::ClientConfig::builder()
|
||||
//builder_with_provider(
|
||||
// rustls::crypto::ring::default_provider().into(),
|
||||
// )
|
||||
// .with_protocol_versions(&[&rustls::version::TLS13])
|
||||
// .unwrap()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
// Set the ALPN protocols
|
||||
config.alpn_protocols = alpn_protocols
|
||||
.iter()
|
||||
.map(|s| s.as_bytes().to_vec())
|
||||
.collect();
|
||||
|
||||
let tls_connector = tokio_rustls::TlsConnector::from(Arc::new(config));
|
||||
|
||||
let server_name = rustls_pki_types::ServerName::try_from(server_hostname)
|
||||
.map_err(|_| raise_error!("Invalid DNS name".into(), ErrorCode::NetworkError))?
|
||||
.to_owned();
|
||||
|
||||
let tls_stream = tls_connector
|
||||
.connect(server_name, stream)
|
||||
.await
|
||||
.map_err(|e| raise_error!(e.to_string(), ErrorCode::NetworkError))?;
|
||||
|
||||
Ok(tls_stream)
|
||||
}
|
||||
Reference in New Issue
Block a user