mod auth; mod config; mod db; mod error; mod hooks; mod mailer; mod models; mod passwords; mod routes; mod sieve; mod state; mod ui; mod validation; use std::{net::SocketAddr, sync::Arc, time::Duration}; use axum::{ Router, http::{HeaderName, HeaderValue, StatusCode}, routing::get, }; use tower_http::{set_header::SetResponseHeaderLayer, timeout::TimeoutLayer, trace::TraceLayer}; use tracing_subscriber::EnvFilter; use crate::{ auth::AuthStore, config::AppConfig, db::Db, error::{AppError, AppResult}, state::AppState, }; #[tokio::main] async fn main() -> Result<(), Box> { if let Err(error) = run().await { eprintln!("postfixadmin-rs: {error}"); std::process::exit(1); } Ok(()) } async fn run() -> AppResult<()> { if std::env::args().any(|arg| arg == "--version" || arg == "-V") { println!("postfixadmin-rs {}", env!("CARGO_PKG_VERSION")); return Ok(()); } tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("postfixadmin_rs=info,tower_http=info")), ) .init(); let config = Arc::new(AppConfig::load()?); let db = Db::connect(&config).await?; if config.database.dev_mock { db.initialize_dev_mock(&config).await?; } db.ping().await?; let warnings = db.check_schema(&config).await?; for warning in &warnings { tracing::warn!(%warning, "schema compatibility warning"); } if std::env::args().any(|arg| arg == "--check-config") { println!("configuration: ok"); println!("database: ok"); if config.database.dev_mock { println!("database mode: development mock"); } if config.database.read_only { println!("database access: read-only"); } for warning in warnings { println!("warning: {warning}"); } return Ok(()); } let state = Arc::new(AppState { auth: AuthStore::new(&config), config: config.clone(), db, }); let api = routes::router(&state); let inner = Router::>::new() .nest("/api", api) .route("/assets/{*path}", get(ui::asset)) .fallback(ui::index) .layer(TimeoutLayer::with_status_code( StatusCode::REQUEST_TIMEOUT, Duration::from_secs(60), )) .layer(SetResponseHeaderLayer::overriding( HeaderName::from_static("content-security-policy"), HeaderValue::from_static("default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'"), )) .layer(SetResponseHeaderLayer::overriding( HeaderName::from_static("x-content-type-options"), HeaderValue::from_static("nosniff"), )) .layer(SetResponseHeaderLayer::overriding( HeaderName::from_static("x-frame-options"), HeaderValue::from_static("DENY"), )) .layer(SetResponseHeaderLayer::overriding( HeaderName::from_static("referrer-policy"), HeaderValue::from_static("no-referrer"), )) .layer(SetResponseHeaderLayer::overriding( HeaderName::from_static("permissions-policy"), HeaderValue::from_static("camera=(), microphone=(), geolocation=()"), )) .layer(TraceLayer::new_for_http()); let base = config.normalized_base_path(); let app: Router = if base == "/" { inner.with_state(state.clone()) } else { Router::>::new() .nest(&base, inner) .with_state(state.clone()) }; let addr: SocketAddr = format!("{}:{}", config.host, config.port) .parse() .map_err(|error| AppError::config(format!("invalid listen address: {error}")))?; tracing::info!(%addr, base_path = %base, "PostfixAdmin-RS listening"); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve( listener, app.into_make_service_with_connect_info::(), ) .await .map_err(|error| AppError::Io(std::io::Error::other(error)))?; Ok(()) }