use crate::{ db::{Table, row_i64}, error::AppResult, passwords, routes, state::AppState, }; use axum::{Json, extract::State}; use axum_extra::extract::cookie::CookieJar; use serde::Serialize; use std::sync::Arc; #[derive(Serialize)] pub struct Health { status: &'static str, version: &'static str, } pub async fn health(State(state): State>) -> AppResult> { state.db.ping().await?; Ok(Json(Health { status: "ok", version: env!("CARGO_PKG_VERSION"), })) } pub async fn overview( State(state): State>, jar: CookieJar, ) -> AppResult> { let s = routes::session(&state, &jar).await?; if !s.role.is_admin() { return Ok(Json(serde_json::json!({ "role": s.role, "username": s.username, }))); } let scope: Option> = if s.role.is_global() { None } else { Some(routes::scoped_domains(&s)) }; let (in_clause, binds) = match &scope { Some(domains) if domains.is_empty() => { return Ok(Json(serde_json::json!({ "domains": 0, "mailboxes": 0, "aliases": 0, "alias_domains": 0, "role": s.role, "username": s.username, }))); } Some(domains) => ( format!(" AND {{col}} IN ({})", vec!["?"; domains.len()].join(",")), domains.clone(), ), None => (String::new(), Vec::new()), }; let db = &state.db; let domain = db.table(Table::Domain); let mailbox = db.table(Table::Mailbox); let alias = db.table(Table::Alias); let mut counts = serde_json::Map::new(); let queries = vec![ ( "domains", format!( "SELECT COUNT(*) AS n FROM {domain} d WHERE d.domain <> 'ALL'{}", in_clause.replace("{col}", "d.domain") ), ), ( "mailboxes", format!( "SELECT COUNT(*) AS n FROM {mailbox} m WHERE 1=1{}", in_clause.replace("{col}", "m.domain") ), ), ( "aliases", format!( "SELECT COUNT(*) AS n FROM {alias} a LEFT JOIN {mailbox} m ON m.username=a.address WHERE m.username IS NULL{}", in_clause.replace("{col}", "a.domain") ), ), ]; for (key, sql) in queries { let sql = db.q(&sql); let mut q = sqlx::query(&sql); for b in &binds { q = q.bind(b.as_str()); } counts.insert( key.into(), row_i64(&q.fetch_one(db.pool()).await?, "n").into(), ); } let alias_domains = if state.config.features.alias_domain && db.has_table(Table::AliasDomain).await { let ad = db.table(Table::AliasDomain); let sql = db.q(&format!( "SELECT COUNT(*) AS n FROM {ad} x WHERE 1=1{}", in_clause.replace("{col}", "x.target_domain") )); let mut q = sqlx::query(&sql); for b in &binds { q = q.bind(b.as_str()); } row_i64(&q.fetch_one(db.pool()).await?, "n") } else { 0 }; counts.insert("alias_domains".into(), alias_domains.into()); counts.insert( "role".into(), serde_json::to_value(s.role).unwrap_or_default(), ); counts.insert("username".into(), s.username.clone().into()); Ok(Json(serde_json::Value::Object(counts))) } pub async fn schema( State(state): State>, jar: CookieJar, ) -> AppResult> { let s = routes::session(&state, &jar).await?; s.require_global()?; let warnings = state.db.check_schema(&state.config).await?; Ok(Json( serde_json::json!({"ok":warnings.is_empty(),"warnings":warnings,"database_backend":state.config.database.backend}), )) } pub async fn features( State(state): State>, jar: CookieJar, ) -> AppResult> { let s = routes::session(&state, &jar).await?; let f = &state.config.features; let domain = s.username.rsplit_once('@').map(|(_, d)| d).unwrap_or(""); let permissions = serde_json::json!({ "edit_own_name": s.kind == crate::models::AccountKind::User && f.edit_mailbox_name.allows(domain), "edit_own_forwarding": s.kind == crate::models::AccountKind::User && f.edit_alias, "edit_own_vacation": s.kind == crate::models::AccountKind::User && f.vacation && f.vacation_control, "edit_mailbox_forwarding": s.role.is_admin() && if s.role.is_global() { f.alias_control } else { f.alias_control_admin }, "edit_vacation": s.role.is_admin() && f.vacation && (s.role.is_global() || f.vacation_control_admin), "manage_domains": s.role.is_global(), "manage_admins": s.role.is_global(), "edit_special_aliases": s.role.is_global() || f.special_alias_control, "sendmail": s.role.is_admin() && f.sendmail && (s.role.is_global() || f.sendmail_all_admins), "backup": s.role.is_global() && f.backup, }); Ok(Json( serde_json::json!({"permissions":permissions,"features":&state.config.features,"defaults":{"page_size":state.config.defaults.page_size,"quota_multiplier":state.config.defaults.quota_multiplier,"quota_level_med_pct":state.config.defaults.quota_level_med_pct,"quota_level_high_pct":state.config.defaults.quota_level_high_pct,"motd_user":state.config.defaults.motd_user,"motd_admin":state.config.defaults.motd_admin,"motd_superadmin":state.config.defaults.motd_superadmin,"vacation_choice_of_reply":state.config.defaults.vacation_choice_of_reply,"transport_options":state.config.defaults.transport_options,"transport_default":state.config.defaults.transport_default,"default_aliases":state.config.defaults.default_aliases.keys().collect::>(),"password_min_length":state.config.password.min_length,"password_min_letters":state.config.password.min_letters,"password_min_digits":state.config.password.min_digits,"mailbox_postpassword_hook":!state.config.hooks.mailbox_postpassword.is_empty()},"password":{"configured":state.config.password.scheme,"supported":passwords::supported_schemes()},"development_mock":state.config.database.dev_mock,"database_read_only":state.config.database.read_only,"dkim":{"default_backend":state.config.dkim.default_backend,"user_read_only":state.config.dkim.user_read_only},"rspamd_dkim":{"enabled":state.config.rspamd_dkim.enabled,"default_selector":state.config.rspamd_dkim.default_selector,"development_mock":state.config.rspamd_dkim.dev_mock},"sieve":{"enabled":state.config.sieve.enabled,"port":state.config.sieve.port,"tls_mode":state.config.sieve.tls_mode,"read_only":state.config.sieve.read_only},"version":env!("CARGO_PKG_VERSION")}), )) }