Files
DOCKERMAILSERVER-LXC/stack/api/lib/mailserver.js
T
karim 1d3818e725 docker-mailserver LXC für Proxmox: Stack + Admin-UI + Webmail + Hardening
- dms-lxc.sh: Proxmox-Host-Installer (unprivilegierter LXC, Debian 13, Docker),
  curl-Self-Download, Multi-Domain-DKIM, SnappyMail-Provisionierung, PVE-Firewall
- Stack: docker-mailserver, Node-Admin-API (Supabase-Auth), React-Admin-UI
  (OPENBUREAU-Look), SnappyMail (Shibui-Theme), Rspamd-Web-UI, docker-socket-proxy
- Admin: Postfächer/Aliase/Catch-all/Quota, editierbare Domains+Settings,
  Server (Quota/Queue über abgesicherte Bridge), Status & DNS
- Hardening: no-new-privileges, Whitelisted exec-Bridge, Rspamd-Passwort,
  .env chmod 600, PVE-CT-Firewall, generisch/teilbar (keine festen Domains)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 02:26:28 +02:00

107 lines
4.4 KiB
JavaScript

// ---------------------------------------------------------------------------
// 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 <value KB> <limit KB|-> <%>
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 };
}