import type { IEmailRoute, IUnifiedEmailServerOptions, } from '@push.rocks/smartmta'; import * as plugins from '../plugins.js'; import type * as interfaces from '../../ts_interfaces/index.js'; type TSyncRequest = interfaces.requests.IReq_SyncWorkAppMailIdentity['request']; interface IStoredWorkAppMailIdentity extends interfaces.data.IWorkAppMailIdentity { smtpPassword: string; } interface IStoredWorkAppMailState { version: 1; identities: IStoredWorkAppMailIdentity[]; } export class WorkAppMailManager { private readonly storageKey = '/workhosters/mail-identities.json'; constructor(private dcRouterRef: any) {} public async listMailIdentities( ownership?: Partial, ): Promise { const identities = await this.readStoredIdentities(); return identities .filter((identity) => this.matchesOwnership(identity.ownership, ownership)) .map((identity) => this.toPublicIdentity(identity)); } public async syncMailIdentity( request: TSyncRequest, createdBy: string, ): Promise { if (!this.dcRouterRef.options.emailConfig) { return { success: false, message: 'Email server is not configured' }; } const ownership = this.normalizeOwnership(request.ownership); const domain = this.normalizeDomain(request.domain); const localPart = this.normalizeLocalPart(request.localPart); const address = `${localPart}@${domain}`; const externalKey = this.buildExternalKey(ownership, address); const identities = await this.readStoredIdentities(); const existingIndex = identities.findIndex((identity) => identity.externalKey === externalKey); if (request.delete) { if (existingIndex < 0) { return { success: true, action: 'unchanged' }; } const [deletedIdentity] = identities.splice(existingIndex, 1); await this.writeStoredIdentities(identities); await this.applyStoredIdentitiesToRuntime(identities); return { success: true, action: 'deleted', identity: this.toPublicIdentity(deletedIdentity), }; } await this.ensureEmailDomainConfigured(domain); const existingIdentity = existingIndex >= 0 ? identities[existingIndex] : undefined; const now = Date.now(); const smtpPassword = existingIdentity && !request.resetSmtpPassword ? existingIdentity.smtpPassword : this.generateSmtpPassword(); const identity: IStoredWorkAppMailIdentity = { id: existingIdentity?.id || plugins.smartunique.shortId(), externalKey, ownership, address, localPart, domain, enabled: request.enabled ?? existingIdentity?.enabled ?? true, displayName: request.displayName ?? existingIdentity?.displayName, inbound: this.normalizeInboundRoute(request.inbound ?? existingIdentity?.inbound), smtp: { enabled: request.smtpEnabled ?? existingIdentity?.smtp.enabled ?? true, username: existingIdentity?.smtp.username || this.buildSmtpUsername(externalKey), }, createdAt: existingIdentity?.createdAt || now, updatedAt: now, createdBy: existingIdentity?.createdBy || createdBy, smtpPassword, }; if (existingIndex >= 0) { identities[existingIndex] = identity; } else { identities.push(identity); } await this.writeStoredIdentities(identities); await this.applyStoredIdentitiesToRuntime(identities); const response: interfaces.data.IWorkAppMailIdentitySyncResult = { success: true, action: existingIndex >= 0 ? 'updated' : 'created', identity: this.toPublicIdentity(identity), }; if (existingIndex < 0 || request.resetSmtpPassword) { response.smtpCredentials = this.buildSmtpCredentials(identity); } return response; } public async applyStoredIdentitiesToEmailConfig( emailConfig: TConfig, ): Promise { const identities = await this.readStoredIdentities(); return this.mergeIdentitiesIntoEmailConfig(emailConfig, identities); } public async applyStoredIdentitiesToRuntime( identities = undefined as IStoredWorkAppMailIdentity[] | undefined, ): Promise { const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined; if (!emailConfig) return; const nextConfig = this.mergeIdentitiesIntoEmailConfig( emailConfig, identities || await this.readStoredIdentities(), ); this.dcRouterRef.options.emailConfig = nextConfig; if (this.dcRouterRef.emailServer) { this.dcRouterRef.emailServer.updateOptions({ auth: nextConfig.auth }); await this.dcRouterRef.updateEmailRoutes(nextConfig.routes); } } private async readStoredIdentities(): Promise { const storedData = await this.dcRouterRef.storageManager.get(this.storageKey); if (!storedData) return []; const parsed = JSON.parse(storedData) as IStoredWorkAppMailState | IStoredWorkAppMailIdentity[]; return Array.isArray(parsed) ? parsed : parsed.identities || []; } private async writeStoredIdentities(identities: IStoredWorkAppMailIdentity[]): Promise { const state: IStoredWorkAppMailState = { version: 1, identities, }; await this.dcRouterRef.storageManager.set(this.storageKey, JSON.stringify(state, null, 2)); } private mergeIdentitiesIntoEmailConfig( emailConfig: TConfig, identities: IStoredWorkAppMailIdentity[], ): TConfig { const generatedRoutes = identities .filter((identity) => identity.enabled && identity.inbound?.enabled) .map((identity) => this.buildInboundRoute(identity)); const configuredRoutes = (emailConfig.routes || []) .filter((route) => !this.isManagedMailRouteName(route.name)); const generatedUsers = identities .filter((identity) => identity.enabled && identity.smtp.enabled) .map((identity) => ({ username: identity.smtp.username, password: identity.smtpPassword, })); const configuredUsers = (emailConfig.auth?.users || []) .filter((user) => !this.isManagedSmtpUsername(user.username)); return { ...emailConfig, routes: [...configuredRoutes, ...generatedRoutes], auth: { ...(emailConfig.auth || {}), users: [...configuredUsers, ...generatedUsers], }, }; } private buildInboundRoute(identity: IStoredWorkAppMailIdentity): IEmailRoute { const inbound = identity.inbound!; return { name: this.buildRouteName(identity.externalKey), priority: 1000, match: { recipients: identity.address, }, action: { type: 'forward', forward: { host: inbound.targetHost, port: inbound.targetPort, preserveHeaders: inbound.preserveHeaders ?? true, addHeaders: { 'X-Dcrouter-WorkHoster-Type': identity.ownership.workHosterType, 'X-Dcrouter-WorkHoster-Id': identity.ownership.workHosterId, 'X-Dcrouter-WorkApp-Id': identity.ownership.workAppId, ...(inbound.addHeaders || {}), }, }, }, }; } private async ensureEmailDomainConfigured(domain: string): Promise { const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined; if (emailConfig?.domains?.some((domainConfig) => domainConfig.domain.toLowerCase() === domain)) { return; } const emailDomainManager = this.dcRouterRef.emailDomainManager; if (!emailDomainManager) { throw new Error(`Email domain is not configured: ${domain}`); } if (await emailDomainManager.getByDomain(domain)) { await emailDomainManager.syncManagedDomainsToRuntime(); return; } await emailDomainManager.ensureEmailDomainForDomainName(domain); } private normalizeOwnership( ownership: interfaces.data.IWorkAppMailOwnership, ): interfaces.data.IWorkAppMailOwnership { const workHosterType = ownership.workHosterType; const workHosterId = ownership.workHosterId?.trim(); const workAppId = ownership.workAppId?.trim(); if (!['onebox', 'cloudly', 'custom'].includes(workHosterType)) { throw new Error(`Invalid WorkHoster type: ${workHosterType}`); } if (!workHosterId) throw new Error('workHosterId is required'); if (!workAppId) throw new Error('workAppId is required'); return { workHosterType, workHosterId, workAppId }; } private normalizeDomain(domain: string): string { const normalized = domain?.trim().toLowerCase(); if (!normalized || normalized.includes('@') || !normalized.includes('.')) { throw new Error(`Invalid email domain: ${domain}`); } return normalized; } private normalizeLocalPart(localPart: string): string { const normalized = localPart?.trim().toLowerCase(); if (!normalized || normalized.includes('@') || /\s/.test(normalized)) { throw new Error(`Invalid email local part: ${localPart}`); } return normalized; } private normalizeInboundRoute( inbound?: interfaces.data.IWorkAppMailInboundRoute, ): interfaces.data.IWorkAppMailInboundRoute | undefined { if (!inbound) return undefined; if (!inbound.enabled) { return { ...inbound, enabled: false }; } const targetHost = inbound.targetHost?.trim(); const targetPort = Number(inbound.targetPort); if (!targetHost) throw new Error('inbound.targetHost is required when inbound routing is enabled'); if (!Number.isInteger(targetPort) || targetPort < 1 || targetPort > 65535) { throw new Error(`Invalid inbound.targetPort: ${inbound.targetPort}`); } return { ...inbound, targetHost, targetPort, }; } private matchesOwnership( ownership: interfaces.data.IWorkAppMailOwnership, filter?: Partial, ): boolean { if (!filter) return true; if (filter.workHosterType && filter.workHosterType !== ownership.workHosterType) return false; if (filter.workHosterId && filter.workHosterId !== ownership.workHosterId) return false; if (filter.workAppId && filter.workAppId !== ownership.workAppId) return false; return true; } private buildExternalKey( ownership: interfaces.data.IWorkAppMailOwnership, address: string, ): string { return [ ownership.workHosterType, ownership.workHosterId, ownership.workAppId, address, ].join(':'); } private buildSmtpUsername(externalKey: string): string { return `workapp-${this.hashExternalKey(externalKey).slice(0, 24)}`; } private buildRouteName(externalKey: string): string { return `workapp-mail-${this.hashExternalKey(externalKey).slice(0, 32)}`; } private hashExternalKey(externalKey: string): string { return plugins.crypto.createHash('sha256').update(externalKey).digest('hex'); } private generateSmtpPassword(): string { return plugins.crypto.randomBytes(24).toString('base64url'); } private isManagedMailRouteName(routeName: string): boolean { return routeName.startsWith('workapp-mail-'); } private isManagedSmtpUsername(username: string): boolean { return username.startsWith('workapp-'); } private buildSmtpCredentials( identity: IStoredWorkAppMailIdentity, ): interfaces.data.IWorkAppMailCredentials { return { username: identity.smtp.username, password: identity.smtpPassword, host: this.dcRouterRef.options.emailConfig?.outbound?.hostname || this.dcRouterRef.options.emailConfig?.hostname, ports: { smtp: this.dcRouterRef.options.emailConfig?.ports?.includes(25) ? 25 : undefined, submission: this.dcRouterRef.options.emailConfig?.ports?.includes(587) ? 587 : undefined, smtps: this.dcRouterRef.options.emailConfig?.ports?.includes(465) ? 465 : undefined, }, }; } private toPublicIdentity( identity: IStoredWorkAppMailIdentity, ): interfaces.data.IWorkAppMailIdentity { const { smtpPassword, ...publicIdentity } = identity; return publicIdentity; } }