This commit is contained in:
2026-04-26 16:19:09 -05:00
parent cb4e28e7b1
commit 8178cd0a08
3 changed files with 184 additions and 44 deletions

View File

@@ -19,17 +19,26 @@ export interface DmsQuota {
function parseAccounts(output: string): DmsAccount[] {
const accounts: DmsAccount[] = [];
for (const line of output.split('\n')) {
const match = line.match(/([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})/i);
if (!match) continue;
const email = normalizeEmail(match[1]);
accounts.push({ email, localPart: localPartFromEmail(email), domain: domainFromEmail(email) });
accounts.push({
email,
localPart: localPartFromEmail(email),
domain: domainFromEmail(email),
});
}
return [...new Map(accounts.map((a) => [a.email, a])).values()].sort((a, b) => a.email.localeCompare(b.email));
return [...new Map(accounts.map((a) => [a.email, a])).values()]
.sort((a, b) => a.email.localeCompare(b.email));
}
function parseQuotaNumber(value: string | undefined): number | null {
if (!value || value === '-' || value.trim() === '') return null;
const parsed = Number.parseInt(value.trim(), 10);
return Number.isFinite(parsed) ? parsed : null;
}
@@ -47,6 +56,7 @@ function parseDoveadmQuota(output: string): DmsQuota {
const parts = trimmed.split(/\s+/);
const typeIndex = parts.findIndex((p) => p === 'STORAGE' || p === 'MESSAGE');
if (typeIndex < 0) continue;
const type = parts[typeIndex];
@@ -67,11 +77,13 @@ function parseDoveadmQuota(output: string): DmsQuota {
}
const usedBytes = Math.max(0, usedStorageKb ?? 0) * 1024;
const quotaBytes = storageLimitKb && storageLimitKb > 0 ? storageLimitKb * 1024 : null;
const quotaBytes = storageLimitKb && storageLimitKb > 0
? storageLimitKb * 1024
: null;
// Dovecot's % column is integer-rounded. For the UI we calculate a more useful
// value ourselves, so small accounts do not always show exactly 0%.
const calculatedPercent = quotaBytes && quotaBytes > 0 ? (usedBytes / quotaBytes) * 100 : null;
const calculatedPercent = quotaBytes && quotaBytes > 0
? (usedBytes / quotaBytes) * 100
: null;
return {
usedBytes,
@@ -83,48 +95,113 @@ function parseDoveadmQuota(output: string): DmsQuota {
}
export class DmsService {
async assertDockerAccess(): Promise<void> {
await run('docker', ['ps', '--format', '{{.Names}}'], 30000);
}
async listAccounts(): Promise<DmsAccount[]> {
const { stdout } = await run('docker', ['exec', config.dmsContainer, 'setup', 'email', 'list']);
return parseAccounts(stdout);
console.log(`[dms] listing accounts from container: ${config.dmsContainer}`);
const { stdout } = await run(
'docker',
['exec', config.dmsContainer, 'setup', 'email', 'list'],
60000,
);
const accounts = parseAccounts(stdout);
console.log(`[dms] found ${accounts.length} account(s)`);
return accounts;
}
async addMailbox(email: string, password: string): Promise<void> {
const normalized = normalizeEmail(email);
console.log(`[dms] creating mailbox: ${normalized}`);
await this.assertDockerAccess();
if (config.manageMailUserScript && existsSync(config.manageMailUserScript)) {
console.log(`[dms] using manage script: ${config.manageMailUserScript}`);
await run(config.manageMailUserScript, ['add', normalized, password]);
console.log(`[dms] mailbox created through manage script: ${normalized}`);
return;
}
await run('docker', ['exec', config.dmsContainer, 'setup', 'email', 'add', normalized, password]);
console.log(`[dms] using docker-mailserver setup command`);
await run(
'docker',
['exec', config.dmsContainer, 'setup', 'email', 'add', normalized, password],
120000,
);
console.log(`[dms] mailbox created: ${normalized}`);
}
async deleteMailbox(email: string): Promise<void> {
const normalized = normalizeEmail(email);
console.log(`[dms] deleting mailbox: ${normalized}`);
await this.assertDockerAccess();
if (config.manageMailUserScript && existsSync(config.manageMailUserScript)) {
console.log(`[dms] using manage script: ${config.manageMailUserScript}`);
await run(config.manageMailUserScript, ['del', normalized]);
console.log(`[dms] mailbox deleted through manage script: ${normalized}`);
return;
}
await run('docker', ['exec', config.dmsContainer, 'setup', 'email', 'del', normalized]);
await run(
'docker',
['exec', config.dmsContainer, 'setup', 'email', 'del', normalized],
120000,
);
console.log(`[dms] mailbox deleted: ${normalized}`);
}
async updatePassword(email: string, password: string): Promise<void> {
const normalized = normalizeEmail(email);
// docker-mailserver supports setup email update <email> <password> in current versions.
await run('docker', ['exec', config.dmsContainer, 'setup', 'email', 'update', normalized, password]);
console.log(`[dms] updating password for mailbox: ${normalized}`);
await this.assertDockerAccess();
await run(
'docker',
['exec', config.dmsContainer, 'setup', 'email', 'update', normalized, password],
120000,
);
console.log(`[dms] password updated for mailbox: ${normalized}`);
}
async syncSesDomain(domain: string): Promise<void> {
const normalizedDomain = domain.toLowerCase();
if (config.manageMailUserScript && existsSync(config.manageMailUserScript)) {
await run(config.manageMailUserScript, ['sync', domain.toLowerCase()]);
console.log(`[dms] syncing SES for domain through manage script: ${normalizedDomain}`);
await run(config.manageMailUserScript, ['sync', normalizedDomain], 120000);
return;
}
console.log(`[dms] no manage script configured, skipping SES sync for ${normalizedDomain}`);
}
async getMailboxQuota(email: string): Promise<DmsQuota> {
const normalized = normalizeEmail(email);
console.log(`[dms] reading quota for mailbox: ${normalized}`);
const { stdout } = await run(
'docker',
['exec', config.dmsContainer, 'doveadm', 'quota', 'get', '-u', normalized],
60000,
);
return parseDoveadmQuota(stdout);
}
}