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,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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user