222 lines
7.5 KiB
TypeScript
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;
|
|
}
|
|
}
|