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>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// settings.js — editierbare Admin-Einstellungen (Domains, Webmail-Domain, Brand)
|
||||
//
|
||||
// Liegt als JSON in der DMS-Config (persistent). Beim ersten Start aus den
|
||||
// ENV-Variablen (Deploy-Dialog) geseedet, danach in der Admin-UI editierbar.
|
||||
// ---------------------------------------------------------------------------
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const CONFIG_DIR = process.env.CONFIG_DIR || '/config';
|
||||
const FILE = path.join(CONFIG_DIR, 'admin-settings.json');
|
||||
|
||||
const isDomain = (s) => /^[^@\s]+\.[^@\s]+$/.test(s);
|
||||
const envDomains = () =>
|
||||
(process.env.MAIL_DOMAINS || process.env.MAIL_DOMAIN || '').split(/[\s,]+/).filter(Boolean);
|
||||
|
||||
function seed() {
|
||||
const domains = envDomains();
|
||||
const primary = process.env.MAIL_DOMAIN || domains[0] || 'example.com';
|
||||
return {
|
||||
brand: process.env.BRAND || primary,
|
||||
fqdn: process.env.MAIL_FQDN || `mail.${primary}`,
|
||||
primaryDomain: primary,
|
||||
domains: domains.length ? domains : [primary],
|
||||
webmailFqdn: process.env.WEBMAIL_FQDN || `mail.${primary}`,
|
||||
adminFqdn: process.env.ADMIN_FQDN || `admin.${primary}`,
|
||||
};
|
||||
}
|
||||
|
||||
let chain = Promise.resolve();
|
||||
const withLock = (fn) => { const r = chain.then(fn, fn); chain = r.catch(() => {}); return r; };
|
||||
const save = (s) => fs.writeFile(FILE, JSON.stringify(s, null, 2) + '\n', 'utf8');
|
||||
|
||||
export async function readSettings() {
|
||||
try {
|
||||
return { id: 'settings', ...JSON.parse(await fs.readFile(FILE, 'utf8')) };
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') { const s = seed(); await save(s); return { id: 'settings', ...s }; }
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeSettings(patch) {
|
||||
return withLock(async () => {
|
||||
const { id, ...cur } = await readSettings();
|
||||
const next = { ...cur };
|
||||
// nur erlaubte Felder
|
||||
for (const k of ['brand', 'fqdn', 'primaryDomain', 'webmailFqdn', 'adminFqdn']) {
|
||||
if (typeof patch[k] === 'string' && patch[k].trim()) next[k] = patch[k].trim();
|
||||
}
|
||||
if (Array.isArray(patch.domains)) {
|
||||
next.domains = [...new Set(patch.domains.map((d) => String(d).trim()).filter(isDomain))];
|
||||
}
|
||||
await save(next);
|
||||
return { id: 'settings', ...next };
|
||||
});
|
||||
}
|
||||
|
||||
export function addDomain(domain) {
|
||||
return withLock(async () => {
|
||||
domain = String(domain || '').trim().toLowerCase();
|
||||
if (!isDomain(domain)) { const e = new Error('Ungültige Domain.'); e.status = 400; throw e; }
|
||||
const { id, ...cur } = await readSettings();
|
||||
if (!cur.domains.includes(domain)) cur.domains.push(domain);
|
||||
await save(cur);
|
||||
return { id: 'settings', ...cur };
|
||||
});
|
||||
}
|
||||
|
||||
export function removeDomain(domain) {
|
||||
return withLock(async () => {
|
||||
const { id, ...cur } = await readSettings();
|
||||
cur.domains = cur.domains.filter((d) => d !== domain);
|
||||
await save(cur);
|
||||
return { id: 'settings', ...cur };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user