mailu: full multi-container stack from the live novox deployment

Rebuild the mailu manifest from the running production deployment as the
source of truth, and finish wiring its tool runtime.

- Full 11-container topology: front, smtp, imap, admin, antispam, antivirus,
  webmail, webdav, fetchmail, resolver, redis — real ghcr.io/mailu images
  pinned by digest at tag 1.9 (clamav/radicale/fetchmail digests newly fetched).
- Admin DB now consumes the mesh postgres-database provider (requires +
  contributes + binds + secrets, DB_* templated from ${bound}/${secret}),
  replacing the bundled postgres:13 admindb the live stack still runs.
- Full config env from the live containers as a plain env-file; SECRET_KEY,
  the admin API token and the initial-admin password become own-secrets;
  DB password comes from the provider secret. No secret values hardcoded.
- Enable the Mailu admin REST API (API=true, WEB_API, API_TOKEN own-secret) so
  the ported tools can reach it — the live deployment runs this API OFF.
- mesh-mailu tool runtime on the mailu network: admin API over the module
  network, token from the mounted own-secret, docker.sock for the doveadm mail
  reads, broker + mergeable config with restart-on.
- client.ts fromEnv reads the API token from its mounted own-secret file
  (MESH_MAILU_API_KEY_FILE), matching the cloudflare-dns/umami pattern.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-09 01:04:55 +02:00
parent 8f99b5bd42
commit b3309a0ce9
2 changed files with 134 additions and 41 deletions
+13 -4
View File
@@ -52,6 +52,13 @@ function meshConfig(file?: string): Record<string, string> {
catch { return {}; }
}
/** Read a secret from the file the mesh mounted it at (an own-secret), if the pointing env is set. */
function readSecret(path: string | undefined): string | undefined {
if (!path) return undefined;
try { return readFileSync(path, "utf8").trim() || undefined; }
catch { return undefined; }
}
export class MailuClient {
readonly baseUrl: string;
@@ -66,15 +73,17 @@ export class MailuClient {
/**
* Build from the module's resolved environment. MESH_MAILU_URL points at the admin API (e.g. the
* admin container's /api/v1), MESH_MAILU_API_KEY authenticates against it. Both are required — a
* client with neither would only fail later, one call at a time, so it fails here instead.
* admin container's /api/v1); the token authenticates against it. The token is an own-secret,
* so it arrives as a file the mesh mounts (MESH_MAILU_API_KEY_FILE) — the deployed path — with a
* bare MESH_MAILU_API_KEY honoured only as a fallback for a hand-run instance. URL and token are
* both required: a client with neither would only fail later, one call at a time, so it fails here.
*/
static fromEnv(env: NodeJS.ProcessEnv = process.env): MailuClient {
const cfg = meshConfig(env.MESH_MAILU_CONFIG_FILE);
const url = cfg.url ?? env.MESH_MAILU_URL;
const apiKey = cfg.apiKey ?? env.MESH_MAILU_API_KEY;
const apiKey = cfg.apiKey ?? readSecret(env.MESH_MAILU_API_KEY_FILE) ?? env.MESH_MAILU_API_KEY;
if (!url || !apiKey) {
throw new Error("Mailu is not configured — set MESH_MAILU_URL and MESH_MAILU_API_KEY");
throw new Error("Mailu is not configured — set MESH_MAILU_URL and the API token own-secret");
}
const imapContainer = cfg.container ?? env.MESH_MAILU_IMAP_CONTAINER ?? "mailu-imap";
return new MailuClient(url, apiKey, imapContainer);