Files
dcrouter/ts/email/classes.email-settings.manager.ts
T

222 lines
7.5 KiB
TypeScript

import type { IUnifiedEmailServerOptions } from '@push.rocks/smartmta';
import { EmailServerSettingsDoc } from '../db/index.js';
import type { IDcRouterOptions } from '../classes.dcrouter.js';
import type {
IEmailPortConfig,
IEmailServerSettings,
TEmailServerSettingsUpdate,
} from '../../ts_interfaces/data/email-settings.js';
const defaultEmailPorts = [25, 587, 465];
function clonePlain<T>(value: T | undefined): T | undefined {
if (value === undefined) return undefined;
return JSON.parse(JSON.stringify(value)) as T;
}
function hasOwn(objectArg: object, keyArg: string): boolean {
return Object.prototype.hasOwnProperty.call(objectArg, keyArg);
}
export class EmailSettingsManager {
private cachedEmailConfig?: IUnifiedEmailServerOptions;
private cachedEmailPortConfig?: IEmailPortConfig;
private enabled = false;
private updatedAt = 0;
private updatedBy = 'default';
constructor(private options: IDcRouterOptions) {}
public async start(): Promise<void> {
let doc = await EmailServerSettingsDoc.load();
if (!doc) {
doc = new EmailServerSettingsDoc();
doc.settingsId = 'email-server-settings';
doc.enabled = false;
doc.updatedAt = Date.now();
doc.updatedBy = 'default';
await doc.save();
}
this.loadFromDoc(doc);
this.applyToRuntimeOptions();
}
public async stop(): Promise<void> {
this.cachedEmailConfig = undefined;
this.cachedEmailPortConfig = undefined;
this.enabled = false;
}
public isEnabled(): boolean {
return this.enabled && Boolean(this.cachedEmailConfig);
}
public getEmailConfig(): IUnifiedEmailServerOptions | undefined {
return this.isEnabled() ? clonePlain(this.cachedEmailConfig) : undefined;
}
public getEmailPortConfig(): IEmailPortConfig | undefined {
return this.isEnabled() ? clonePlain(this.cachedEmailPortConfig) : undefined;
}
public getPublicSettings(): IEmailServerSettings {
const emailConfig = this.cachedEmailConfig;
const emailPortConfig = this.cachedEmailPortConfig;
return {
enabled: this.isEnabled(),
hostname: emailConfig?.hostname || null,
ports: [...(emailConfig?.ports || [])],
portMapping: emailPortConfig?.portMapping ? { ...emailPortConfig.portMapping } : null,
receivedEmailsPath: emailPortConfig?.receivedEmailsPath || null,
maxMessageSize: emailConfig?.maxMessageSize ?? null,
domainCount: emailConfig?.domains?.length || 0,
routeCount: emailConfig?.routes?.length || 0,
authUserCount: emailConfig?.auth?.users?.length || 0,
updatedAt: this.updatedAt,
updatedBy: this.updatedBy,
};
}
public async updateSettings(
updates: TEmailServerSettingsUpdate,
updatedBy: string,
): Promise<IEmailServerSettings> {
let doc = await EmailServerSettingsDoc.load();
if (!doc) {
doc = new EmailServerSettingsDoc();
doc.settingsId = 'email-server-settings';
}
const nextEnabled = hasOwn(updates, 'enabled') ? Boolean(updates.enabled) : doc.enabled;
const nextEmailConfig = this.patchEmailConfig(doc.emailConfig, updates, nextEnabled);
const nextEmailPortConfig = this.patchEmailPortConfig(doc.emailPortConfig, updates);
doc.enabled = nextEnabled;
doc.emailConfig = nextEmailConfig;
doc.emailPortConfig = nextEmailPortConfig;
doc.updatedAt = Date.now();
doc.updatedBy = updatedBy;
await doc.save();
this.loadFromDoc(doc);
this.applyToRuntimeOptions();
return this.getPublicSettings();
}
private loadFromDoc(doc: EmailServerSettingsDoc): void {
this.enabled = doc.enabled;
this.cachedEmailConfig = clonePlain(doc.emailConfig);
this.cachedEmailPortConfig = clonePlain(doc.emailPortConfig);
this.updatedAt = doc.updatedAt;
this.updatedBy = doc.updatedBy;
}
private applyToRuntimeOptions(): void {
this.options.emailConfig = this.getEmailConfig();
this.options.emailPortConfig = this.getEmailPortConfig();
}
private patchEmailConfig(
existingConfig: IUnifiedEmailServerOptions | undefined,
updates: TEmailServerSettingsUpdate,
nextEnabled: boolean,
): IUnifiedEmailServerOptions | undefined {
const nextConfig: IUnifiedEmailServerOptions | undefined = clonePlain(existingConfig) || (nextEnabled ? {
hostname: 'localhost',
ports: [...defaultEmailPorts],
domains: [],
routes: [],
} : undefined);
if (!nextConfig) return undefined;
if (hasOwn(updates, 'hostname')) {
const hostname = updates.hostname?.trim() || '';
if (nextEnabled && !hostname) {
throw new Error('Email hostname is required when email is enabled');
}
nextConfig.hostname = hostname || nextConfig.hostname;
}
if (hasOwn(updates, 'ports')) {
nextConfig.ports = this.normalizePorts(updates.ports || []);
}
if (hasOwn(updates, 'maxMessageSize')) {
if (updates.maxMessageSize === null || updates.maxMessageSize === undefined) {
delete nextConfig.maxMessageSize;
} else {
const maxMessageSize = Number(updates.maxMessageSize);
if (!Number.isInteger(maxMessageSize) || maxMessageSize <= 0) {
throw new Error('maxMessageSize must be a positive integer');
}
nextConfig.maxMessageSize = maxMessageSize;
}
}
if (nextEnabled) {
if (!nextConfig.hostname?.trim()) {
throw new Error('Email hostname is required when email is enabled');
}
nextConfig.ports = this.normalizePorts(nextConfig.ports || []);
}
nextConfig.domains = nextConfig.domains || [];
nextConfig.routes = nextConfig.routes || [];
return nextConfig;
}
private patchEmailPortConfig(
existingPortConfig: IEmailPortConfig | undefined,
updates: TEmailServerSettingsUpdate,
): IEmailPortConfig | undefined {
const nextPortConfig: IEmailPortConfig = clonePlain(existingPortConfig) || {};
if (hasOwn(updates, 'portMapping')) {
if (updates.portMapping === null) {
delete nextPortConfig.portMapping;
} else {
nextPortConfig.portMapping = this.normalizePortMapping(updates.portMapping || {});
}
}
if (hasOwn(updates, 'receivedEmailsPath')) {
const receivedEmailsPath = updates.receivedEmailsPath?.trim() || '';
if (receivedEmailsPath) {
nextPortConfig.receivedEmailsPath = receivedEmailsPath;
} else {
delete nextPortConfig.receivedEmailsPath;
}
}
return Object.keys(nextPortConfig).length > 0 ? nextPortConfig : undefined;
}
private normalizePorts(ports: number[]): number[] {
const normalized = [...new Set(ports.map((port) => Number(port)))];
if (normalized.length === 0) {
throw new Error('At least one email port is required when email is enabled');
}
for (const port of normalized) {
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`Invalid email port: ${port}`);
}
}
return normalized.sort((a, b) => a - b);
}
private normalizePortMapping(portMapping: Record<number, number>): Record<number, number> {
const normalized: Record<number, number> = {};
for (const [externalPortString, internalPortValue] of Object.entries(portMapping)) {
const externalPort = Number(externalPortString);
const internalPort = Number(internalPortValue);
for (const port of [externalPort, internalPort]) {
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`Invalid email port mapping value: ${port}`);
}
}
normalized[externalPort] = internalPort;
}
return normalized;
}
}