2025-11-19 02:14:37 +08:00
|
|
|
//
|
2026-03-10 01:32:57 +08:00
|
|
|
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
2025-11-19 02:14:37 +08:00
|
|
|
//
|
|
|
|
|
// 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};
|
|
|
|
|
|
2025-12-26 14:17:34 +08:00
|
|
|
use crate::modules::error::BichonResult;
|
|
|
|
|
use base64::engine::general_purpose::STANDARD;
|
2025-11-19 02:14:37 +08:00
|
|
|
use base64::{engine::general_purpose, Engine};
|
2026-03-03 12:30:26 +08:00
|
|
|
use rand::{rng, RngExt};
|
2025-11-19 02:14:37 +08:00
|
|
|
|
|
|
|
|
use super::error::code::ErrorCode;
|
|
|
|
|
|
|
|
|
|
pub mod encrypt;
|
2026-01-28 13:19:33 +08:00
|
|
|
pub mod html;
|
2025-11-19 02:14:37 +08:00
|
|
|
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,
|
|
|
|
|
code: $code,
|
2026-03-18 01:04:41 +08:00
|
|
|
location: snafu::location!(),
|
2025-11-19 02:14:37 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-17 11:39:02 +08:00
|
|
|
|
2025-11-19 02:14:37 +08:00
|
|
|
#[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! 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 01:04:41 +08:00
|
|
|
pub fn hex_hash(s: &str) -> String {
|
|
|
|
|
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();
|
|
|
|
|
format!("{:032x}", hash)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 02:14:37 +08:00
|
|
|
pub fn create_hash(account_id: u64, field: &str) -> u64 {
|
|
|
|
|
let mut buffer = Vec::new();
|
2026-03-03 12:30:26 +08:00
|
|
|
buffer.extend_from_slice(&account_id.to_le_bytes());
|
2025-11-19 02:14:37 +08:00
|
|
|
buffer.push(b':'); // Separator
|
2026-03-03 12:30:26 +08:00
|
|
|
buffer.extend_from_slice(field.as_bytes());
|
|
|
|
|
let mut cursor = std::io::Cursor::new(buffer);
|
|
|
|
|
let hash = murmur3::murmur3_x64_128(&mut cursor, 0).unwrap();
|
|
|
|
|
(hash & 0x1F_FFFF_FFFF_FFFF) as u64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn create_hash2(account_id: u64, field1: u64, field2: &str) -> u64 {
|
|
|
|
|
let mut buffer = Vec::new();
|
|
|
|
|
buffer.extend_from_slice(&account_id.to_le_bytes());
|
|
|
|
|
buffer.push(b':'); // Separator
|
|
|
|
|
buffer.extend_from_slice(&field1.to_le_bytes());
|
|
|
|
|
buffer.push(b':'); // Separator
|
|
|
|
|
buffer.extend_from_slice(field2.as_bytes());
|
2025-11-19 02:14:37 +08:00
|
|
|
let mut cursor = std::io::Cursor::new(buffer);
|
|
|
|
|
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)
|
|
|
|
|
}
|
2025-12-26 14:17:34 +08:00
|
|
|
|
|
|
|
|
const MAX_AVATAR_BYTES: usize = 128 * 1024;
|
|
|
|
|
|
|
|
|
|
pub fn decode_avatar_bytes(base64_str: &str) -> BichonResult<Vec<u8>> {
|
|
|
|
|
let bytes = STANDARD.decode(base64_str).map_err(|e| {
|
|
|
|
|
raise_error!(
|
|
|
|
|
format!("Invalid avatar base64 encoding: {}", e),
|
|
|
|
|
ErrorCode::InvalidParameter
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
if bytes.len() > MAX_AVATAR_BYTES {
|
|
|
|
|
return Err(raise_error!(
|
|
|
|
|
format!(
|
|
|
|
|
"Avatar image exceeds maximum size ({} KB).",
|
|
|
|
|
MAX_AVATAR_BYTES / 1024
|
|
|
|
|
),
|
|
|
|
|
ErrorCode::InvalidParameter
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(bytes)
|
|
|
|
|
}
|
2026-03-03 12:30:26 +08:00
|
|
|
|
|
|
|
|
pub fn validate_tag(tag: &str) -> Result<(), String> {
|
|
|
|
|
if tag.is_empty() {
|
|
|
|
|
return Err("Tag cannot be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const INVALID: &[char] = &[
|
|
|
|
|
'\'', '"', '`', ';', ',', '(', ')', '[', ']', '{', '}', '<', '>',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let mut found = Vec::new();
|
|
|
|
|
|
|
|
|
|
for c in tag.chars() {
|
|
|
|
|
if INVALID.contains(&c) && !found.contains(&c) {
|
|
|
|
|
found.push(c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !found.is_empty() {
|
|
|
|
|
let chars: String = found.iter().collect();
|
|
|
|
|
return Err(format!("Tag contains invalid characters: {}", chars));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-03-18 01:04:41 +08:00
|
|
|
|
|
|
|
|
pub fn content_hash(content: &[u8]) -> String {
|
|
|
|
|
let hash = blake3::hash(content);
|
|
|
|
|
hash.to_hex().to_string()
|
|
|
|
|
}
|