initial commit

This commit is contained in:
rustmailer
2025-11-19 02:14:37 +08:00
commit 1a8f95117e
355 changed files with 54089 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
//
// 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 clap::{builder::ValueParser, Parser, ValueEnum};
use std::{collections::HashSet, env, fmt, path::PathBuf, sync::LazyLock};
pub static SETTINGS: LazyLock<Settings> = LazyLock::new(Settings::parse);
#[derive(Debug, Parser)]
#[clap(
name = "bichon",
about = "A self-hosted email synchronization and backup tool built in Rust",
version = env!("CARGO_PKG_VERSION")
)]
pub struct Settings {
/// bichon log level (default: "info")
#[clap(
long,
default_value = "info",
env,
help = "Set the log level for bichon"
)]
pub bichon_log_level: String,
/// bichon HTTP port (default: 15630)
#[clap(
long,
default_value = "15630",
env,
help = "Set the HTTP port for bichon"
)]
pub bichon_http_port: i32,
/// The IP address that the node binds to, in IPv4 format (e.g., 192.168.1.1).
#[clap(
long,
env,
default_value = "0.0.0.0",
help = "The IP address that the node binds to, in IPv4 format (e.g., 192.168.1.1). Required in cluster mode.",
value_parser = ValueParser::new(|s: &str| {
// Ensure the input is a valid IPv4 address
if s.parse::<std::net::Ipv4Addr>().is_err() {
return Err("The bind IP address must be a valid IPv4 address.".to_string());
}
// If the address is valid, return it
Ok(s.to_string())
})
)]
pub bichon_bind_ip: Option<String>,
/// RustMail public URL (default: "http://localhost:15630")
#[clap(
long,
default_value = "http://localhost:15630",
env,
help = "Set the public URL for bichon"
)]
pub bichon_public_url: String,
/// CORS allowed origins (default: "*")
#[clap(
long,
default_value = "http://localhost:5173, http://localhost:15630, http://192.168.3.2:15630, *",
env,
help = "Set the allowed CORS origins (comma-separated list, e.g., \"https://example.com, https://another.com\")",
value_parser = ValueParser::new(|s: &str| -> Result<HashSet<String>, String> {
let set: HashSet<String> = s.split(',')
.map(|origin| origin.trim().to_string())
.filter(|origin| !origin.is_empty())
.collect();
Ok(set)
})
)]
pub bichon_cors_origins: HashSet<String>,
/// CORS max age in seconds (default: 86400)
#[clap(
long,
default_value = "86400",
env,
help = "Set the CORS max age in seconds"
)]
pub bichon_cors_max_age: i32,
/// Enable ANSI logs (default: false)
#[clap(long, default_value = "true", env, help = "Enable ANSI formatted logs")]
pub bichon_ansi_logs: bool,
/// Enable log file output (default: false)
/// If false, logs will be printed to stdout
#[clap(
long,
default_value = "false",
env,
help = "Enable log file output (otherwise logs go to stdout)"
)]
pub bichon_log_to_file: bool,
/// Enable JSON logs (default: false)
#[clap(
long,
default_value = "false",
env,
help = "Enable JSON formatted logs"
)]
pub bichon_json_logs: bool,
/// Maximum number of log files (default: 5)
#[clap(
long,
default_value = "5",
env,
help = "Set the maximum number of server log files"
)]
pub bichon_max_server_log_files: usize,
/// bichon encryption password
#[clap(
long,
default_value = "change-this-default-password-now",
env,
help = "Set the encryption password for bichon. ⚠️ Change this default in production!"
)]
pub bichon_encrypt_password: String,
#[clap(
long,
env,
help = "Set the file path for bichon database",
value_parser = ValueParser::new(|s: &str| {
let path = PathBuf::from(s);
if !path.is_absolute() {
return Err("Path must be an absolute directory path".to_string());
}
if !path.exists() {
return Err(format!("Path {:?} does not exist", path));
}
if !path.is_dir() {
return Err(format!("Path {:?} is not a directory", path));
}
Ok(s.to_string())
})
)]
pub bichon_root_dir: String,
#[clap(
long,
env,
default_value = "134217728",
help = "Set the cache size for bichon metadata database in bytes"
)]
pub bichon_metadata_cache_size: Option<usize>,
#[clap(
long,
env,
default_value = "1073741824",
help = "Set the cache size for envelope database in bytes"
)]
pub bichon_envelope_cache_size: Option<usize>,
/// Enables or disables the access token mechanism for HTTP endpoints.
///
/// When set to `true`, HTTP requests will be subject to access token validation.
/// If the `Authorization` header is missing or the token is invalid, the service will return a 401 Unauthorized response.
/// When set to `false`, access token validation will be skipped.
#[clap(
long,
default_value = "false",
env,
help = "Enables or disables the access token mechanism for HTTP endpoints."
)]
pub bichon_enable_access_token: bool,
/// Enables or disables HTTPS for REST API endpoints.
///
/// When set to `true`, the REST API will use HTTPS with a valid SSL/TLS certificate for secure communication.
/// If no valid certificate is configured or HTTPS cannot be established, the service will fail to start.
/// When set to `false`, the REST API will use plain HTTP without encryption.
#[clap(
long,
default_value = "false",
env,
help = "Enables or disables HTTPS for REST API endpoints."
)]
pub bichon_enable_rest_https: bool,
#[clap(
long,
default_value = "true",
env,
help = "Enable compression for the open api server"
)]
pub bichon_http_compression_enabled: bool,
#[clap(
long,
env,
help = "Maximum number of concurrent email sync tasks (default: number of CPU cores x 2)",
value_parser = clap::value_parser!(u16).range(1..)
)]
pub bichon_sync_concurrency: Option<u16>,
}
#[derive(Clone, Copy, Debug, PartialEq, ValueEnum)]
pub enum CompressionAlgorithm {
#[clap(name = "none")]
None,
#[clap(name = "gzip")]
Gzip,
#[clap(name = "brotli")]
Brotli,
#[clap(name = "zstd")]
Zstd,
#[clap(name = "deflate")]
Deflate,
}
impl fmt::Display for CompressionAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompressionAlgorithm::None => write!(f, "none"),
CompressionAlgorithm::Gzip => write!(f, "gzip"),
CompressionAlgorithm::Brotli => write!(f, "brotli"),
CompressionAlgorithm::Zstd => write!(f, "zstd"),
CompressionAlgorithm::Deflate => write!(f, "deflate"),
}
}
}
+81
View File
@@ -0,0 +1,81 @@
//
// 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::context::Initialize;
use crate::modules::settings::cli::SETTINGS;
use crate::{
modules::error::{code::ErrorCode, BichonResult},
raise_error,
};
use std::path::PathBuf;
use std::sync::LazyLock;
pub const META_FILE: &str = "meta.db";
pub const MAILBOX_FILE: &str = "mailbox.db";
const ENVELOPE_DIR: &str = "envelope";
const EML_DIR: &str = "eml";
const TMP_DIR: &str = "tmp";
const LOG_DIR: &str = "logs";
const TLS_CERT: &str = "cert.pem";
const TLS_KEY: &str = "key.pem";
pub static DATA_DIR_MANAGER: LazyLock<DataDirManager> =
LazyLock::new(|| DataDirManager::new(PathBuf::from(&SETTINGS.bichon_root_dir)));
#[derive(Debug)]
pub struct DataDirManager {
pub root_dir: PathBuf,
pub meta_db: PathBuf,
pub mailbox_db: PathBuf,
pub temp_dir: PathBuf,
pub tls_cert: PathBuf,
pub tls_key: PathBuf,
pub envelope_dir: PathBuf,
pub eml_dir: PathBuf,
pub log_dir: PathBuf
}
impl Initialize for DataDirManager {
async fn initialize() -> BichonResult<()> {
std::fs::create_dir_all(&DATA_DIR_MANAGER.root_dir)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
std::fs::create_dir_all(&DATA_DIR_MANAGER.log_dir)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
std::fs::create_dir_all(&DATA_DIR_MANAGER.temp_dir)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
Ok(())
}
}
impl DataDirManager {
pub fn new(root_dir: PathBuf) -> Self {
Self {
root_dir: root_dir.clone(),
meta_db: root_dir.join(META_FILE),
mailbox_db: root_dir.join(MAILBOX_FILE),
tls_key: root_dir.join(TLS_KEY),
tls_cert: root_dir.join(TLS_CERT),
log_dir: root_dir.join(LOG_DIR),
envelope_dir: root_dir.join(ENVELOPE_DIR),
temp_dir: root_dir.join(TMP_DIR),
eml_dir: root_dir.join(EML_DIR),
}
}
}
+23
View File
@@ -0,0 +1,23 @@
//
// 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/>.
pub mod cli;
pub mod dir;
pub mod proxy;
pub mod system;
+145
View File
@@ -0,0 +1,145 @@
//
// 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 native_db::*;
use native_model::{native_model, Model};
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
use crate::{
id,
modules::{
database::{
async_find_impl, delete_impl, insert_impl, list_all_impl, manager::DB_MANAGER,
update_impl,
},
error::{code::ErrorCode, BichonResult},
utils::net::parse_proxy_addr,
},
raise_error, utc_now,
};
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
#[native_model(id = 8, version = 1)]
#[native_db]
pub struct Proxy {
/// The unique identifier for this proxy configuration.
#[primary_key]
pub id: u64,
/// The proxy URL (e.g., socks5://127.0.0.1:1080) used to route network requests.
pub url: String,
/// The creation timestamp of this record, represented as milliseconds since the Unix epoch.
pub created_at: i64,
/// The last update timestamp of this record, represented as milliseconds since the Unix epoch.
pub updated_at: i64,
}
impl Proxy {
/// Create a new Proxy instance with the given URL and timestamps.
pub fn new(url: String) -> Self {
Self {
id: id!(64),
url,
created_at: utc_now!(),
updated_at: utc_now!(),
}
}
pub async fn get(id: u64) -> BichonResult<Proxy> {
async_find_impl(DB_MANAGER.meta_db(), id)
.await?
.ok_or_else(|| {
raise_error!(
format!("Proxy with id={} not found", id),
ErrorCode::ResourceNotFound
)
})
}
pub async fn list_all() -> BichonResult<Vec<Proxy>> {
list_all_impl(DB_MANAGER.meta_db()).await
}
pub async fn delete(id: u64) -> BichonResult<()> {
delete_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get()
.primary::<Proxy>(id)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
.ok_or_else(|| raise_error!("proxy missing".into(), ErrorCode::InternalError))
})
.await
}
pub async fn update(id: u64, url: String) -> BichonResult<()> {
update_impl(
DB_MANAGER.meta_db(),
move |rw| {
rw.get()
.primary::<Proxy>(id)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
.ok_or_else(|| {
raise_error!(
format!("Proxy with id={} not found", id),
ErrorCode::ResourceNotFound
)
})
},
move |current| {
let mut updated = current.clone();
updated.url = url;
updated.updated_at = utc_now!();
Ok(updated)
},
)
.await?;
Ok(())
}
pub async fn save(&self) -> BichonResult<()> {
self.validate()?;
insert_impl(DB_MANAGER.meta_db(), self.to_owned()).await
}
/// Validate that the URL is a valid SOCKS5 proxy URL.
pub fn validate(&self) -> BichonResult<()> {
parse_proxy_addr(&self.url)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_proxy_urls() {
let urls = vec![
"socks5://127.0.0.1:1080",
"http://127.0.0.1:8080",
];
for url in urls {
let proxy = Proxy::new(url.to_string());
assert!(proxy.validate().is_ok(), "URL should be valid: {}", url);
}
}
}
+70
View File
@@ -0,0 +1,70 @@
//
// 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::database::manager::DB_MANAGER;
use crate::modules::database::{find_impl, upsert_impl};
use crate::modules::error::BichonResult;
use crate::utc_now;
use native_db::*;
use native_model::{native_model, Model};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[native_model(id = 2, version = 1)]
#[native_db]
pub struct SystemSetting {
#[primary_key]
pub key: String,
pub value: String,
pub created_at: i64,
pub updated_at: i64,
}
impl SystemSetting {
pub fn new(key: String, value: String) -> Self {
Self {
key,
value,
created_at: utc_now!(),
updated_at: utc_now!(),
}
}
//overwrite
pub async fn set(&self) -> BichonResult<()> {
upsert_impl(DB_MANAGER.meta_db(), self.to_owned()).await
}
pub fn get(key: &str) -> BichonResult<Option<SystemSetting>> {
find_impl(DB_MANAGER.meta_db(), key)
}
// pub async fn list() -> RustMailerResult<Vec<SystemSetting>> {
// list_all_impl(DB_MANAGER.metadata_db()).await
// }
pub fn get_existing_value(key: &str) -> BichonResult<Option<String>> {
let setting = Self::get(key)?;
Ok(setting.map(|s| s.value))
}
pub async fn set_value(key: &str, value: String) -> BichonResult<()> {
let setting = Self::new(key.to_string(), value);
setting.set().await
}
}