diff --git a/modules/mailu/client.ts b/modules/mailu/client.ts index 15d2203..340c70f 100644 --- a/modules/mailu/client.ts +++ b/modules/mailu/client.ts @@ -52,6 +52,13 @@ function meshConfig(file?: string): Record { 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); diff --git a/modules/mailu/module.json b/modules/mailu/module.json index d73c409..cd80df1 100644 --- a/modules/mailu/module.json +++ b/modules/mailu/module.json @@ -4,6 +4,20 @@ "capabilities": [ "container-runtime" ], + "requires": [ + "postgres-database" + ], + "contributes": { + "postgres-database": { + "name": "mailu" + } + }, + "binds": { + "postgres-database": "/var/lib/mailu/database.json" + }, + "secrets": { + "postgres-database": "/var/lib/mailu/database.secret" + }, "emits": [ "module.mailu.user.created", "module.mailu.user.deleted", @@ -43,13 +57,13 @@ "port": 7080, "protocol": "tcp", "from": "mesh", - "why": "the web interface, behind a proxy" + "why": "the web interface (admin, webmail, admin API), behind the route proxy" } ], "own-secrets": { "secret-key": "/var/lib/mailu/secret-key.secret", - "database": "/var/lib/mailu/database.secret", "admin": "/var/lib/mailu/admin.secret", + "api-token": "/var/lib/mailu/api-token.secret", "broker": "/var/lib/mesh/mailu/broker" }, "resources": [ @@ -65,6 +79,13 @@ "path": "/var/lib/mailu", "mode": "0700" }, + { + "id": "config-env", + "type": "file", + "path": "/var/lib/mailu/mailu.env", + "mode": "0644", + "content": "DOMAIN=novox.be\nHOSTNAMES=mail.novox.be\nPOSTMASTER=admin\nSITENAME=Novox\nWEBSITE=https://novox.be\nTLS_FLAVOR=cert\nSUBNET=192.168.203.0/24\nCOMPOSE_PROJECT_NAME=mailu\nHOST_ADMIN=mailu-admin\nHOST_ANTISPAM=mailu-antispam:11332\nHOST_IMAP=mailu-imap\nHOST_SMTP=mailu-smtp\nHOST_WEBMAIL=mailu-webmail\nHOST_WEBDAV=mailu-webdav:5232\nHOST_REDIS=mailu-redis\nHOST_FRONT=mailu-front\nREDIS_ADDRESS=mailu-redis\nANTIVIRUS=clamav\nWEBMAIL=roundcube\nWEBDAV=radicale\nFETCHMAIL_ENABLED=True\nFETCHMAIL_DELAY=600\nADMIN=true\nWEB_ADMIN=/admin\nWEB_WEBMAIL=/webmail\nWEBROOT_REDIRECT=/webmail\nWEBMAIL_ADDRESS=webmail\nAPI=true\nWEB_API=/api\nAUTH_RATELIMIT_IP=6000/hour\nAUTH_RATELIMIT_USER=1000/day\nCREDENTIAL_ROUNDS=12\nPASSWORD_SCHEME=PBKDF2\nDISABLE_STATISTICS=True\nMESSAGE_SIZE_LIMIT=50000000\nMESSAGE_RATELIMIT=200/day\nRECIPIENT_DELIMITER=+\nPOSTFIX_MYNETWORKS=127.0.0.0/8 [::1]/128\nRELAYNETS=\nRELAYHOST=\nREJECT_UNLISTED_RECIPIENT=\nDB_FLAVOR=postgresql\nINITIAL_ADMIN_ACCOUNT=admin\nINITIAL_ADMIN_DOMAIN=novox.be\nINITIAL_ADMIN_MODE=ifmissing\nSMTP_PORT=25\nSMTPS_PORT=465\nSUBMISSION_PORT=587\nPOP3_PORT=110\nPOP3S_PORT=995\nIMAP_PORT=143\nIMAPS_PORT=993\nHTTP_PORT=7080\nHTTPS_PORT=7443\nAUTOMX_PORT=4243\nAMX_SMTP_ADDRESS=mail.novox.be\nAMX_SMTP_PORT=587\nAMX_IMAP_ADDRESS=mail.novox.be\nAMX_IMAP_PORT=143\nAMX_MAIL_DOMAINS=novox.be\nDMARC_RUA=admin\nDMARC_RUF=admin\nLETSENCRYPT_SHORTCHAIN=True\nTZ=Etc/UTC\nLOG_LEVEL=INFO\nWELCOME=false\nREAL_IP_HEADER=X-Real-IP\nREAL_IP_FROM=\nCOMPRESSION=\nCOMPRESS_LEVEL=\nCOMPRESSION_LEVEL=\nBIND_ADDRESS4=127.0.0.1\nBIND_ADDRESS6=::1\nMAILU_VERSION=1.9\nDOCKER_ORG=mailu\nDOCKER_PREFIX=\n" + }, { "id": "secret-env", "type": "file", @@ -77,14 +98,14 @@ "type": "file", "path": "/var/lib/mailu/database.env", "mode": "0600", - "content": "POSTGRES_USER=mailu\nPOSTGRES_DB=mailu\nPOSTGRES_PASSWORD=${secret:database}\nDB_USER=mailu\nDB_NAME=mailu\nDB_PW=${secret:database}\n" + "content": "DB_FLAVOR=postgresql\nDB_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nDB_USER=${bound:postgres-database:as}\nDB_NAME=${bound:postgres-database:as}\nDB_PW=${secret:postgres-database}\n" }, { "id": "admin-env", "type": "file", "path": "/var/lib/mailu/admin.env", "mode": "0600", - "content": "INITIAL_ADMIN_PW=${secret:admin}\n" + "content": "INITIAL_ADMIN_PW=${secret:admin}\nAPI_TOKEN=${secret:api-token}\n" }, { "id": "data-certs", @@ -104,12 +125,6 @@ "path": "/services/mailu/data/dkim", "mode": "0700" }, - { - "id": "data-filter", - "type": "directory", - "path": "/services/mailu/data/filter", - "mode": "0700" - }, { "id": "data-mail", "type": "directory", @@ -122,6 +137,12 @@ "path": "/services/mailu/data/mailqueue", "mode": "0700" }, + { + "id": "data-filter", + "type": "directory", + "path": "/services/mailu/data/filter", + "mode": "0700" + }, { "id": "data-redis", "type": "directory", @@ -135,21 +156,45 @@ "mode": "0700" }, { - "id": "data-dovecot", + "id": "data-dav", "type": "directory", - "path": "/services/mailu/data/overrides/dovecot", + "path": "/services/mailu/data/dav", "mode": "0700" }, { - "id": "data-nginx", + "id": "data-fetchmail", + "type": "directory", + "path": "/services/mailu/data/data/fetchmail", + "mode": "0700" + }, + { + "id": "data-overrides-nginx", "type": "directory", "path": "/services/mailu/data/overrides/nginx", "mode": "0700" }, { - "id": "data-pgdata", + "id": "data-overrides-dovecot", "type": "directory", - "path": "/services/mailu/data/data/psql_admindb/pgdata", + "path": "/services/mailu/data/overrides/dovecot", + "mode": "0700" + }, + { + "id": "data-overrides-postfix", + "type": "directory", + "path": "/services/mailu/data/overrides/postfix", + "mode": "0700" + }, + { + "id": "data-overrides-rspamd", + "type": "directory", + "path": "/services/mailu/data/overrides/rspamd", + "mode": "0700" + }, + { + "id": "data-overrides-roundcube", + "type": "directory", + "path": "/services/mailu/data/overrides/roundcube", "mode": "0700" }, { @@ -164,6 +209,7 @@ "image": "ghcr.io/mailu/unbound@sha256:142aaad82ad1b0d5b59a5f1303778dba61a3e0a540f5d969c48862bcc99f6f5d", "network": "mailu", "env-file": [ + "/var/lib/mailu/mailu.env", "/var/lib/mailu/secret.env" ] }, @@ -177,22 +223,6 @@ "/services/mailu/data/redis:/data" ] }, - { - "id": "admindb", - "type": "container", - "name": "mailu-admindb", - "image": "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee", - "network": "mailu", - "env": { - "PGDATA": "/var/lib/postgresql/data/pgdata" - }, - "env-file": [ - "/var/lib/mailu/database.env" - ], - "volumes": [ - "/services/mailu/data/data/psql_admindb/pgdata:/var/lib/postgresql/data/pgdata" - ] - }, { "id": "admin", "type": "container", @@ -200,6 +230,7 @@ "image": "ghcr.io/mailu/admin@sha256:dcac20e9cbdad560faef9653b1b5ac0d9266f4098dc00f0e7f0d35f4e70ed8f1", "network": "mailu", "env-file": [ + "/var/lib/mailu/mailu.env", "/var/lib/mailu/secret.env", "/var/lib/mailu/database.env", "/var/lib/mailu/admin.env" @@ -216,6 +247,7 @@ "image": "ghcr.io/mailu/dovecot@sha256:46d18ba51032be8ebd6841aa49c1ef8762c729038c5fd86a081b5b884d478af9", "network": "mailu", "env-file": [ + "/var/lib/mailu/mailu.env", "/var/lib/mailu/secret.env" ], "volumes": [ @@ -230,10 +262,12 @@ "image": "ghcr.io/mailu/postfix@sha256:bbf882880f68849511710b35237a933f3fe80c4b28bf48ff20205dbd1f1433d7", "network": "mailu", "env-file": [ + "/var/lib/mailu/mailu.env", "/var/lib/mailu/secret.env" ], "volumes": [ - "/services/mailu/data/mailqueue:/queue" + "/services/mailu/data/mailqueue:/queue", + "/services/mailu/data/overrides/postfix:/overrides:ro" ] }, { @@ -243,23 +277,69 @@ "image": "ghcr.io/mailu/rspamd@sha256:e87ab93dd252cc69499caa5317dd10d445fd4291a7ecf6bca09793c7d475a0c8", "network": "mailu", "env-file": [ + "/var/lib/mailu/mailu.env", "/var/lib/mailu/secret.env" ], "volumes": [ - "/services/mailu/data/filter:/var/lib/rspamd" + "/services/mailu/data/filter:/var/lib/rspamd", + "/services/mailu/data/overrides/rspamd:/etc/rspamd/override.d:ro" + ] + }, + { + "id": "antivirus", + "type": "container", + "name": "mailu-antivirus", + "image": "ghcr.io/mailu/clamav@sha256:01d30483e4a8a20a54566addb1f9b00ebb51e8a103f9226602379c412cf5fb62", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/mailu.env", + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/filter:/data" ] }, { "id": "webmail", "type": "container", "name": "mailu-webmail", - "image": "ghcr.io/mailu/webmail@sha256:076b720fc766e58a97321cdb700e887c2008d6d323685fe59f323088333059dc", + "image": "ghcr.io/mailu/roundcube@sha256:076b720fc766e58a97321cdb700e887c2008d6d323685fe59f323088333059dc", "network": "mailu", "env-file": [ + "/var/lib/mailu/mailu.env", "/var/lib/mailu/secret.env" ], "volumes": [ - "/services/mailu/data/webmail:/data" + "/services/mailu/data/webmail:/data", + "/services/mailu/data/overrides/roundcube:/overrides:ro" + ] + }, + { + "id": "webdav", + "type": "container", + "name": "mailu-webdav", + "image": "ghcr.io/mailu/radicale@sha256:e13cbad3791c0a6841b5d387e57e49a117808dcef87b8c9969f671ae9c3b67c0", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/mailu.env", + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/dav:/data" + ] + }, + { + "id": "fetchmail", + "type": "container", + "name": "mailu-fetchmail", + "image": "ghcr.io/mailu/fetchmail@sha256:7dcd1392882925d612ab2d0230d437f0c660989d572283c48b0d0f2d491adce7", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/mailu.env", + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/data/fetchmail:/data" ] }, { @@ -269,6 +349,7 @@ "image": "ghcr.io/mailu/nginx@sha256:09f28ab6d36367fcacc7994f7021f132ac845bdc05f04bf80906102d11aaa057", "network": "mailu", "env-file": [ + "/var/lib/mailu/mailu.env", "/var/lib/mailu/secret.env" ], "ports": [ @@ -296,15 +377,18 @@ "type": "container", "name": "mesh-mailu", "image": "mesh-runtime-mailu@sha256:0000000000000000000000000000000000000000000000000000000000000000", - "network": "host", + "network": "mailu", "volumes": [ "/var/lib/mesh/mailu/broker:/run/secrets/broker:ro", + "/var/lib/mailu/api-token.secret:/run/secrets/api-token:ro", "/var/lib/mesh/mailu/config.json:/run/config/config.json:ro", "/var/run/docker.sock:/var/run/docker.sock" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", - "MESH_MAILU_URL": "http://127.0.0.1:80", + "MESH_MAILU_URL": "http://mailu-admin/api/v1", + "MESH_MAILU_API_KEY_FILE": "/run/secrets/api-token", + "MESH_MAILU_IMAP_CONTAINER": "mailu-imap", "MESH_MAILU_CONFIG_FILE": "/run/config/config.json" }, "restart-on": [