From 575f851cfbdb5802e70d3ec81bb9c7cc30ac519e Mon Sep 17 00:00:00 2001 From: rustmailer Date: Sun, 24 May 2026 02:27:52 +0800 Subject: [PATCH] update --- Cargo.lock | 12 +-- Cargo.toml | 5 +- crates/server/src/lib.rs | 133 +++++++++++++++++++++++ crates/server/src/main.rs | 195 +--------------------------------- crates/server/src/rest/mod.rs | 45 ++++---- 5 files changed, 168 insertions(+), 222 deletions(-) create mode 100644 crates/server/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index bf2b38f..8f02438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2266,9 +2266,9 @@ checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libmimalloc-sys" -version = "0.1.48" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2892ae4ea6fa2cb7acb0e236a6880d39523239cd9089de71d220910ccc806790" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" dependencies = [ "cc", ] @@ -2479,9 +2479,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebca48a43116bc25f18a61360f1be98412f50cc218f5e52c823086b999a4a21a" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" dependencies = [ "libmimalloc-sys", ] @@ -3943,9 +3943,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 6d4ed65..15ba551 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/server", "crates/cli", "crates/admin", + "crates/smtp", ] resolver = "2" @@ -17,12 +18,12 @@ edition = "2021" [workspace.dependencies] chrono = "0.4.44" clap = { version = "4.6.1", features = ["derive", "env"] } -mimalloc = "0.1.51" +mimalloc = "0.1.52" memdb = { path = "crates/memdb" } itertools = "0.14.0" ring = { version = "0.17.14", features = ["std"] } serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.149" +serde_json = "1.0.150" tokio = { version = "1.52.3", features = ["full"] } tracing = "0.1.44" tracing-appender = "0.2.3" diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs new file mode 100644 index 0000000..b450714 --- /dev/null +++ b/crates/server/src/lib.rs @@ -0,0 +1,133 @@ +// +// Copyright (c) 2025-2026 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 . + + +pub mod common; +pub mod error; +pub mod rest; + +use std::sync::LazyLock; + +use bichon_core::{ + bichon_version, + cache::imap::task::SYNC_TASKS, + common::{rustls::BichonTls, signal::SignalManager}, + context::{executors::BichonContext, Initialize}, + error::{code::ErrorCode, BichonResult}, + logger, + migrate::check_data_status, + raise_error, + settings::{cli::SETTINGS, dir::DataDirManager}, + store::{ + blob::BLOB_MANAGER, + tantivy::{attachment::ATTACHMENT_MANAGER, envelope::ENVELOPE_MANAGER}, + }, + tasks::PeriodicTasks, + users::manager::UserManager, +}; +use bichon_smtp::server::{start_smtp_server, SmtpServer}; +use tracing::{error, info}; + +pub async fn run() -> BichonResult<()> { + logger::initialize_logging(); + info!( + r#" + _ _ _ + | | (_) | | + | |__ _ ___ | |__ ___ _ __ + | '_ \ | | / __|| '_ \ / _ \ | '_ \ + | |_) || || (__ | | | || (_) || | | | + |_.__/ |_| \___||_| |_| \___/ |_| |_| + + "# + ); + info!("Starting bichon-server"); + info!("Version: {}", bichon_version!()); + info!("Git: [{}]", env!("GIT_HASH")); + info!("GitHub: https://github.com/rustmailer/bichon"); + + match check_data_status() { + Ok(false) => { + error!("Incompatible data format detected."); + error!("Your data was created by an older version of Bichon and must be migrated before use."); + error!("Please stop the Bichon v0.3.7 service before migration."); + error!("Please run: bichon-admin"); + error!("Documentation: https://github.com/rustmailer/bichon/wiki/Bichon-Data-Migration:-v0.3.7-%E2%86%92-v1.0"); + return Err(raise_error!( + "Legacy data layout detected".into(), + ErrorCode::InternalError + )); + } + Err(e) => { + error!("Failed to check data layout: {:#?}", e); + return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)); + } + Ok(true) => {} + } + + if let Err(error) = initialize().await { + eprintln!("{:?}", error); + return Err(error); + } + + let periodic_tasks = PeriodicTasks::setup(); + let mut smtp_service: Option = None; + if SETTINGS.bichon_enable_smtp { + info!("SMTP service is enabled, starting..."); + match start_smtp_server().await { + Ok(server) => { + info!("SMTP server listening on: {}", server.smtp_addr); + smtp_service = Some(server); + } + Err(e) => { + error!("Failed to start SMTP server: {}", e); + return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)); + } + } + } else { + info!("SMTP service is disabled by configuration."); + } + + rest::start_http_server().await?; + periodic_tasks.shutdown().await; + + if let Some(server) = smtp_service { + info!("Shutting down SMTP server..."); + server.stop().await; + info!("SMTP server stopped."); + } + + SYNC_TASKS.shutdown().await; + ENVELOPE_MANAGER.shutdown().await; + ATTACHMENT_MANAGER.shutdown().await; + BLOB_MANAGER.shutdown().await; + info!("Bichon server stopped."); + Ok(()) +} + +async fn initialize() -> BichonResult<()> { + SignalManager::initialize().await?; + DataDirManager::initialize().await?; + UserManager::initialize().await?; + BichonTls::initialize().await?; + BichonContext::initialize().await?; + LazyLock::force(&BLOB_MANAGER); + LazyLock::force(&ENVELOPE_MANAGER); + LazyLock::force(&ATTACHMENT_MANAGER); + Ok(()) +} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index ac51591..e3802de 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -16,205 +16,14 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::sync::LazyLock; -use bichon_core::{ - bichon_version, - cache::imap::task::SYNC_TASKS, - common::rustls::BichonTls, - context::{executors::BichonContext, Initialize}, - error::{code::ErrorCode, BichonResult}, - logger, - migrate::check_data_status, - raise_error, - settings::cli::SETTINGS, - store::{ - blob::BLOB_MANAGER, - tantivy::{attachment::ATTACHMENT_MANAGER, envelope::ENVELOPE_MANAGER}, - }, - tasks::PeriodicTasks, -}; -use bichon_smtp::server::{start_smtp_server, SmtpServer}; +use bichon_core::error::BichonResult; use mimalloc::MiMalloc; -use tracing::{error, info}; - -use bichon_core::{ - common::signal::SignalManager, settings::dir::DataDirManager, users::manager::UserManager, -}; - -use crate::rest::start_http_server; - -pub mod common; -pub mod error; -pub mod rest; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; -static LOGO: &str = r#" - _ _ _ -| | (_) | | -| |__ _ ___ | |__ ___ _ __ -| '_ \ | | / __|| '_ \ / _ \ | '_ \ -| |_) || || (__ | | | || (_) || | | | -|_.__/ |_| \___||_| |_| \___/ |_| |_| - -"#; #[tokio::main] async fn main() -> BichonResult<()> { - logger::initialize_logging(); - info!("{}", LOGO); - info!("Starting bichon-server"); - info!("Version: {}", bichon_version!()); - info!("Git: [{}]", env!("GIT_HASH")); - info!("GitHub: https://github.com/rustmailer/bichon"); - - match check_data_status() { - Ok(false) => { - error!("Incompatible data format detected."); - error!("Your data was created by an older version of Bichon and must be migrated before use."); - error!("Please stop the Bichon v0.3.7 service before migration."); - error!("Please run: bichon-admin"); - error!("Documentation: https://github.com/rustmailer/bichon/wiki/Bichon-Data-Migration:-v0.3.7-%E2%86%92-v1.0"); - return Err(raise_error!( - "Legacy data layout detected".into(), - ErrorCode::InternalError - )); - } - Err(e) => { - error!("Failed to check data layout: {:#?}", e); - return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)); - } - Ok(true) => {} - } - - if let Err(error) = initialize().await { - eprintln!("{:?}", error); - return Err(error); - } - - let periodic_tasks = PeriodicTasks::setup(); - let mut smtp_service: Option = None; - if SETTINGS.bichon_enable_smtp { - info!("SMTP service is enabled, starting..."); - match start_smtp_server().await { - Ok(server) => { - info!("SMTP server listening on: {}", server.smtp_addr); - smtp_service = Some(server); - } - Err(e) => { - error!("Failed to start SMTP server: {}", e); - return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)); - } - } - } else { - info!("SMTP service is disabled by configuration."); - } - - start_http_server().await?; - periodic_tasks.shutdown().await; - - if let Some(server) = smtp_service { - info!("Shutting down SMTP server..."); - server.stop().await; - info!("SMTP server stopped."); - } - - SYNC_TASKS.shutdown().await; - ENVELOPE_MANAGER.shutdown().await; - ATTACHMENT_MANAGER.shutdown().await; - BLOB_MANAGER.shutdown().await; - info!("Bichon server stopped."); - Ok(()) -} - -/// Initialize the system by validating settings and starting necessary tasks. -async fn initialize() -> BichonResult<()> { - SignalManager::initialize().await?; - DataDirManager::initialize().await?; - UserManager::initialize().await?; - BichonTls::initialize().await?; - BichonContext::initialize().await?; - LazyLock::force(&BLOB_MANAGER); - LazyLock::force(&ENVELOPE_MANAGER); - LazyLock::force(&ATTACHMENT_MANAGER); - Ok(()) -} - -#[cfg(test)] -mod tests; - -#[cfg(test)] -mod api_tests { - use super::rest::api::create_openapi_service; - use poem::test::TestClient; - - #[tokio::test] - async fn openapi_spec_json_is_served() { - let api_service = create_openapi_service(); - let spec_endpoint = api_service.spec_endpoint(); - let cli = TestClient::new(spec_endpoint); - - let resp = cli.get("/").send().await; - resp.assert_status_is_ok(); - - let body = resp.json().await; - let obj = body.value().object(); - assert!(obj.get_opt("openapi").is_some(), "missing openapi version"); - assert!(obj.get_opt("info").is_some(), "missing info section"); - assert!(obj.get_opt("paths").is_some(), "missing paths section"); - } - - #[tokio::test] - async fn openapi_spec_yaml_is_served() { - let api_service = create_openapi_service(); - let spec_endpoint = api_service.spec_endpoint_yaml(); - let cli = TestClient::new(spec_endpoint); - - let resp = cli.get("/").send().await; - resp.assert_status_is_ok(); - } - - #[tokio::test] - async fn swagger_ui_is_served() { - let api_service = create_openapi_service(); - let swagger = api_service.swagger_ui(); - let cli = TestClient::new(swagger); - - let resp = cli.get("/").send().await; - resp.assert_status_is_ok(); - } - - #[tokio::test] - async fn openapi_spec_lists_all_tag_groups() { - let api_service = create_openapi_service(); - let spec_endpoint = api_service.spec_endpoint(); - let cli = TestClient::new(spec_endpoint); - - let resp = cli.get("/").send().await; - let body = resp.json().await; - let value = body.value(); - - let tag_names: Vec<&str> = value - .object() - .get("tags") - .array() - .iter() - .map(|v| v.object().get("name").string()) - .collect(); - - assert!( - tag_names.contains(&"AccessToken"), - "missing AccessToken tag" - ); - assert!(tag_names.contains(&"Attachment"), "missing Attachment tag"); - assert!(tag_names.contains(&"AutoConfig"), "missing AutoConfig tag"); - assert!(tag_names.contains(&"Account"), "missing Account tag"); - assert!(tag_names.contains(&"System"), "missing System tag"); - assert!(tag_names.contains(&"Mailbox"), "missing Mailbox tag"); - assert!(tag_names.contains(&"OAuth2"), "missing OAuth2 tag"); - assert!(tag_names.contains(&"Message"), "missing Message tag"); - assert!(tag_names.contains(&"Import"), "missing Import tag"); - assert!(tag_names.contains(&"Users"), "missing Users tag"); - } + bichon_server::run().await } diff --git a/crates/server/src/rest/mod.rs b/crates/server/src/rest/mod.rs index f9b0b32..1349665 100644 --- a/crates/server/src/rest/mod.rs +++ b/crates/server/src/rest/mod.rs @@ -19,26 +19,25 @@ use crate::common::error::ErrorCapture; use crate::common::log::Tracing; use crate::common::tls::rustls_config; +use crate::common::timeout::{Timeout, TIMEOUT_HEADER}; use crate::error::handler::error_handler; use crate::rest::public::login::login; use crate::rest::public::status::get_status; use bichon_core::common::signal::SIGNAL_MANAGER; use bichon_core::error::code::ErrorCode; use bichon_core::error::BichonResult; +use bichon_core::raise_error; use bichon_core::settings::cli::SETTINGS; use super::error::ApiErrorResponse; use crate::common::auth::ApiGuard; -use crate::common::timeout::{Timeout, TIMEOUT_HEADER}; use api::create_openapi_service; use assets::FrontEndAssets; -use bichon_core::raise_error; use http::{HeaderValue, Method}; use poem::endpoint::EmbeddedFilesEndpoint; use poem::listener::{Listener, TcpListener}; -use poem::middleware::{CatchPanic, Compression, SetHeader}; -use poem::{get, handler, post, IntoResponse}; -use poem::{middleware::Cors, EndpointExt, Route, Server}; +use poem::middleware::{CatchPanic, Compression, Cors, SetHeader}; +use poem::{get, handler, post, Endpoint, EndpointExt, IntoResponse, Route, Server}; use public::oauth2::oauth2_callback; use std::collections::HashSet; use std::time::Duration; @@ -49,18 +48,9 @@ pub mod public; pub type ApiResult = std::result::Result; -pub async fn start_http_server() -> BichonResult<()> { - let listener = TcpListener::bind(( - SETTINGS.bichon_bind_ip.clone().unwrap_or("0.0.0.0".into()), - SETTINGS.bichon_http_port as u16, - )); - - let listener = if SETTINGS.bichon_enable_rest_https { - listener.rustls(rustls_config()?).boxed() - } else { - listener.boxed() - }; - +/// Build the community route tree. Pro/Enterprise servers can call this +/// and then add their own routes before passing the tree to the server. +pub fn build_routes() -> impl Endpoint { let api_service = create_openapi_service() .summary("A lightweight, high-performance Rust email archiver with WebUI"); @@ -79,7 +69,6 @@ pub async fn start_http_server() -> BichonResult<()> { .with(Tracing); let cors_origins: Option> = SETTINGS.bichon_cors_origins.clone(); - let cors_origins: Vec = cors_origins.unwrap_or_default().into_iter().collect(); let cors = Cors::new() @@ -92,7 +81,6 @@ pub async fn start_http_server() -> BichonResult<()> { } cors_origins.iter().any(|o| o == origin) }) - //.allow_origins(cors_origins) .allow_credentials(true) .allow_methods(&[ Method::GET, @@ -131,11 +119,26 @@ pub async fn start_http_server() -> BichonResult<()> { ) .at("/*", serve_index_with_base); - let route = Route::new() + Route::new() .nest(&SETTINGS.bichon_base_url, app_logic) .with(cors) .with_if(SETTINGS.bichon_http_compression_enabled, Compression::new()) - .with(CatchPanic::new()); + .with(CatchPanic::new()) +} + +pub async fn start_http_server() -> BichonResult<()> { + let listener = TcpListener::bind(( + SETTINGS.bichon_bind_ip.clone().unwrap_or("0.0.0.0".into()), + SETTINGS.bichon_http_port as u16, + )); + + let listener = if SETTINGS.bichon_enable_rest_https { + listener.rustls(rustls_config()?).boxed() + } else { + listener.boxed() + }; + + let route = build_routes(); let mut rx = SIGNAL_MANAGER.subscribe(); let shutdown_fut = async move {