first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-03 16:52:28 +01:00
commit 2eb8aac027
91 files changed
+20139

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
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<Arc<AppState>>) -> AppResult<Json<Health>> {
state.db.ping().await?;
Ok(Json(Health {
status: "ok",
version: env!("CARGO_PKG_VERSION"),
}))
}
pub async fn overview(
State(state): State<Arc<AppState>>,
jar: CookieJar,
) -> AppResult<Json<serde_json::Value>> {
let s = routes::session(&state, &jar)?;
let domains = count(&state, Table::Domain, "domain<>'ALL'").await?;
let mailboxes = count_scoped(&state, Table::Mailbox, "domain", &s).await?;
let aliases = count_scoped(&state, Table::Alias, "domain", &s).await?;
Ok(Json(
serde_json::json!({"domains":if s.role.is_global(){domains}else{s.allowed_domains.iter().filter(|d|d.as_str()!="ALL").count() as i64},"mailboxes":mailboxes,"aliases":aliases,"role":s.role,"username":s.username,"features":&state.config.features}),
))
}
pub async fn schema(
State(state): State<Arc<AppState>>,
jar: CookieJar,
) -> AppResult<Json<serde_json::Value>> {
let s = routes::session(&state, &jar)?;
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<Arc<AppState>>,
jar: CookieJar,
) -> AppResult<Json<serde_json::Value>> {
let _ = routes::session(&state, &jar)?;
Ok(Json(
serde_json::json!({"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},"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")}),
))
}
async fn count(state: &AppState, table: Table, where_sql: &str) -> AppResult<i64> {
let t = state.db.table(table);
let r = sqlx::query(&format!("SELECT COUNT(*) AS n FROM {t} WHERE {where_sql}"))
.fetch_one(state.db.pool())
.await?;
Ok(row_i64(&r, "n"))
}
async fn count_scoped(
state: &AppState,
table: Table,
col: &str,
s: &crate::auth::Session,
) -> AppResult<i64> {
let t = state.db.table(table);
if s.role.is_global() {
let r = sqlx::query(&format!("SELECT COUNT(*) AS n FROM {t}"))
.fetch_one(state.db.pool())
.await?;
return Ok(row_i64(&r, "n"));
}
if !s.role.is_admin() {
let q = state.db.q(&format!(
"SELECT COUNT(*) AS n FROM {t} WHERE {}=?",
if matches!(table, Table::Mailbox) {
"username"
} else {
"address"
}
));
let r = sqlx::query(&q)
.bind(&s.username)
.fetch_one(state.db.pool())
.await?;
return Ok(row_i64(&r, "n"));
}
let ds: Vec<_> = s
.allowed_domains
.iter()
.filter(|d| d.as_str() != "ALL")
.collect();
if ds.is_empty() {
return Ok(0);
}
let q = state.db.q(&format!(
"SELECT COUNT(*) AS n FROM {t} WHERE {col} IN ({})",
vec!["?"; ds.len()].join(",")
));
let mut x = sqlx::query(&q);
for d in ds {
x = x.bind(d.as_str());
}
let r = x.fetch_one(state.db.pool()).await?;
Ok(row_i64(&r, "n"))
}