234 lines
6.7 KiB
TypeScript
234 lines
6.7 KiB
TypeScript
import { existsSync } from 'node:fs';
|
|
import { config } from '../config.js';
|
|
import { run } from '../utils/shell.js';
|
|
import { domainFromEmail, localPartFromEmail, normalizeEmail } from '../utils/email.js';
|
|
|
|
export interface DmsAccount {
|
|
email: string;
|
|
localPart: string;
|
|
domain: string;
|
|
}
|
|
|
|
export interface DmsQuota {
|
|
usedBytes: number;
|
|
quotaBytes: number | null;
|
|
quotaPercent: number | null;
|
|
messageCount: number | null;
|
|
messageLimit: number | null;
|
|
}
|
|
|
|
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),
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function parseDoveadmQuota(output: string): DmsQuota {
|
|
let usedStorageKb: number | null = null;
|
|
let storageLimitKb: number | null = null;
|
|
let storagePercent: number | null = null;
|
|
let messageCount: number | null = null;
|
|
let messageLimit: number | null = null;
|
|
|
|
for (const line of output.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.toLowerCase().startsWith('quota name')) continue;
|
|
|
|
const parts = trimmed.split(/\s+/);
|
|
const typeIndex = parts.findIndex((p) => p === 'STORAGE' || p === 'MESSAGE');
|
|
|
|
if (typeIndex < 0) continue;
|
|
|
|
const type = parts[typeIndex];
|
|
const value = parseQuotaNumber(parts[typeIndex + 1]);
|
|
const limit = parseQuotaNumber(parts[typeIndex + 2]);
|
|
const percent = parseQuotaNumber(parts[typeIndex + 3]);
|
|
|
|
if (type === 'STORAGE') {
|
|
usedStorageKb = value;
|
|
storageLimitKb = limit;
|
|
storagePercent = percent;
|
|
}
|
|
|
|
if (type === 'MESSAGE') {
|
|
messageCount = value;
|
|
messageLimit = limit;
|
|
}
|
|
}
|
|
|
|
const usedBytes = Math.max(0, usedStorageKb ?? 0) * 1024;
|
|
const quotaBytes = storageLimitKb && storageLimitKb > 0
|
|
? storageLimitKb * 1024
|
|
: null;
|
|
|
|
const calculatedPercent = quotaBytes && quotaBytes > 0
|
|
? (usedBytes / quotaBytes) * 100
|
|
: null;
|
|
|
|
return {
|
|
usedBytes,
|
|
quotaBytes,
|
|
quotaPercent: calculatedPercent ?? storagePercent,
|
|
messageCount,
|
|
messageLimit,
|
|
};
|
|
}
|
|
|
|
export class DmsService {
|
|
async assertDockerAccess(): Promise<void> {
|
|
await run('docker', ['ps', '--format', '{{.Names}}'], 30000);
|
|
}
|
|
|
|
async listAccounts(): Promise<DmsAccount[]> {
|
|
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;
|
|
}
|
|
|
|
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],
|
|
120000,
|
|
);
|
|
|
|
console.log(`[dms] mailbox deleted: ${normalized}`);
|
|
}
|
|
|
|
async updatePassword(email: string, password: string): Promise<void> {
|
|
const normalized = normalizeEmail(email);
|
|
|
|
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}`);
|
|
}
|
|
|
|
/**
|
|
* Set the storage quota for a mailbox via the docker-mailserver
|
|
* `setup quota set` subcommand. quotaGb must be a positive integer.
|
|
* Equivalent to: docker exec mailserver setup quota set user@example.com 35G
|
|
*/
|
|
async setQuota(email: string, quotaGb: number): Promise<void> {
|
|
const normalized = normalizeEmail(email);
|
|
const gb = Math.floor(Number(quotaGb));
|
|
|
|
if (!Number.isFinite(gb) || gb <= 0 || gb > 1024) {
|
|
throw new Error(`Invalid quota size: ${quotaGb}`);
|
|
}
|
|
|
|
console.log(`[dms] setting quota for ${normalized} to ${gb}G`);
|
|
|
|
await this.assertDockerAccess();
|
|
|
|
await run(
|
|
'docker',
|
|
['exec', config.dmsContainer, 'setup', 'quota', 'set', normalized, `${gb}G`],
|
|
120000,
|
|
);
|
|
|
|
console.log(`[dms] quota set for ${normalized}: ${gb}G`);
|
|
}
|
|
|
|
async syncSesDomain(domain: string): Promise<void> {
|
|
const normalizedDomain = domain.toLowerCase();
|
|
|
|
if (config.manageMailUserScript && existsSync(config.manageMailUserScript)) {
|
|
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);
|
|
}
|
|
}
|