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 {