import * as plugins from '../plugins.js'; import type { IEmailDomainConfig } from '@push.rocks/smartmta'; import { logger } from '../logger.js'; import { EmailDomainDoc } from '../db/documents/classes.email-domain.doc.js'; import { DomainDoc } from '../db/documents/classes.domain.doc.js'; import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js'; import type { IEmailDomain, IEmailDnsRecord, IEmailDomainRemoteIngress, IEmailDkimMaterial, IEmailDomainOperationError, TEmailDomainLifecycleStatus, } from '../../ts_interfaces/data/email-domain.js'; import { MAIL_DNS_MAX_RETRY_INTERVAL_MS, MAIL_DNS_RETRY_INTERVAL_MS, } from './classes.mail-dns-sync.js'; import { applyDefaultInboundPolicy } from './inbound-policy.js'; import { projectMailDnsIntentStatus } from './mail-dns-status.js'; export interface IEmailDomainManagerActionResult { success: boolean; message?: string; lifecycleStatus?: TEmailDomainLifecycleStatus; code?: string; errors?: IEmailDomainOperationError[]; retryAt?: string; capability?: 'supported' | 'unsupported'; domain?: IEmailDomain; records?: IEmailDnsRecord[]; provisioned?: number; } export interface IManagedDkimRepairResult { repairedDomainIds: string[]; repairedConfiguredDomains: string[]; failedConfiguredDomains: string[]; failedDomainIds: string[]; } interface IDkimMaterialRepairResult { regenerated: boolean; } interface IDkimDocumentRepairOutcome { outcome?: 'repaired' | 'failed'; failureMessage?: string; } /** * Persists managed email domains and projects them into SmartMTA. DNS mutation * is delegated exclusively to MailDnsSync. */ export class EmailDomainManager { private dcRouter: any; // DcRouter — avoids circular import private baseEmailDomains: IEmailDomainConfig[] = []; private failedBaseDkimDomainNames = new Set(); private mutationChain: Promise = Promise.resolve(); /** Explicit upstream capability marker; absence is deliberately fail-closed. */ private get selectorCorrectSigningCapability(): boolean { return (plugins.smartmta as any).smartMtaCapabilities?.selectorCorrectDkimSigning === true; } constructor(dcRouterRef: any) { this.dcRouter = dcRouterRef; this.setBaseEmailDomains(this.dcRouter.options?.emailConfig?.domains as IEmailDomainConfig[] | undefined); } public setBaseEmailDomains(domains: IEmailDomainConfig[] | undefined): void { this.baseEmailDomains = (domains || []) .map((domainConfig) => JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig); const configuredNames = new Set(this.baseEmailDomains.map((domainConfig) => domainConfig.domain.toLowerCase())); this.failedBaseDkimDomainNames = new Set( [...this.failedBaseDkimDomainNames].filter((domainName) => configuredNames.has(domainName)), ); } private get dkimCreator(): any | undefined { return this.dcRouter.emailServer?.dkimCreator; } public async start(): Promise { // Inbound projection is safe before the SMTP server exists. DNS scheduling // starts later, after DNS, RemoteIngress, tunnel, and DKIM dependencies. await this.syncManagedDomainsToRuntime(); } public async stop(): Promise { await this.mutationChain.catch(() => undefined); } public supportsSelectorCorrectSigning(): boolean { return this.selectorCorrectSigningCapability; } private async runMutationExclusive(action: () => Promise): Promise { const result = this.mutationChain.catch(() => undefined).then(action); this.mutationChain = result.then(() => undefined, () => undefined); return await result; } public async getAll(): Promise { const docs = await EmailDomainDoc.findAll(); return docs.map((doc) => this.docToInterface(doc)); } public async getById(id: string): Promise { const doc = await EmailDomainDoc.findById(id); return doc ? this.docToInterface(doc) : null; } public async getByDomain(domainName: string): Promise { const doc = await EmailDomainDoc.findByDomain(domainName); return doc ? this.docToInterface(doc) : null; } public async ensureEmailDomainForDomainName(domainName: string): Promise { const normalizedDomain = domainName.trim().toLowerCase(); const existing = await this.getByDomain(normalizedDomain); if (existing) return existing; if (this.isDomainAlreadyConfigured(normalizedDomain)) return null; const linkedDomain = await this.findLinkedDnsDomain(normalizedDomain); if (!linkedDomain) throw new Error(`DNS domain not found for email domain: ${normalizedDomain}`); const subdomain = normalizedDomain === linkedDomain.name ? undefined : normalizedDomain.slice(0, -(linkedDomain.name.length + 1)); return await this.createEmailDomain({ linkedDomainId: linkedDomain.id, subdomain }); } public async createEmailDomain(opts: { linkedDomainId: string; subdomain?: string; dkimSelector?: string; dkimKeySize?: number; rotateKeys?: boolean; rotationIntervalDays?: number; remoteIngress?: IEmailDomainRemoteIngress; }): Promise { const created = await this.runMutationExclusive(async () => { const domainDoc = await DomainDoc.findById(opts.linkedDomainId); if (!domainDoc) throw new Error(`DNS domain not found: ${opts.linkedDomainId}`); const subdomain = opts.subdomain?.trim().toLowerCase() || undefined; const domainName = (subdomain ? `${subdomain}.${domainDoc.name}` : domainDoc.name).toLowerCase(); if (this.isDomainAlreadyConfigured(domainName)) { throw new Error(`Email domain already configured for ${domainName}`); } if (await EmailDomainDoc.findByDomain(domainName)) { throw new Error(`Email domain already exists for ${domainName}`); } const selector = opts.dkimSelector?.trim() || 'default'; if (selector !== 'default' && !this.selectorCorrectSigningCapability) { throw this.capabilityError( 'SMARTMTA_SELECTOR_SIGNING_UNSUPPORTED', 'Custom DKIM selectors are disabled until the selector-correct SmartMTA release is consumed', ); } if (opts.rotateKeys && !this.selectorCorrectSigningCapability) { throw this.capabilityError( 'SMARTMTA_DKIM_ROTATION_UNSUPPORTED', 'Automatic DKIM rotation is disabled until the selector-correct SmartMTA release is consumed', ); } if (!this.dkimCreator) { throw new Error('DKIM key creator is not ready; email domain was not persisted'); } const keySize = opts.dkimKeySize || 2048; const rotationIntervalDays = this.normalizeRotationIntervalDays(opts.rotationIntervalDays ?? 90); await this.dkimCreator.handleDKIMKeysForSelector(domainName, selector, keySize); const dnsRecord = await this.dkimCreator.getDNSRecordForDomain(domainName, selector); const publicKey = this.extractDkimPublicKey(dnsRecord?.value); if (!publicKey) { throw new Error(`DKIM key generation for ${domainName} returned no usable public key; email domain was not persisted`); } const now = new Date().toISOString(); const activeDkim: IEmailDkimMaterial = { selector, keySize, publicKey, createdAt: now, }; const doc = new EmailDomainDoc(); doc.id = plugins.smartunique.shortId(); doc.domain = domainName; doc.linkedDomainId = opts.linkedDomainId; doc.subdomain = subdomain; doc.dkim = { selector, keySize, publicKey, rotateKeys: opts.rotateKeys ?? false, rotationIntervalDays, retirementOverlapDays: 30, }; doc.activeDkim = activeDkim; doc.retiringDkim = []; doc.remoteIngress = this.normalizeRemoteIngressPin(opts.remoteIngress, domainName); doc.dnsStatus = { mx: 'unchecked', spf: 'unchecked', dkim: 'unchecked', dmarc: 'unchecked' }; doc.reconciliation = { lifecycleStatus: 'pending', desiredGeneration: 1, intents: [], errors: [{ code: 'RECONCILIATION_PENDING', message: 'Initial DNS reconciliation has not completed', retryable: true, }], capability: { egressIdentityProof: 'unsupported', selectorCorrectSigning: selector === 'default' || this.selectorCorrectSigningCapability ? 'supported' : 'unsupported', }, }; doc.createdAt = now; doc.updatedAt = now; try { await doc.save(); } catch (error: unknown) { if ((error as any)?.code === 11000 || (error as any)?.codeName === 'DuplicateKey') { throw new Error(`Email domain already exists for ${domainName}`); } throw error; } await this.syncManagedDomainsToRuntime(); return { id: doc.id, domainName }; }); await this.dcRouter.mailDnsSync?.sync(`email domain created: ${created.domainName}`); const current = await EmailDomainDoc.findById(created.id); if (!current) throw new Error(`Email domain disappeared during reconciliation: ${created.domainName}`); logger.log('info', `Email domain created in ${current.reconciliation?.lifecycleStatus || 'pending'} state: ${created.domainName}`); return this.docToInterface(current); } public async updateEmailDomain( id: string, changes: { rotateKeys?: boolean; rotationIntervalDays?: number; rateLimits?: IEmailDomain['rateLimits']; remoteIngress?: IEmailDomainRemoteIngress | null; }, ): Promise { const mutation = await this.runMutationExclusive(async () => { const doc = await EmailDomainDoc.findById(id); if (!doc) throw new Error(`Email domain not found: ${id}`); if (changes.rotateKeys === true && !this.selectorCorrectSigningCapability) { return { kind: 'result' as const, result: this.unsupportedResult( doc, 'SMARTMTA_DKIM_ROTATION_UNSUPPORTED', 'Automatic DKIM rotation is disabled until the selector-correct SmartMTA release is consumed', ), }; } if (changes.rotateKeys !== undefined) doc.dkim.rotateKeys = changes.rotateKeys; if (changes.rotationIntervalDays !== undefined) { doc.dkim.rotationIntervalDays = this.normalizeRotationIntervalDays(changes.rotationIntervalDays); } if (changes.rateLimits !== undefined) doc.rateLimits = changes.rateLimits; if (changes.remoteIngress !== undefined) { doc.remoteIngress = changes.remoteIngress === null ? undefined : this.normalizeRemoteIngressPin(changes.remoteIngress, doc.domain); } doc.reconciliation = this.invalidateReconciliation(doc, 'EMAIL_DOMAIN_CONFIGURATION_CHANGED'); doc.updatedAt = new Date().toISOString(); await doc.save(); await this.syncManagedDomainsToRuntime(); return { kind: 'sync' as const, domainName: doc.domain }; }); if (mutation.kind === 'result') return mutation.result; await this.dcRouter.mailDnsSync?.sync(`email domain updated: ${mutation.domainName}`); const current = await EmailDomainDoc.findById(id); if (!current) throw new Error(`Email domain disappeared during reconciliation: ${id}`); return this.resultForDoc(current, !this.hasDnsMutationFailure(current)); } public async deleteEmailDomain(id: string): Promise { const mutation = await this.runMutationExclusive(async () => { const doc = await EmailDomainDoc.findById(id); if (!doc) throw new Error(`Email domain not found: ${id}`); const bindings = await this.dcRouter.workAppMailManager?.listMailAddressBindings({ domain: doc.domain }) || []; if (bindings.length > 0) { return { kind: 'result' as const, result: { success: false, lifecycleStatus: doc.reconciliation?.lifecycleStatus || 'pending', code: 'EMAIL_DOMAIN_HAS_BINDINGS', message: `Email domain ${doc.domain} still has ${bindings.length} WorkApp mail binding(s)`, errors: [{ code: 'EMAIL_DOMAIN_HAS_BINDINGS', message: 'Delete all WorkApp mail bindings before deleting the email domain', retryable: false, }], } satisfies IEmailDomainManagerActionResult, }; } const now = new Date().toISOString(); doc.reconciliation = { ...(doc.reconciliation || { desiredGeneration: 1, intents: [], errors: [] }), lifecycleStatus: 'deleting', intents: [], errors: [], deletingAt: now, retryAt: new Date(Date.now() + 300_000).toISOString(), }; doc.updatedAt = now; await doc.save(); await this.syncManagedDomainsToRuntime(); return { kind: 'sync' as const, domainName: doc.domain }; }); if (mutation.kind === 'result') return mutation.result; await this.dcRouter.mailDnsSync?.sync(`email domain deleting: ${mutation.domainName}`); const remaining = await EmailDomainDoc.findById(id); if (!remaining) { return { success: true, lifecycleStatus: 'deleting', message: `Email domain ${mutation.domainName} deleted` }; } return this.resultForDoc(remaining, remaining.reconciliation?.errors.length === 0); } public async getRequiredDnsRecords(id: string): Promise { const doc = await EmailDomainDoc.findById(id); if (!doc) throw new Error(`Email domain not found: ${id}`); return (doc.reconciliation?.intents || []).map((intent) => ({ type: intent.type, name: intent.name, value: intent.value, status: projectMailDnsIntentStatus(intent), })); } public async provisionDnsRecords(id: string): Promise { const current = await EmailDomainDoc.findById(id); if (!current) throw new Error(`Email domain not found: ${id}`); const before = new Set((await DnsRecordDoc.findByDomainId(current.linkedDomainId)) .filter((record) => record.managedBy === 'mail-dns-reconciler').map((record) => record.id)); const mailDnsSync = this.dcRouter.mailDnsSync; if (!mailDnsSync) throw new Error('MailDnsSync is unavailable'); const ticket = await mailDnsSync.syncDomain(id, `manual provision: ${id}`); const doc = ticket.domain; if (!doc) throw new Error(`Email domain not found: ${id}`); const provisioned = ticket.records .filter((record) => record.managedBy === 'mail-dns-reconciler' && !before.has(record.id)).length; return { ...this.resultForDoc(doc, !this.hasDnsMutationFailure(doc)), provisioned }; } public async validateDns(id: string): Promise { const mailDnsSync = this.dcRouter.mailDnsSync; if (!mailDnsSync) throw new Error('MailDnsSync is unavailable'); const ticket = await mailDnsSync.syncDomain(id, `manual validation: ${id}`); const doc = ticket.domain; if (!doc) throw new Error(`Email domain not found: ${id}`); const records = (doc.reconciliation?.intents || []).map((intent) => ({ type: intent.type, name: intent.name, value: intent.value, status: projectMailDnsIntentStatus(intent), })); return { ...this.resultForDoc(doc, !this.hasDnsMutationFailure(doc)), domain: this.docToInterface(doc), records, }; } public async getOutboundReadiness(domainName: string): Promise<{ ready: boolean; reason?: string; edgeIds: string[]; selector?: string; }> { const doc = await EmailDomainDoc.findByDomain(domainName.toLowerCase()); if (!doc) return { ready: true, edgeIds: [] }; // unmanaged/static domain policy const repairFailure = doc.reconciliation?.errors?.find( (error) => error.code === 'DKIM_KEY_REPAIR_FAILED', ); if (repairFailure) { return { ready: false, reason: repairFailure.message, edgeIds: [] }; } const activeDkim = this.getActiveDkim(doc); if (!activeDkim) return { ready: false, reason: 'no usable active DKIM key', edgeIds: [] }; if (activeDkim.selector !== 'default' && !this.selectorCorrectSigningCapability) { return { ready: false, reason: 'SmartMTA selector-correct signing capability is unavailable', edgeIds: [] }; } const reconciliation = doc.reconciliation; if (reconciliation?.lifecycleStatus !== 'active') { return { ready: false, reason: 'managed email domain DNS lifecycle is not active', edgeIds: [] }; } if (reconciliation.errors.length > 0) { return { ready: false, reason: reconciliation.errors[0].message, edgeIds: [] }; } if (!await DomainDoc.findById(doc.linkedDomainId)) { return { ready: false, reason: 'linked DNS domain is missing', edgeIds: [] }; } if ( !reconciliation?.activeRevision || reconciliation.activeRevision.generation !== reconciliation.desiredGeneration ) { return { ready: false, reason: 'no current validated active DNS revision', edgeIds: [] }; } const slots = reconciliation.activeRevision.mxSlots; if ( slots.length !== 2 || slots[0]?.priority !== 10 || slots[1]?.priority !== 20 || slots[0].edgeId === slots[1].edgeId || slots[0].hostname === slots[1].hostname ) { return { ready: false, reason: 'validated DNS revision does not contain distinct priority 10/20 MX edges', edgeIds: [] }; } if (!reconciliation.intents.every((intent) => intent.status === 'valid')) { return { ready: false, reason: 'managed email DNS intents are not currently valid', edgeIds: [] }; } const live = await this.dcRouter.mailEdgeEligibility.resolveActiveForDomain(doc); if (live.edges.length !== 2) { return { ready: false, reason: live.reason || 'both validated active MX edges must be live', edgeIds: [] }; } return { ready: true, edgeIds: live.edges.map((candidate: any) => candidate.edge.id), selector: activeDkim.selector, }; } /** * Caller-owned repair for legacy managed domains. SmartMTA remains in * validation-only mode; dcrouter explicitly owns any required key creation. */ public async repairManagedDkimBeforeEmailStart(): Promise { const repairedDomainIds: string[] = []; const failedDomainIds: string[] = []; const repairedConfiguredDomains: string[] = []; const failedConfiguredDomains: string[] = []; await this.runMutationExclusive(async () => { const dkimCreator = this.dkimCreator; if (!dkimCreator) { throw new Error('DKIM key creator is not ready for managed-domain repair'); } const managedDocs = await EmailDomainDoc.findAll(); for (const doc of managedDocs) { if (doc.reconciliation?.lifecycleStatus === 'deleting') continue; const { outcome, failureMessage } = await this.repairManagedDkimDocument(doc, dkimCreator); // Persistence failures are global prerequisites, not per-domain key // failures. Keep this save outside the material-repair catch. await doc.save(); if (outcome === 'repaired') { repairedDomainIds.push(doc.id); } else if (outcome === 'failed') { failedDomainIds.push(doc.id); logger.log('error', failureMessage!); } } const managedNames = new Set( managedDocs .filter((doc) => doc.reconciliation?.lifecycleStatus !== 'deleting') .map((doc) => doc.domain.toLowerCase()), ); for (const domainConfig of this.baseEmailDomains) { const domainName = domainConfig.domain.toLowerCase(); if (managedNames.has(domainName)) continue; const selector = domainConfig.dkim?.selector || 'default'; const keySize = domainConfig.dkim?.keySize || 2048; try { const result = await this.ensureValidatedDkimMaterial(domainName, selector, keySize, dkimCreator); const wasFailed = this.failedBaseDkimDomainNames.delete(domainName); if (result.regenerated || wasFailed) repairedConfiguredDomains.push(domainName); } catch (error: unknown) { const message = `Failed to repair caller-managed DKIM for ${selector}._domainkey.${domainName}: ${(error as Error).message}`; this.failedBaseDkimDomainNames.add(domainName); failedConfiguredDomains.push(domainName); logger.log('error', message); } } await this.syncManagedDomainsToRuntime({ excludeDkimRepairFailures: true }); }); if (repairedDomainIds.length > 0) { this.dcRouter.mailDnsSync?.requestSync( 'caller-managed DKIM material repaired before SmartMTA start', [...new Set(repairedDomainIds)], ); } return { repairedDomainIds, repairedConfiguredDomains, failedConfiguredDomains, failedDomainIds, }; } /** Retry a previously failed managed-domain repair when its bounded retry is due. */ public async repairManagedDkimIfDue(id: string, now = Date.now()): Promise { return await this.runMutationExclusive(async () => { const doc = await EmailDomainDoc.findById(id); if (!doc || !this.hasDkimRepairFailure(doc)) return doc || undefined; const retryAt = Date.parse(doc.reconciliation?.retryAt || ''); if (Number.isFinite(retryAt) && retryAt > now) return doc; const dkimCreator = this.dkimCreator; if (!dkimCreator) return doc; const result = await this.repairManagedDkimDocument(doc, dkimCreator); await doc.save(); await this.syncManagedDomainsToRuntime({ excludeDkimRepairFailures: true }); if (result.outcome === 'failed') logger.log('error', result.failureMessage!); return doc; }); } private async repairManagedDkimDocument( doc: EmailDomainDoc, dkimCreator: any, ): Promise { const currentActive = this.getActiveDkim(doc); const selector = currentActive?.selector || doc.dkim?.selector || 'default'; const keySize = currentActive?.keySize || doc.dkim?.keySize || 2048; try { const { regenerated } = await this.ensureValidatedDkimMaterial( doc.domain, selector, keySize, dkimCreator, ); const record = await dkimCreator.getDNSRecordForSelector(doc.domain, selector); const publicKey = this.extractDkimPublicKey(record?.value); if (!publicKey) throw new Error('validated DKIM material produced no usable public key'); if ( !regenerated && !this.hasDkimRepairFailure(doc) && currentActive?.selector === selector && currentActive.publicKey === publicKey && doc.dkim?.selector === selector && doc.dkim.publicKey === publicKey ) return {}; const timestamp = new Date().toISOString(); doc.activeDkim = { selector, keySize, publicKey, createdAt: regenerated ? timestamp : currentActive?.createdAt || timestamp, validatedAt: timestamp, }; doc.dkim = { ...doc.dkim, selector, keySize, publicKey, ...(regenerated ? { lastRotatedAt: timestamp } : {}), }; doc.dnsStatus = { ...doc.dnsStatus, dkim: 'unchecked' }; doc.reconciliation = this.invalidateDkimReconciliation( doc, regenerated ? 'DKIM_KEY_MATERIAL_REGENERATED' : 'DKIM_KEY_MATERIAL_REPAIRED', regenerated ? 'Caller-managed DKIM key material was regenerated; DNS deployment must be revalidated' : 'Caller-managed DKIM metadata was repaired; DNS deployment must be revalidated', ); doc.updatedAt = timestamp; return { outcome: 'repaired' }; } catch (error: unknown) { const message = `Failed to repair caller-managed DKIM for ${selector}._domainkey.${doc.domain}: ${(error as Error).message}`; const previousAttempt = doc.reconciliation?.retryAttempt || 0; const retryAttempt = previousAttempt + 1; const retryDelay = Math.min( MAIL_DNS_RETRY_INTERVAL_MS * (2 ** Math.max(0, retryAttempt - 1)), MAIL_DNS_MAX_RETRY_INTERVAL_MS, ); doc.reconciliation = this.invalidateDkimReconciliation(doc, 'DKIM_KEY_REPAIR_FAILED', message); doc.reconciliation.retryAttempt = retryAttempt; doc.reconciliation.retryAt = new Date(Date.now() + retryDelay).toISOString(); doc.dnsStatus = { ...doc.dnsStatus, dkim: 'invalid' }; doc.updatedAt = new Date().toISOString(); return { outcome: 'failed', failureMessage: message }; } } private async ensureValidatedDkimMaterial( domain: string, selector: string, keySize: number, dkimCreator: any, ): Promise { let initialError: unknown; try { await dkimCreator.readValidatedDKIMKeysForSelector(domain, selector); return { regenerated: false }; } catch (error: unknown) { initialError = error; } let creationError: unknown; try { await dkimCreator.createAndStoreDKIMKeysForSelector(domain, selector, keySize); } catch (error: unknown) { creationError = error; } try { await dkimCreator.readValidatedDKIMKeysForSelector(domain, selector); // Creation may have stored a complete key pair before a metadata write // failed. Treat any attempted regeneration as a DNS-changing operation. return { regenerated: true }; } catch (finalError: unknown) { const parts = [ `initial validation failed: ${(initialError as Error).message}`, creationError ? `regeneration failed: ${(creationError as Error).message}` : 'regeneration completed', `final validation failed: ${(finalError as Error).message}`, ]; throw new Error(parts.join('; ')); } } public async requestDkimRotation(id: string): Promise { const mutation = await this.runMutationExclusive(async () => { const doc = await EmailDomainDoc.findById(id); if (!doc) throw new Error(`Email domain not found: ${id}`); if (!this.selectorCorrectSigningCapability) { return { kind: 'result' as const, result: this.unsupportedResult( doc, 'SMARTMTA_DKIM_ROTATION_UNSUPPORTED', 'DKIM rotation remains pending until the selector-correct SmartMTA release is consumed', ), }; } if (doc.pendingDkim) { return { kind: 'result' as const, result: { ...this.resultForDoc(doc, true), message: `Selector ${doc.pendingDkim.selector} is already staged`, }, }; } await this.stageDkimRotation(doc); return { kind: 'sync' as const, domainName: doc.domain }; }); if (mutation.kind === 'result') return mutation.result; await this.dcRouter.mailDnsSync?.sync(`DKIM rotation staged: ${mutation.domainName}`); const current = await EmailDomainDoc.findById(id); if (!current) throw new Error(`Email domain disappeared during reconciliation: ${id}`); return this.resultForDoc(current, !this.hasDnsMutationFailure(current)); } /** Called by the reconciler before planning so scheduled external rotation is durable first. */ public async stageDkimRotationIfDue(id: string): Promise { return await this.runMutationExclusive(async () => { const doc = await EmailDomainDoc.findById(id); if (!doc || !doc.dkim.rotateKeys || doc.pendingDkim || !this.selectorCorrectSigningCapability) { return doc || undefined; } const active = this.getActiveDkim(doc); const reference = Date.parse(active?.promotedAt || doc.dkim.lastRotatedAt || active?.createdAt || doc.createdAt); const intervalMs = Math.max(1, doc.dkim.rotationIntervalDays || 90) * 86_400_000; if (Number.isFinite(reference) && Date.now() - reference < intervalMs) return doc; try { await this.stageDkimRotation(doc); } catch (error: unknown) { doc.reconciliation = this.invalidateReconciliation(doc, 'DKIM_ROTATION_STAGE_FAILED'); doc.reconciliation.errors = [{ code: 'DKIM_ROTATION_STAGE_FAILED', message: `Failed to stage DKIM rotation: ${(error as Error).message}`, retryable: true, }]; doc.updatedAt = new Date().toISOString(); await doc.save(); } return doc; }); } private async stageDkimRotation(doc: EmailDomainDoc): Promise { if (!this.dkimCreator) throw new Error('DKIM key creator is not ready'); const selector = `s${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${plugins.smartunique.shortId()}`; const keySize = doc.dkim.keySize || 2048; await this.dkimCreator.handleDKIMKeysForSelector(doc.domain, selector, keySize); const record = await this.dkimCreator.getDNSRecordForDomain(doc.domain, selector); const publicKey = this.extractDkimPublicKey(record?.value); if (!publicKey) throw new Error(`DKIM rotation generated no public key for ${selector}`); doc.pendingDkim = { selector, keySize, publicKey, createdAt: new Date().toISOString() }; doc.reconciliation = this.invalidateReconciliation(doc, 'DKIM_ROTATION_STAGED'); doc.updatedAt = new Date().toISOString(); await doc.save(); } private normalizeRemoteIngressPin( pin: IEmailDomainRemoteIngress | undefined, domainName: string, ): IEmailDomainRemoteIngress | undefined { const edgeFilter = [...new Set((pin?.edgeFilter || []).map((entry) => `${entry}`.trim()).filter(Boolean))]; if (edgeFilter.length === 0) return undefined; const manager = this.dcRouter.remoteIngressManager; if (manager && manager.resolveEdgesByFilter(edgeFilter).length === 0) { logger.log('warn', `Email domain ${domainName}: pin [${edgeFilter.join(', ')}] matches no enabled edge yet`); } return { edgeFilter }; } private normalizeRotationIntervalDays(value: number): number { if (!Number.isInteger(value) || value < 1) { throw new Error('DKIM rotationIntervalDays must be a positive integer'); } return value; } private extractDkimPublicKey(value: unknown): string | undefined { if (typeof value !== 'string') return undefined; return value.match(/(?:^|;)\s*p=([A-Za-z0-9+/=]+)(?:;|$)/)?.[1]; } private getActiveDkim(doc: EmailDomainDoc): IEmailDkimMaterial | undefined { if (doc.activeDkim?.publicKey) return doc.activeDkim; if (!doc.dkim?.publicKey) return undefined; return { selector: doc.dkim.selector || 'default', keySize: doc.dkim.keySize || 2048, publicKey: doc.dkim.publicKey, createdAt: doc.createdAt, }; } private invalidateReconciliation(doc: EmailDomainDoc, code: string) { return { ...(doc.reconciliation || { desiredGeneration: 0, intents: [], errors: [] }), lifecycleStatus: doc.reconciliation?.activeRevision ? 'active' as const : 'pending' as const, desiredGeneration: (doc.reconciliation?.desiredGeneration || 0) + 1, errors: [{ code, message: 'DNS reconciliation is pending', retryable: true }], retryAt: new Date().toISOString(), }; } private invalidateDkimReconciliation( doc: EmailDomainDoc, code: string, message: string, retryable = true, ) { const previous = doc.reconciliation || { lifecycleStatus: 'pending' as const, desiredGeneration: 0, intents: [], errors: [], }; const { activeRevision: _activeRevision, pendingRevision: _pendingRevision, intents: _intents, errors: _errors, lastSuccessAt: _lastSuccessAt, retryAt: _retryAt, retryAttempt: _retryAttempt, ...preserved } = previous; const alreadyInvalidated = ( !previous.activeRevision && previous.errors.length === 1 && previous.errors[0].code === code && previous.errors[0].message === message ); return { ...preserved, lifecycleStatus: retryable ? 'pending' as const : 'failed' as const, desiredGeneration: alreadyInvalidated ? previous.desiredGeneration : previous.desiredGeneration + 1, intents: [], errors: [{ code, message, retryable }], ...(retryable ? { retryAt: new Date().toISOString() } : {}), }; } private capabilityError(code: string, message: string): Error { const error = new Error(message) as Error & Record; error.code = code; error.capability = 'unsupported'; error.errors = [{ code, message, retryable: false }]; return error; } private unsupportedResult(doc: EmailDomainDoc, code: string, message: string): IEmailDomainManagerActionResult { return { success: false, lifecycleStatus: doc.reconciliation?.lifecycleStatus || 'pending', code, capability: 'unsupported', message, errors: [{ code, message, retryable: false }], }; } private hasDnsMutationFailure(doc: EmailDomainDoc): boolean { return (doc.reconciliation?.intents || []).some((intent) => ( intent.status === 'failed' || intent.status === 'conflict' )) || (doc.reconciliation?.errors || []).some((error) => ( ['DNS_PROVIDER_ZONE_FAILED', 'DNS_UNMANAGED_CONFLICT', 'DNS_CLEANUP_FAILED'].includes(error.code) )); } private resultForDoc(doc: EmailDomainDoc, actionSucceeded: boolean): IEmailDomainManagerActionResult { const state = doc.reconciliation; const firstError = state?.errors[0]; return { success: actionSucceeded, lifecycleStatus: state?.lifecycleStatus || 'pending', message: firstError?.message, code: firstError?.code, errors: state?.errors, retryAt: state?.retryAt, capability: state?.capability?.egressIdentityProof, domain: this.docToInterface(doc), }; } private docToInterface(doc: EmailDomainDoc): IEmailDomain { return { id: doc.id, domain: doc.domain, linkedDomainId: doc.linkedDomainId, subdomain: doc.subdomain, dkim: doc.dkim, rateLimits: doc.rateLimits, remoteIngress: doc.remoteIngress, dnsStatus: doc.dnsStatus, reconciliation: doc.reconciliation, activeDkim: doc.activeDkim, pendingDkim: doc.pendingDkim, retiringDkim: doc.retiringDkim, createdAt: doc.createdAt, updatedAt: doc.updatedAt, }; } private isDomainAlreadyConfigured(domainName: string): boolean { return ((this.dcRouter.options?.emailConfig?.domains || []) as IEmailDomainConfig[]) .some((domainConfig) => domainConfig.domain.toLowerCase() === domainName.toLowerCase()); } private async findLinkedDnsDomain(domainName: string): Promise { const domains = await DomainDoc.findAll(); return domains .filter((domainDoc) => domainName === domainDoc.name || domainName.endsWith(`.${domainDoc.name}`)) .sort((left, right) => right.name.length - left.name.length)[0] || null; } private async buildManagedDomainConfigs(optionsArg: { excludeDkimRepairFailures?: boolean; } = {}): Promise { const docs = await EmailDomainDoc.findAll(); const managedConfigs: IEmailDomainConfig[] = []; for (const doc of docs) { if (doc.reconciliation?.lifecycleStatus === 'deleting') continue; const dkimRepairFailed = this.hasDkimRepairFailure(doc); if (dkimRepairFailed && optionsArg.excludeDkimRepairFailures) continue; if (!await DomainDoc.findById(doc.linkedDomainId)) { logger.log('warn', `Managed email domain ${doc.domain} has no linked DNS domain; keeping it out of runtime`); continue; } const activeDkim = this.getActiveDkim(doc); const dkimConfig = !dkimRepairFailed && activeDkim && (activeDkim.selector === 'default' || this.selectorCorrectSigningCapability) ? { selector: activeDkim.selector, keySize: activeDkim.keySize, // Dcrouter exclusively owns staged external rotation. rotateKeys: false, rotationInterval: doc.dkim.rotationIntervalDays, } : undefined; managedConfigs.push({ domain: doc.domain, dnsMode: 'external-dns', ...(dkimConfig ? { dkim: dkimConfig } : {}), rateLimits: doc.rateLimits, }); } return managedConfigs; } public async syncManagedDomainsToRuntime(optionsArg: { excludeDkimRepairFailures?: boolean; } = {}): Promise { if (!this.dcRouter.options?.emailConfig) return; const failedDomainNames = new Set( (await EmailDomainDoc.findAll()) .filter((doc) => this.hasDkimRepairFailure(doc)) .map((doc) => doc.domain.toLowerCase()), ); const mergedDomains = new Map(); for (const domainConfig of this.baseEmailDomains) { const key = domainConfig.domain.toLowerCase(); const dkimRepairFailed = failedDomainNames.has(key) || this.failedBaseDkimDomainNames.has(key); if (dkimRepairFailed && optionsArg.excludeDkimRepairFailures) continue; const projectedConfig = JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig; if (dkimRepairFailed) delete projectedConfig.dkim; mergedDomains.set(key, projectedConfig); } for (const managedConfig of await this.buildManagedDomainConfigs(optionsArg)) { const key = managedConfig.domain.toLowerCase(); if (mergedDomains.has(key)) { logger.log('warn', `Managed email domain ${managedConfig.domain} duplicates a configured domain; keeping the configured definition`); continue; } mergedDomains.set(key, managedConfig); } const domains = applyDefaultInboundPolicy([...mergedDomains.values()]); this.dcRouter.options.emailConfig.domains = domains; this.dcRouter.emailServer?.updateOptions({ domains }); } private hasDkimRepairFailure(doc: EmailDomainDoc): boolean { return (doc.reconciliation?.errors || []).some( (error) => error.code === 'DKIM_KEY_REPAIR_FAILED', ); } }