mod auth; mod calendar; mod config; mod db; mod email_theme; mod error; mod imap; mod pgp; mod realtime; mod routes; mod search; mod sieve; mod smtp; mod ui; use std::sync::Arc; use std::time::Duration; use config::AppConfig; use tracing_subscriber::{EnvFilter, fmt, prelude::*}; use crate::auth::session::SessionStore; use crate::imap::client::RealImapClient; use crate::realtime::idle::IdleManager; use crate::smtp::client::RealSmtpClient; #[tokio::main] async fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); if args.iter().any(|arg| arg == "--version" || arg == "-V") { println!("rustbox {}", env!("CARGO_PKG_VERSION")); return Ok(()); } // Initialize structured JSON logging with env filter. // Default to INFO level; override with RUST_LOG env var. tracing_subscriber::registry() .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) .with(fmt::layer().json()) .init(); // Load configuration via figment (serde defaults + env vars). let loaded_config = AppConfig::load()?; loaded_config.validate().map_err(|message| { std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("invalid RustBox configuration: {message}"), ) })?; if args.iter().any(|arg| arg == "--check-config") { println!("RustBox configuration OK"); return Ok(()); } let config = Arc::new(loaded_config); // Create the in-memory session store with the configured timeout. let store = Arc::new(SessionStore::new(Duration::from_secs( config.session_timeout_hours * 3600, ))); // Spawn a background task that periodically purges expired sessions. { let store = Arc::clone(&store); tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(300)); loop { interval.tick().await; store.purge_expired(); tracing::debug!("Purged expired sessions"); } }); } // Create the IMAP and SMTP clients for production use. let imap_client: Arc = Arc::new(RealImapClient::default()); let smtp_client: Arc = Arc::new(RealSmtpClient); // Create the Tantivy search engine for full-text indexing. let search_engine = Arc::new(search::engine::SearchEngine::new(std::path::PathBuf::from( &config.data_dir, ))); // Create the real-time event bus and IDLE manager. let event_bus = Arc::new(realtime::events::EventBus::new()); let idle_manager = Arc::new(realtime::idle::IdleManager::new()); // Build the application router with auth, session, and static file serving. let app = routes::create_router( config.clone(), store, imap_client, smtp_client, search_engine, event_bus, idle_manager.clone(), ); // Bind to the configured host and port. let bind_addr = format!("{}:{}", config.host, config.port); let listener = tokio::net::TcpListener::bind(&bind_addr).await?; tracing::info!( host = %config.host, port = %config.port, "RustBox server starting" ); axum::serve( listener, app.into_make_service_with_connect_info::(), ) .with_graceful_shutdown(shutdown_signal(idle_manager)) .await?; tracing::info!("RustBox server stopped"); Ok(()) } async fn shutdown_signal(idle_manager: Arc) { let ctrl_c = async { if let Err(error) = tokio::signal::ctrl_c().await { tracing::error!(error = %error, "Failed to install Ctrl+C handler"); } }; #[cfg(unix)] let terminate = async { match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { Ok(mut signal) => { signal.recv().await; } Err(error) => { tracing::error!(error = %error, "Failed to install SIGTERM handler"); std::future::pending::<()>().await; } } }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, } tracing::info!("Shutdown signal received; stopping IDLE tasks"); idle_manager.stop_all(); }