// --------------------------------------------------------------------------- // mailserver.js — abgesicherte Bridge zum docker-mailserver Container // // Läuft NUR über einen docker-socket-proxy (nur exec freigegeben) und führt // ausschließlich WHITELISTED Kommandos im Mailserver-Container aus. Argumente // werden als argv-Array übergeben (keine Shell -> keine Injection). // --------------------------------------------------------------------------- import Docker from 'dockerode'; import { httpErr } from './store.js'; const MAILSERVER = process.env.MAILSERVER_CONTAINER || 'mailserver'; // DOCKER_PROXY = "host:port" (docker-socket-proxy), z.B. socket-proxy:2375 const [proxyHost, proxyPort] = (process.env.DOCKER_PROXY || 'socket-proxy:2375').split(':'); const docker = new Docker({ host: proxyHost, port: Number(proxyPort) || 2375 }); const isEmail = (s) => /^@?[^@\s]*@?[^@\s]+\.[^@\s]+$/.test(s); // erlaubt auch @domain const isDomain = (s) => /^[^@\s]+\.[^@\s]+$/.test(s); // --- whitelisted exec: nimmt ein argv-Array, gibt stdout zurück ------------ async function exec(cmd) { const container = docker.getContainer(MAILSERVER); const ex = await container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }); const stream = await ex.start({ hijack: true, stdin: false }); const out = []; const err = []; await new Promise((resolve, reject) => { container.modem.demuxStream( stream, { write: (d) => out.push(d) }, { write: (d) => err.push(d) }, ); stream.on('end', resolve); stream.on('error', reject); }); const info = await ex.inspect(); if (info.ExitCode && info.ExitCode !== 0) { throw httpErr(502, `Mailserver-Kommando fehlgeschlagen: ${Buffer.concat(err).toString() || 'Exit ' + info.ExitCode}`); } return Buffer.concat(out).toString('utf8'); } // =========================================================================== // QUOTA-Auslastung (doveadm quota get -A) // =========================================================================== export async function quotaUsage() { const raw = await exec(['doveadm', 'quota', 'get', '-A']); const rows = {}; for (const line of raw.split('\n')) { // Username ... STORAGE <%> const m = line.match(/^(\S+)\s+User quota\s+STORAGE\s+(\d+)\s+(\d+|-)\s+(\d+|-)/); if (m) { const [, user, valueKB, limitKB, pct] = m; rows[user] = { id: user, user, usedBytes: Number(valueKB) * 1024, limitBytes: limitKB === '-' ? null : Number(limitKB) * 1024, percent: pct === '-' ? null : Number(pct), }; } } return Object.values(rows); } // =========================================================================== // Mail-Queue (postqueue -p) // =========================================================================== export async function queue() { const raw = await exec(['postqueue', '-p']); const empty = /Mail queue is empty/.test(raw); // letzte Zeile: "-- N Kbytes in M Requests." const m = raw.match(/in (\d+) Request/i); return { empty, count: empty ? 0 : (m ? Number(m[1]) : null), raw: raw.trim() }; } // =========================================================================== // Aktive Logins (doveadm who) // =========================================================================== export async function who() { const raw = await exec(['doveadm', 'who']); const lines = raw.trim().split('\n').slice(1).filter(Boolean); // ohne Header return { sessions: lines.map((l) => { const [username, count, proto] = l.split(/\s+/); return { username, count: Number(count) || count, proto }; }), raw: raw.trim(), }; } // =========================================================================== // DKIM-Schlüssel pro Domain erzeugen (setup config dkim ...) // =========================================================================== export async function generateDkim(domain) { if (!isDomain(domain)) throw httpErr(400, 'Ungültige Domain.'); await exec(['setup', 'config', 'dkim', 'keysize', '2048', 'domain', domain]); return { ok: true, domain }; } // =========================================================================== // Übersicht // =========================================================================== export async function overview() { const [q, qu, w] = await Promise.all([queue(), quotaUsage(), who()]); return { queue: q, quota: qu, who: w }; }