import type { IEmailRoute, IExtendedSmtpSession, IMessageAcceptanceContext, IMessageAcceptanceDecision, IUnifiedEmailServerOptions, } from '@push.rocks/smartmta'; import type { Buffer } from 'node:buffer'; import * as plugins from '../plugins.js'; import { CachedEmail } from '../db/index.js'; import type { ICachedEmailSmtpTransaction } from '../db/documents/classes.cached.email.js'; import type { DcRouter } from '../classes.dcrouter.js'; import type * as interfaces from '../../ts_interfaces/index.js'; import { GatewayMailDomainStore, type IGatewayMailDomainStore } from './classes.gateway-mail-domain.store.js'; import { normalizeGatewayMailDomain, normalizeGatewayMailDomainOwner, type IStoredWorkAppMailIdentity, } from './gateway-mail-domain-authority.js'; type TSyncRequest = { ownership: interfaces.data.IWorkAppMailOwnership; localPart: string; domain: string; displayName?: string; inbound?: interfaces.data.IWorkAppMailInboundRoute; inboundTarget?: plugins.servezoneInterfaces.data.IMailInboundTarget; enabled?: boolean; smtpEnabled?: boolean; resetSmtpPassword?: boolean; delete?: boolean; }; type TMailResourceOwner = plugins.servezoneInterfaces.data.IMailResourceOwner; type TMailAddressBinding = plugins.servezoneInterfaces.data.IMailAddressBinding; type TMailAddressBindingSync = plugins.servezoneInterfaces.requests.mail.TMailAddressBindingSync; type TMailAddressBindingSyncResponse = plugins.servezoneInterfaces.requests.mail.IReq_SyncMailAddressBinding['response']; type TMailAddressBindingDeleteResponse = plugins.servezoneInterfaces.requests.mail.IReq_DeleteMailAddressBinding['response']; type TMailCredentialRotateResponse = plugins.servezoneInterfaces.requests.mail.IReq_RotateMailCredential['response']; type TMailEnqueueResponse = plugins.servezoneInterfaces.requests.mail.IReq_EnqueueMail['response']; type TMailSubmissionErrorCode = plugins.servezoneInterfaces.data.TMailSubmissionErrorCode; type TMailDeliveryStatusResponse = plugins.servezoneInterfaces.requests.mail.IReq_GetMailDeliveryStatus['response']; type TMailEndpointRegistrationResponse = plugins.servezoneInterfaces.requests.mail.IReq_RegisterServiceMailEndpoint['response']; type TWorkAppMailBinding = plugins.servezoneInterfaces.data.IWorkAppMailBinding; type TCachedEmailWithTimestamps = CachedEmail & { createdAt?: Date | number | string; updatedAt?: Date | number | string; }; type TSmartMtaStatusQueueItem = { status?: 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred'; attempts?: number; nextAttempt?: Date; lastError?: string; smtpTransactions?: ICachedEmailSmtpTransaction[]; processingResult?: { headers?: Record; email?: { headers?: Record; }; }; }; type TCachedEmailRouteData = { session?: { user?: { username?: string; }; }; smartMta?: { status?: 'queued' | 'deferred' | 'delivered' | 'failed'; nextAttempt?: string; lastError?: string; updatedAt?: string; }; }; class MailSubmissionValidationError extends Error { constructor( public readonly code: TMailSubmissionErrorCode, messageArg: string, ) { super(messageArg); this.name = 'MailSubmissionValidationError'; } } /** * WorkApp-generated routes and SMTP users contributed to the composed email * runtime config. Consumed by SmtpAccountManager, the single owner of the * final `auth` block and route-list composition. */ export interface IWorkAppMailRuntimeContribution { routes: IEmailRoute[]; users: Array<{ username: string; password: string }>; } /** Route names generated (and owned wholesale) by WorkAppMailManager. */ export function isWorkAppManagedMailRouteName(routeName: string): boolean { return routeName.startsWith('workapp-mail-'); } /** SMTP usernames generated (and owned wholesale) by WorkAppMailManager. */ export function isWorkAppManagedSmtpUsername(username: string): boolean { return username.startsWith('workapp-'); } export class WorkAppMailManager { private readonly emailValidator = new plugins.smartmta.Core.EmailValidator(); private readonly gatewayMailDomainStore: IGatewayMailDomainStore; private identityMutationPromise: Promise = Promise.resolve(); constructor(private dcRouterRef: DcRouter) { this.gatewayMailDomainStore = dcRouterRef.gatewayMailDomainStore || new GatewayMailDomainStore(); } 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 { return await this.runIdentityMutationExclusive(async () => { return await this.syncMailIdentityInner(request, createdBy); }); } private async runIdentityMutationExclusive(actionArg: () => Promise): Promise { const actionPromise = this.identityMutationPromise.catch(() => undefined).then(actionArg); this.identityMutationPromise = actionPromise.then(() => undefined, () => undefined); return await actionPromise; } private async syncMailIdentityInner( 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); const persistedIdentities = await this.writeStoredIdentities(identities); await this.applyStoredIdentitiesToRuntime(persistedIdentities); 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 shouldRotateSmtpPassword = !existingIdentity || request.resetSmtpPassword; const smtpPassword = shouldRotateSmtpPassword ? this.generateSmtpPassword() : existingIdentity.smtpPassword; const requestedSmtpEnabled = request.smtpEnabled ?? existingIdentity?.smtp.enabled ?? true; const outboundReadiness = requestedSmtpEnabled ? await this.dcRouterRef.emailDomainManager?.getOutboundReadiness(domain) : { ready: false, reason: 'outbound identity disabled', edgeIds: [] }; 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, inboundTarget: this.resolveNextInboundTarget(request, existingIdentity), inbound: this.toLegacyInboundRouteIfSmtpForward(this.resolveNextInboundTarget(request, existingIdentity)), smtp: { enabled: requestedSmtpEnabled, username: existingIdentity?.smtp.username || this.buildSmtpUsername(externalKey), }, createdAt: existingIdentity?.createdAt || now, updatedAt: now, createdBy: existingIdentity?.createdBy || createdBy, smtpPassword, smtpLastRotatedAt: shouldRotateSmtpPassword ? now : existingIdentity?.smtpLastRotatedAt || existingIdentity?.createdAt || now, }; if (existingIndex >= 0) { identities[existingIndex] = identity; } else { identities.push(identity); } const persistedIdentities = await this.writeStoredIdentities(identities); await this.applyStoredIdentitiesToRuntime(persistedIdentities); const response: interfaces.data.IWorkAppMailIdentitySyncResult = { success: true, action: existingIndex >= 0 ? 'updated' : 'created', identity: this.toPublicIdentity(identity), }; if ((existingIndex < 0 || request.resetSmtpPassword) && outboundReadiness?.ready) { response.smtpCredentials = this.buildSmtpCredentials(identity); } if (requestedSmtpEnabled && !outboundReadiness?.ready) { response.message = `Outbound identity is persisted but remains gated: ${outboundReadiness?.reason || 'email domain is not active'}`; } return response; } public async listMailAddressBindings(options: { owner?: Partial; domain?: string; address?: string; } = {}): Promise { const domain = options.domain ? this.normalizeDomain(options.domain) : undefined; const address = options.address ? this.normalizeAddress(options.address) : undefined; const identities = await this.readStoredIdentities(); return identities .filter((identity) => this.matchesMailOwner(this.toMailOwner(identity.ownership), options.owner)) .filter((identity) => domain ? identity.domain === domain : true) .filter((identity) => address ? identity.address === address : true) .map((identity) => this.toMailAddressBinding(identity)); } public async listWorkAppMailBindings( owner?: Partial, ): Promise { const identities = (await this.readStoredIdentities()) .filter((identity) => this.matchesMailOwner(this.toMailOwner(identity.ownership), owner)); const groups = new Map(); for (const identity of identities) { const ownerKey = this.buildMailOwnerKey(this.toMailOwner(identity.ownership)); const group = groups.get(ownerKey) || []; group.push(identity); groups.set(ownerKey, group); } return Array.from(groups.values()).map((group) => this.toWorkAppMailBinding(group)); } public async countMailDomains( ownerArg: Pick, ): Promise { return await this.gatewayMailDomainStore.countDomains( normalizeGatewayMailDomainOwner(ownerArg), ); } public async syncMailAddressBinding( binding: TMailAddressBindingSync, createdBy: string, ): Promise { return await this.runIdentityMutationExclusive(async () => { const ownership = this.normalizeMailResourceOwner(binding.owner); const { localPart, domain } = this.normalizeMailAddressParts(binding); const syncRequest: TSyncRequest = { ownership, localPart, domain, inboundTarget: binding.inboundTarget, inbound: this.toLegacyInboundRouteIfSmtpForward(binding.inboundTarget), enabled: binding.enabled, }; if (binding.outboundEnabled !== undefined) { syncRequest.smtpEnabled = binding.outboundEnabled; } else if (binding.outboundIdentityId !== undefined) { syncRequest.smtpEnabled = Boolean(binding.outboundIdentityId); } const result = await this.syncMailIdentityInner(syncRequest, createdBy); const storedIdentity = result.identity ? (await this.readStoredIdentities()).find((identity) => identity.id === result.identity!.id) : undefined; return { success: result.success, binding: storedIdentity ? this.toMailAddressBinding(storedIdentity) : undefined, message: result.message, }; }); } public async rotateMailCredential( credentialId: string, createdBy: string, owner?: Partial, ): Promise { return await this.runIdentityMutationExclusive(async () => { return await this.rotateMailCredentialInner(credentialId, createdBy, owner); }); } private async rotateMailCredentialInner( credentialId: string, createdBy: string, owner?: Partial, ): Promise { if (!this.dcRouterRef.options.emailConfig) { return { success: false, message: 'Email server is not configured' }; } const normalizedCredentialId = credentialId?.trim(); if (!normalizedCredentialId) { return { success: false, message: 'credentialId is required' }; } const identities = await this.readStoredIdentities(); const identityIndex = identities.findIndex((identityArg) => { return this.matchesMailOwner(this.toMailOwner(identityArg.ownership), owner) && ( identityArg.id === normalizedCredentialId || identityArg.externalKey === normalizedCredentialId || identityArg.smtp.username === normalizedCredentialId ); }); if (identityIndex < 0) { return { success: false, message: 'Mail credential not found' }; } const identity = identities[identityIndex]; if (!identity.enabled || !identity.smtp.enabled) { return { success: false, message: 'Mail credential is disabled' }; } try { await this.assertIdentityOutboundReady(identity); } catch (error: unknown) { return { success: false, message: (error as Error).message }; } const now = Date.now(); identities[identityIndex] = { ...identity, smtpPassword: this.generateSmtpPassword(), smtpLastRotatedAt: now, updatedAt: now, }; const persistedIdentities = await this.writeStoredIdentities(identities); await this.applyStoredIdentitiesToRuntime(persistedIdentities); return { success: true, credential: this.toMailCredentialSecret( persistedIdentities.find((identityArg) => identityArg.id === identity.id)!, ), }; } public async enforceManagedSmtpSender( context: IMessageAcceptanceContext, ): Promise { const username = context.session.user?.username; if (!username || !this.isManagedSmtpUsername(username)) { return undefined; } const identities = await this.readStoredIdentities(); const identity = identities.find((identityArg) => identityArg.smtp.username === username); if (!identity || !identity.enabled || !identity.smtp.enabled) { return this.rejectSenderAuthorization('Managed SMTP credential is not active'); } try { await this.assertIdentityOutboundReady(identity); } catch (error: unknown) { return this.rejectSenderAuthorization((error as Error).message); } const envelopeFrom = this.normalizeMailboxAddress( context.session.envelope?.mailFrom?.address || context.session.mailFrom || '', ); const headerFrom = this.extractSingleHeaderFromAddress(context.rawMessage, context.email.from); if (!envelopeFrom || !headerFrom) { return this.rejectSenderAuthorization('Managed SMTP sender must use exactly one From address'); } if (envelopeFrom !== identity.address || headerFrom !== identity.address) { return this.rejectSenderAuthorization('Managed SMTP sender is not authorized for this From address'); } return undefined; } public async enqueueMail( authArg: plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth, messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload, outboundIdentityIdArg?: string, idempotencyKeyArg?: string, ): Promise { if (!this.dcRouterRef.emailServer) { return { accepted: false, message: 'Email server is not running' }; } try { const identity = await this.authenticateMailCredential( authArg, messageArg.from, outboundIdentityIdArg, ); await this.assertIdentityOutboundReady(identity); const idempotencyKey = this.normalizeMailIdempotencyKey(idempotencyKeyArg); const submissionDigest = idempotencyKey ? this.getMailSubmissionDigest(identity, messageArg) : undefined; const existingReplay = idempotencyKey ? await this.getMailSubmissionReplay(identity, idempotencyKey, submissionDigest!) : undefined; if (existingReplay) return existingReplay; const rawMessage = this.buildOutboundRawMessage(identity, messageArg); const recipients = this.getMessageRecipients(messageArg); const session = this.buildAuthenticatedSession(identity, messageArg.from, recipients, rawMessage); let acceptance; try { acceptance = await this.dcRouterRef.acceptedEmailSpool.acceptRawMessage({ rawMessage, envelope: { mailFrom: identity.address, rcptTo: recipients, }, session, messageId: this.extractHeader(rawMessage, 'message-id'), subject: messageArg.subject, ...(idempotencyKey ? { submissionCredentialId: identity.id, submissionIdempotencyKey: idempotencyKey, submissionDigest, } : {}), }); } catch (errorArg) { const racedReplay = idempotencyKey ? await this.getMailSubmissionReplay(identity, idempotencyKey, submissionDigest!) : undefined; if (racedReplay) return racedReplay; throw errorArg; } return { accepted: acceptance.accepted, spoolItemId: acceptance.spoolItemId, outboundIdentity: this.toMailOutboundIdentity(identity), message: acceptance.smtpMessage, }; } catch (error) { return { accepted: false, ...(error instanceof MailSubmissionValidationError ? { errorCode: error.code } : {}), message: (error as Error).message, }; } } private normalizeMailIdempotencyKey(valueArg?: string): string | undefined { if (valueArg === undefined) return undefined; if ( typeof valueArg !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._/-]{0,199}$/.test(valueArg) ) throw new Error('Mail idempotencyKey is invalid'); return valueArg; } private canonicalMailSubmissionJson(valueArg: unknown): string { if (valueArg === null || typeof valueArg === 'string' || typeof valueArg === 'boolean') { return JSON.stringify(valueArg); } if (typeof valueArg === 'number') { if (!Number.isFinite(valueArg)) throw new Error('Mail submission contains an invalid number'); return JSON.stringify(valueArg); } if (Array.isArray(valueArg)) { return `[${valueArg.map((itemArg) => this.canonicalMailSubmissionJson(itemArg)).join(',')}]`; } if (valueArg && typeof valueArg === 'object') { const entries = Object.entries(valueArg as Record) .filter(([, entryValue]) => entryValue !== undefined) .sort(([leftKey], [rightKey]) => leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0); return `{${entries.map(([keyArg, entryValue]) => ( `${JSON.stringify(keyArg)}:${this.canonicalMailSubmissionJson(entryValue)}` )).join(',')}}`; } throw new Error('Mail submission contains an unsupported value'); } private getMailSubmissionDigest( identityArg: IStoredWorkAppMailIdentity, messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload, ): string { const canonical = this.canonicalMailSubmissionJson({ identityId: identityArg.id, message: messageArg, version: 1, }); return `sha256:${plugins.crypto.createHash('sha256').update(canonical, 'utf8').digest('hex')}`; } private async getMailSubmissionReplay( identityArg: IStoredWorkAppMailIdentity, idempotencyKeyArg: string, submissionDigestArg: string, ): Promise { const existing = await CachedEmail.findBySubmissionIdempotencyKey( identityArg.id, idempotencyKeyArg, ); if (!existing) return undefined; if (existing.submissionDigest !== submissionDigestArg) { throw new Error('Mail idempotencyKey was reused with different message semantics'); } return { accepted: true, spoolItemId: existing.id, outboundIdentity: this.toMailOutboundIdentity(identityArg), message: 'Idempotent service-mail replay', }; } public async getMailDeliveryStatus( spoolItemIdArg: string, ): Promise { const cachedEmail = await this.getCachedEmailById(spoolItemIdArg); if (!cachedEmail) { return { attempts: [] }; } return await this.buildMailDeliveryStatus(cachedEmail); } public async getMailDeliveryStatusForCredential( authArg: plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth, spoolItemIdArg: string, ): Promise { const identity = await this.authenticateMailCredential(authArg); const cachedEmail = await this.getCachedEmailById(spoolItemIdArg); if (!cachedEmail) { return { attempts: [] }; } if (!this.cachedEmailBelongsToIdentity(cachedEmail, identity)) { throw new Error('Mail credential is not authorized for this spool item'); } return await this.buildMailDeliveryStatus(cachedEmail); } public async registerServiceMailEndpoint( authArg: plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth, peerArg: { tags?: Set }, addressesArg?: string[], ): Promise { if (!peerArg?.tags) { return { success: false, message: 'TypedSocket peer context is required' }; } try { const identity = await this.authenticateMailCredential(authArg); const requestedAddresses = addressesArg?.length ? addressesArg.map((addressArg) => this.normalizeAddress(addressArg)) : [identity.address]; const registeredAddresses: string[] = []; for (const address of requestedAddresses) { const addressIdentity = await this.authenticateMailCredential(authArg, address); peerArg.tags.add(this.buildServiceMailAddressTag(addressIdentity.address)); registeredAddresses.push(addressIdentity.address); } return { success: true, addresses: registeredAddresses }; } catch (error) { return { success: false, message: (error as Error).message }; } } public async deliverCachedEmailToTypedEndpoint( cachedEmailArg: CachedEmail, rawMessageArg: Buffer | plugins.buffer.Buffer, sessionArg: IExtendedSmtpSession, ): Promise { const identities = await this.readStoredIdentities(); const recipientIdentity = identities.find((identityArg) => { if (!identityArg.enabled) return false; const inboundTarget = this.getCanonicalInboundTarget(identityArg); return inboundTarget?.type === 'typedEndpoint' && cachedEmailArg.to.some((recipientArg) => this.normalizeAddress(recipientArg) === identityArg.address); }); if (!recipientIdentity) return false; const inboundTarget = this.getCanonicalInboundTarget(recipientIdentity)!; const typedEndpoint = inboundTarget.typedEndpoint; if (!typedEndpoint?.method) { throw new Error(`Typed mail endpoint method is missing for ${recipientIdentity.address}`); } const typedsocket = this.dcRouterRef.opsServer?.server?.typedserver?.typedsocket; if (!typedsocket) { throw new Error('OpsServer TypedSocket is not available for inbound mail delivery'); } const connection = await typedsocket.findTargetConnectionByTag( this.buildServiceMailAddressTag(recipientIdentity.address) as any, ); if (!connection) { throw new Error(`No registered typed mail endpoint for ${recipientIdentity.address}`); } const rawMessage = plugins.buffer.Buffer.isBuffer(rawMessageArg) ? rawMessageArg.toString('utf8') : plugins.buffer.Buffer.from(rawMessageArg).toString('utf8'); const delivery: plugins.servezoneInterfaces.data.IMailInboundMessagePayload = { spoolItemId: cachedEmailArg.id, owner: this.toMailOwner(recipientIdentity.ownership), envelope: { mailFrom: cachedEmailArg.from, rcptTo: cachedEmailArg.to, }, rawMessage, sizeBytes: plugins.buffer.Buffer.byteLength(rawMessage), messageId: cachedEmailArg.messageId, subject: cachedEmailArg.subject, headers: this.extractHeaders(rawMessage), source: { protocol: sessionArg.authenticated ? 'submission' : 'smtp', remoteAddress: sessionArg.remoteAddress, heloName: sessionArg.clientHostname, tls: !!sessionArg.secure, authUsername: sessionArg.user?.username, }, receivedAt: Date.now(), }; const request = typedsocket.createTypedRequest( typedEndpoint.method as 'deliverInboundMail', connection, ); const result = await request.fire({ auth: {}, delivery, }); if (!result.accepted) { throw new Error(result.message || `Typed mail endpoint rejected ${cachedEmailArg.id}`); } return true; } public async deleteMailAddressBinding( id: string, createdBy: string, ): Promise { return await this.runIdentityMutationExclusive(async () => { return await this.deleteMailAddressBindingInner(id, createdBy); }); } private async deleteMailAddressBindingInner( id: string, createdBy: string, ): Promise { const identities = await this.readStoredIdentities(); const identity = identities.find((storedIdentity) => storedIdentity.id === id || storedIdentity.externalKey === id); if (!identity) { return { success: true }; } const result = await this.syncMailIdentityInner({ ownership: identity.ownership, localPart: identity.localPart, domain: identity.domain, delete: true, }, createdBy); return { success: result.success, message: result.message, }; } /** * Compute the workapp-generated routes and SMTP users for the given * candidate email config. Pure contribution — the final composition of * routes and the `auth` block is owned exclusively by SmtpAccountManager. */ public async getStoredIdentityContribution( emailConfig: IUnifiedEmailServerOptions, identities = undefined as IStoredWorkAppMailIdentity[] | undefined, ): Promise { const nextIdentities = identities || await this.readStoredIdentities(); const readyDomains = await this.getReadyOutboundDomains(nextIdentities); const generatedRoutes = nextIdentities .filter((identity) => { const inboundTarget = this.getCanonicalInboundTarget(identity); return identity.enabled && ['smtpForward', 'typedEndpoint'].includes(inboundTarget?.type || ''); }) .map((identity) => this.buildInboundRoute(identity)); const generatedOutboundRoutes = nextIdentities .filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain)) .map((identity) => this.buildOutboundRoute(identity, emailConfig)); const generatedUsers = nextIdentities .filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain)) .map((identity) => ({ username: identity.smtp.username, password: identity.smtpPassword, })); return { routes: [...generatedRoutes, ...generatedOutboundRoutes], users: generatedUsers, }; } public async applyStoredIdentitiesToRuntime( identities = undefined as IStoredWorkAppMailIdentity[] | undefined, ): Promise { const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined; if (!emailConfig) return; const contribution = await this.getStoredIdentityContribution(emailConfig, identities); await this.dcRouterRef.smtpAccountManager.applyToRuntime(contribution); } private async readStoredIdentities(): Promise { return await this.gatewayMailDomainStore.readAllIdentities(); } private async writeStoredIdentities( identities: IStoredWorkAppMailIdentity[], ): Promise { return await this.gatewayMailDomainStore.writeAllIdentities(identities); } private buildInboundRoute(identity: IStoredWorkAppMailIdentity): IEmailRoute { const inboundTarget = this.getCanonicalInboundTarget(identity); if (inboundTarget?.type === 'typedEndpoint') { return { name: this.buildRouteName(identity.externalKey), priority: 1000, match: { recipients: identity.address, }, action: { type: 'process', process: { scan: true, queue: 'normal', }, }, }; } const smtpForward = inboundTarget?.smtpForward; if (inboundTarget?.type !== 'smtpForward' || !smtpForward) { throw new Error(`SMTP forward target is missing for ${identity.address}`); } return { name: this.buildRouteName(identity.externalKey), priority: 1000, match: { recipients: identity.address, }, action: { type: 'forward', forward: { host: smtpForward.host, port: smtpForward.port, preserveHeaders: smtpForward.preserveHeaders ?? true, addHeaders: { 'X-Dcrouter-WorkHoster-Type': identity.ownership.workHosterType, 'X-Dcrouter-WorkHoster-Id': identity.ownership.workHosterId, 'X-Dcrouter-WorkApp-Id': identity.ownership.workAppId, ...(smtpForward.addHeaders || {}), }, }, }, }; } private buildOutboundRoute( identity: IStoredWorkAppMailIdentity, emailConfig: IUnifiedEmailServerOptions, ): IEmailRoute { return { name: this.buildOutboundRouteName(identity.externalKey), priority: 900, match: { authenticated: true, senders: identity.address, }, action: { type: 'process', allowRelay: true, process: { dkim: true, queue: 'normal', }, options: { mtaOptions: { dkimSign: true, dkimOptions: { domainName: identity.domain, keySelector: this.getActiveDkimSelector(identity.domain, emailConfig), }, }, }, }, }; } 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 async getReadyOutboundDomains( identities: IStoredWorkAppMailIdentity[], ): Promise> { const ready = new Set(); const requestedDomains = [...new Set( identities .filter((identity) => identity.enabled && identity.smtp.enabled) .map((identity) => identity.domain), )]; for (const domain of requestedDomains) { const readiness = await this.dcRouterRef.emailDomainManager?.getOutboundReadiness(domain); if (readiness?.ready) ready.add(domain); } return ready; } private getActiveDkimSelector( domain: string, emailConfig: IUnifiedEmailServerOptions = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions, ): string { return emailConfig?.domains ?.find((domainConfig) => domainConfig.domain.toLowerCase() === domain.toLowerCase()) ?.dkim?.selector || 'default'; } private async assertIdentityOutboundReady(identity: IStoredWorkAppMailIdentity): Promise { const readiness = await this.dcRouterRef.emailDomainManager?.getOutboundReadiness(identity.domain); if (!readiness?.ready) { throw new Error( `Outbound mail for ${identity.domain} is gated until DNS and egress identity validation succeeds: ${readiness?.reason || 'EmailDomainManager unavailable'}`, ); } } 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 { return normalizeGatewayMailDomain(domain); } 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 normalizeAddress(address: string): string { const normalized = address?.trim().toLowerCase(); const [localPart, domain, extra] = normalized?.split('@') || []; if (!localPart || !domain || extra) { throw new Error(`Invalid email address: ${address}`); } return `${this.normalizeLocalPart(localPart)}@${this.normalizeDomain(domain)}`; } private normalizeMailboxAddress(addressArg: string): string | undefined { const trimmed = addressArg?.trim(); if (!trimmed || trimmed === '<>') return undefined; const angleMatch = trimmed.match(/<([^<>]+)>/); const candidate = (angleMatch ? angleMatch[1] : trimmed).replace(/^mailto:/i, '').trim(); try { return this.normalizeAddress(candidate); } catch { return undefined; } } private extractSingleHeaderFromAddress(rawMessageArg: Buffer, parsedFromArg: string): string | undefined { const headerSlice = rawMessageArg.subarray(0, 64 * 1024); const crlfHeaderEnd = headerSlice.indexOf('\r\n\r\n'); const lfHeaderEnd = headerSlice.indexOf('\n\n'); const headerEnd = crlfHeaderEnd >= 0 ? crlfHeaderEnd : lfHeaderEnd >= 0 ? lfHeaderEnd : headerSlice.length; const headersText = headerSlice.subarray(0, headerEnd).toString('utf8'); const unfoldedLines: string[] = []; for (const line of headersText.split(/\r?\n/)) { if (/^[\t ]/.test(line) && unfoldedLines.length > 0) { unfoldedLines[unfoldedLines.length - 1] += ` ${line.trim()}`; } else { unfoldedLines.push(line); } } const fromHeaders = unfoldedLines.filter((line) => /^from\s*:/i.test(line)); if (fromHeaders.length > 1) return undefined; const headerValue = fromHeaders.length === 1 ? fromHeaders[0].replace(/^from\s*:/i, '').trim() : parsedFromArg; if (!headerValue || headerValue.includes(',')) return undefined; return this.normalizeMailboxAddress(headerValue); } private rejectSenderAuthorization(messageArg: string): IMessageAcceptanceDecision { return { accepted: false, smtpCode: 553, smtpMessage: messageArg, }; } private normalizeMailResourceOwner(owner: TMailResourceOwner): interfaces.data.IWorkAppMailOwnership { const gatewayClientType = owner.gatewayClientType; const gatewayClientId = owner.gatewayClientId?.trim(); const appInstanceId = owner.appInstanceId?.trim(); if (gatewayClientType !== 'onebox' && gatewayClientType !== 'cloudly' && gatewayClientType !== 'custom') { throw new Error(`Invalid gateway client type: ${gatewayClientType}`); } if (!gatewayClientId) throw new Error('gatewayClientId is required'); if (!appInstanceId) throw new Error('appInstanceId is required'); return { workHosterType: gatewayClientType as interfaces.data.TGatewayClientType, workHosterId: gatewayClientId, workAppId: appInstanceId, }; } private normalizeMailAddressParts(binding: TMailAddressBindingSync): { localPart: string; domain: string; } { const localPart = this.normalizeLocalPart(binding.localPart); const domain = this.normalizeDomain(binding.domain); const address = this.normalizeAddress(binding.address); if (address !== `${localPart}@${domain}`) { throw new Error('mail address, localPart, and domain do not match'); } return { localPart, domain }; } private toLegacyInboundRoute( inboundTarget?: TMailAddressBinding['inboundTarget'], ): interfaces.data.IWorkAppMailInboundRoute | undefined { if (!inboundTarget) return undefined; if (inboundTarget.type !== 'smtpForward' || !inboundTarget.smtpForward) { throw new Error(`Unsupported WorkApp mail inbound target: ${inboundTarget.type}`); } return this.normalizeInboundRoute({ enabled: true, targetHost: inboundTarget.smtpForward.host, targetPort: inboundTarget.smtpForward.port, preserveHeaders: inboundTarget.smtpForward.preserveHeaders, addHeaders: inboundTarget.smtpForward.addHeaders, }); } 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 toLegacyInboundRouteIfSmtpForward( inboundTarget?: plugins.servezoneInterfaces.data.IMailInboundTarget, ): interfaces.data.IWorkAppMailInboundRoute | undefined { if (!inboundTarget || inboundTarget.type !== 'smtpForward') return undefined; return this.toLegacyInboundRoute(inboundTarget); } private resolveNextInboundTarget( request: TSyncRequest, existingIdentity?: IStoredWorkAppMailIdentity, ): plugins.servezoneInterfaces.data.IMailInboundTarget | undefined { if (request.inboundTarget !== undefined) { return this.normalizeInboundTarget(request.inboundTarget); } if (request.inbound !== undefined) { const inbound = this.normalizeInboundRoute(request.inbound); return inbound ? this.toMailInboundTarget(inbound) : undefined; } return this.normalizeInboundTarget( existingIdentity?.inboundTarget || this.toMailInboundTarget(existingIdentity?.inbound), ); } private normalizeInboundTarget( inboundTarget?: plugins.servezoneInterfaces.data.IMailInboundTarget, ): plugins.servezoneInterfaces.data.IMailInboundTarget | undefined { if (!inboundTarget) return undefined; if (inboundTarget.type === 'smtpForward') { const smtpForward = inboundTarget.smtpForward; if (!smtpForward?.host) throw new Error('smtpForward.host is required'); const port = Number(smtpForward.port); if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`Invalid smtpForward.port: ${smtpForward.port}`); } return { type: 'smtpForward', smtpForward: { ...smtpForward, host: smtpForward.host.trim(), port, }, }; } if (inboundTarget.type === 'typedEndpoint') { const typedEndpoint = inboundTarget.typedEndpoint; return { type: 'typedEndpoint', typedEndpoint: { method: typedEndpoint?.method?.trim() || 'deliverInboundMail', ...(typedEndpoint?.serviceRef ? { serviceRef: typedEndpoint.serviceRef } : {}), ...(typedEndpoint?.queueName ? { queueName: typedEndpoint.queueName } : {}), ...(typedEndpoint?.timeoutMs !== undefined ? { timeoutMs: typedEndpoint.timeoutMs } : {}), }, }; } throw new Error(`Unsupported WorkApp mail inbound target: ${inboundTarget.type}`); } private getCanonicalInboundTarget( identity: Pick, ): plugins.servezoneInterfaces.data.IMailInboundTarget | undefined { return this.normalizeInboundTarget(identity.inboundTarget || this.toMailInboundTarget(identity.inbound)); } private async getCachedEmailById(spoolItemIdArg: string): Promise { const spoolItemId = String(spoolItemIdArg || '').trim(); if (!spoolItemId || !this.dcRouterRef.dcRouterDb?.isReady()) { return null; } return await CachedEmail.findById(spoolItemId); } private async buildMailDeliveryStatus( cachedEmailArg: CachedEmail, ): Promise { const queueItem = this.findQueueItemForCachedEmail(cachedEmailArg.id); const routeData = this.parseCachedEmailRouteData(cachedEmailArg); const owner = await this.resolveCachedEmailOwner(cachedEmailArg); const status = this.mapMailSpoolStatus(cachedEmailArg, queueItem); const cachedEmailWithTimestamps = cachedEmailArg as TCachedEmailWithTimestamps; const createdAt = this.toTimestamp(cachedEmailWithTimestamps.createdAt) || Date.now(); const updatedAt = this.toTimestamp(cachedEmailWithTimestamps.updatedAt) || createdAt; const nextAttemptAt = status === 'delivered' || status === 'failed' ? undefined : this.toTimestamp(queueItem?.nextAttempt) || this.toTimestamp(routeData.smartMta?.nextAttempt) || this.toTimestamp(cachedEmailArg.nextAttempt); const lastError = queueItem?.lastError || cachedEmailArg.lastError || routeData.smartMta?.lastError || undefined; return { spoolItem: { id: cachedEmailArg.id, owner, // The persisted field is the single source of truth: it is derived from // the recipients at acceptance, whereas re-deriving from the session // username here reproduced the "authenticated means outbound" mistake. direction: cachedEmailArg.direction, status, envelope: { mailFrom: cachedEmailArg.from, rcptTo: cachedEmailArg.to || [], }, messageId: cachedEmailArg.messageId, subject: cachedEmailArg.subject, target: this.getCachedEmailSessionUsername(cachedEmailArg) ? { type: 'mtaRelay', label: 'SmartMTA outbound relay' } : undefined, attempts: Math.max(cachedEmailArg.attempts || 0, queueItem?.attempts || 0), nextAttemptAt, lastError, createdAt, updatedAt, }, attempts: this.mapDeliveryAttempts( queueItem?.smtpTransactions || cachedEmailArg.smtpTransactions || [], cachedEmailArg.id, ), }; } private mapDeliveryAttempts( transactionsArg: ICachedEmailSmtpTransaction[], spoolItemIdArg: string, ): plugins.servezoneInterfaces.data.IMailDeliveryAttempt[] { return transactionsArg.map((transaction) => ({ id: transaction.id, spoolItemId: spoolItemIdArg, target: { type: 'mtaRelay', label: transaction.recipientDomain || `${transaction.targetHost}:${transaction.targetPort}`, host: transaction.targetHost, port: transaction.targetPort, }, status: transaction.outcome === 'succeeded' ? 'delivered' : transaction.retryable ? 'deferred' : 'failed', startedAt: this.toTimestamp(transaction.startedAt) || 0, completedAt: this.toTimestamp(transaction.completedAt), smtpCode: transaction.smtpCode, response: transaction.recipientResults?.map((result) => { return `${result.recipient}: ${result.responseCode} ${result.accepted ? 'accepted' : 'rejected'}`; }).join('; '), error: transaction.error, })); } private findQueueItemForCachedEmail(cachedEmailIdArg: string): TSmartMtaStatusQueueItem | undefined { const emailServer = this.dcRouterRef.emailServer; if (!emailServer?.getQueueItems) { return undefined; } return (emailServer.getQueueItems() as TSmartMtaStatusQueueItem[]).find((itemArg) => { return this.getQueueItemCachedEmailId(itemArg) === cachedEmailIdArg; }); } private getQueueItemCachedEmailId(itemArg: TSmartMtaStatusQueueItem): string | undefined { const headerName = 'x-dcrouter-cached-email-id'; const headers = itemArg?.processingResult?.headers || itemArg?.processingResult?.email?.headers; if (!headers) { return undefined; } const matchingHeaderName = Object.keys(headers).find((keyArg) => keyArg.toLowerCase() === headerName); return matchingHeaderName ? headers[matchingHeaderName] : undefined; } private async resolveCachedEmailOwner( cachedEmailArg: CachedEmail, ): Promise { const username = this.getCachedEmailSessionUsername(cachedEmailArg); if (!username) { return undefined; } const identity = (await this.readStoredIdentities()).find((identityArg) => identityArg.smtp.username === username); return identity ? this.toMailOwner(identity.ownership) : undefined; } private cachedEmailBelongsToIdentity( cachedEmailArg: CachedEmail, identityArg: IStoredWorkAppMailIdentity, ): boolean { return this.normalizeAddress(cachedEmailArg.from || '') === identityArg.address && this.getCachedEmailSessionUsername(cachedEmailArg) === identityArg.smtp.username; } private getCachedEmailSessionUsername(cachedEmailArg: CachedEmail): string | undefined { const routeData = this.parseCachedEmailRouteData(cachedEmailArg); const username = routeData.session?.user?.username; return typeof username === 'string' && username ? username : undefined; } private parseCachedEmailRouteData(cachedEmailArg: CachedEmail): TCachedEmailRouteData { try { return cachedEmailArg.routeData ? JSON.parse(cachedEmailArg.routeData) : {}; } catch { return {}; } } private mapMailSpoolStatus( cachedEmailArg: CachedEmail, queueItemArg?: TSmartMtaStatusQueueItem, ): plugins.servezoneInterfaces.data.TMailSpoolStatus { if (!queueItemArg && cachedEmailArg.status === 'pending') { return 'accepted'; } const status = queueItemArg?.status || cachedEmailArg.status; switch (status) { case 'processing': return 'delivering'; case 'delivered': return 'delivered'; case 'deferred': return 'deferred'; case 'failed': return 'failed'; case 'pending': case 'queued': default: return 'queued'; } } private toTimestamp(valueArg: unknown): number | undefined { if (!valueArg) { return undefined; } if (typeof valueArg === 'number' && Number.isFinite(valueArg)) { return valueArg; } const timestamp = valueArg instanceof Date ? valueArg.getTime() : new Date(String(valueArg)).getTime(); return Number.isFinite(timestamp) ? timestamp : undefined; } private async authenticateMailCredential( authArg: plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth, addressArg?: string, credentialIdArg?: string, ): Promise { const credentialId = (credentialIdArg || authArg.credentialId || '').trim(); const credentialSecret = authArg.credentialSecret || ''; if (!credentialId || !credentialSecret) { throw new Error('credentialId and credentialSecret are required'); } const normalizedAddress = addressArg ? this.normalizeAddress(addressArg) : undefined; const identities = await this.readStoredIdentities(); const identity = identities.find((identityArg) => { return identityArg.id === credentialId || identityArg.externalKey === credentialId || identityArg.smtp.username === credentialId; }); if (!identity || !identity.enabled || !identity.smtp.enabled) { throw new Error('Mail credential is not active'); } if (identity.smtpPassword !== credentialSecret) { throw new Error('Mail credential secret is invalid'); } if (normalizedAddress && identity.address !== normalizedAddress) { throw new Error('Mail credential is not authorized for this address'); } return identity; } private buildAuthenticatedSession( identityArg: IStoredWorkAppMailIdentity, fromArg: string, recipientsArg: string[], rawMessageArg: string, ): IExtendedSmtpSession { return { id: `typed-${plugins.crypto.randomUUID()}`, state: 'DATA' as IExtendedSmtpSession['state'], mailFrom: fromArg, rcptTo: recipientsArg, emailData: rawMessageArg, useTLS: true, connectionEnded: false, remoteAddress: '127.0.0.1', clientHostname: 'typed-service-mail', secure: true, authenticated: true, user: { username: identityArg.smtp.username, }, envelope: { mailFrom: { address: fromArg, args: {} }, rcptTo: recipientsArg.map((recipientArg) => ({ address: recipientArg, args: {} })), }, } as IExtendedSmtpSession; } private buildOutboundRawMessage( identityArg: IStoredWorkAppMailIdentity, messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload, ): string { const from = this.normalizeAddress(messageArg.from); if (from !== identityArg.address) { throw new Error('Mail credential is not authorized for this From address'); } if (!messageArg.text && !messageArg.html) { throw new Error('Either text or html is required'); } const customReplyToHeader = Object.keys(messageArg.headers || {}) .find((keyArg) => keyArg.toLowerCase() === 'reply-to'); if (messageArg.replyTo !== undefined && customReplyToHeader) { throw new MailSubmissionValidationError( 'REPLY_TO_FIELD_HEADER_CONFLICT', 'replyTo cannot be combined with a custom Reply-To header', ); } const replyTo = messageArg.replyTo === undefined ? undefined : this.validateReplyTo(messageArg.replyTo); const messageId = `<${plugins.crypto.randomUUID()}@${identityArg.domain}>`; const headers: Record = { From: identityArg.address, To: messageArg.to.join(', '), ...(messageArg.cc?.length ? { Cc: messageArg.cc.join(', ') } : {}), ...(replyTo ? { 'Reply-To': replyTo } : {}), Subject: messageArg.subject, Date: new Date().toUTCString(), 'Message-ID': messageId, 'MIME-Version': '1.0', }; for (const [keyArg, valueArg] of Object.entries(messageArg.headers || {})) { const headerKey = this.sanitizeHeaderKey(keyArg); if (this.isProtectedOutboundHeader(headerKey)) { throw new Error(`Custom mail header is not allowed to override ${headerKey}`); } headers[headerKey] = String(valueArg); } const body = this.buildMimeBody(messageArg, headers); return `${Object.entries(headers) .map(([keyArg, valueArg]) => `${this.sanitizeHeaderKey(keyArg)}: ${this.sanitizeHeaderValue(valueArg)}`) .join('\r\n')}\r\n\r\n${body}`; } private buildMimeBody( messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload, headersArg: Record, ): string { const attachments = messageArg.attachments || []; if (attachments.length === 0) { return this.buildTextHtmlPart(messageArg, headersArg); } const mixedBoundary = `mixed-${plugins.crypto.randomBytes(12).toString('hex')}`; headersArg['Content-Type'] = `multipart/mixed; boundary="${mixedBoundary}"`; const parts = [`--${mixedBoundary}\r\n${this.buildBodyPart(messageArg)}`]; for (const attachment of attachments) { parts.push(`--${mixedBoundary}\r\n${this.buildAttachmentPart(attachment)}`); } parts.push(`--${mixedBoundary}--`); return parts.join('\r\n'); } private buildTextHtmlPart( messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload, headersArg: Record, ): string { if (messageArg.text && messageArg.html) { const boundary = `alternative-${plugins.crypto.randomBytes(12).toString('hex')}`; headersArg['Content-Type'] = `multipart/alternative; boundary="${boundary}"`; return [ `--${boundary}`, 'Content-Type: text/plain; charset=utf-8', 'Content-Transfer-Encoding: 8bit', '', this.normalizeNewlines(messageArg.text), `--${boundary}`, 'Content-Type: text/html; charset=utf-8', 'Content-Transfer-Encoding: 8bit', '', this.normalizeNewlines(messageArg.html), `--${boundary}--`, ].join('\r\n'); } headersArg['Content-Type'] = `${messageArg.html ? 'text/html' : 'text/plain'}; charset=utf-8`; headersArg['Content-Transfer-Encoding'] = '8bit'; return this.normalizeNewlines(messageArg.html || messageArg.text || ''); } private buildBodyPart(messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload): string { const headers: Record = {}; const body = this.buildTextHtmlPart(messageArg, headers); return `${Object.entries(headers).map(([keyArg, valueArg]) => `${keyArg}: ${valueArg}`).join('\r\n')}\r\n\r\n${body}`; } private buildAttachmentPart(attachmentArg: plugins.servezoneInterfaces.data.IMailOutboundAttachmentPayload): string { const filename = this.sanitizeHeaderValue(attachmentArg.filename); return [ `Content-Type: ${attachmentArg.contentType || 'application/octet-stream'}; name="${filename}"`, 'Content-Transfer-Encoding: base64', `Content-Disposition: attachment; filename="${filename}"`, '', this.wrapBase64(attachmentArg.binaryAttachmentString), ].join('\r\n'); } private getMessageRecipients(messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload): string[] { const recipients = [ ...(messageArg.to || []), ...(messageArg.cc || []), ...(messageArg.bcc || []), ].map((recipientArg) => this.normalizeAddress(recipientArg)); if (recipients.length === 0) { throw new Error('At least one mail recipient is required'); } return recipients; } private toMailOutboundIdentity( identityArg: IStoredWorkAppMailIdentity, ): plugins.servezoneInterfaces.data.IMailOutboundIdentity { return { id: identityArg.smtp.username, owner: this.toMailOwner(identityArg.ownership), enabled: identityArg.enabled && identityArg.smtp.enabled, status: identityArg.enabled && identityArg.smtp.enabled ? 'active' : 'disabled', allowedFromPatterns: [{ type: 'exactAddress', domain: identityArg.domain, localPart: identityArg.localPart }], defaultFrom: identityArg.address, domain: identityArg.domain, dkimSelector: this.getActiveDkimSelector(identityArg.domain), credential: this.toMailCredentialPublic(identityArg), createdAt: identityArg.createdAt, updatedAt: identityArg.updatedAt, createdBy: identityArg.createdBy, }; } private extractHeaders(rawMessageArg: string): Record { const headers: Record = {}; const headerText = rawMessageArg.split(/\r?\n\r?\n/, 1)[0] || ''; let currentHeader = ''; for (const line of headerText.split(/\r?\n/)) { if (/^[\t ]/.test(line) && currentHeader) { headers[currentHeader] = `${headers[currentHeader] || ''} ${line.trim()}`; continue; } const separatorIndex = line.indexOf(':'); if (separatorIndex < 1) continue; currentHeader = line.slice(0, separatorIndex).trim(); const value = line.slice(separatorIndex + 1).trim(); const existing = headers[currentHeader]; if (Array.isArray(existing)) { existing.push(value); } else if (existing) { headers[currentHeader] = [String(existing), value]; } else { headers[currentHeader] = value; } } return headers; } private extractHeader(rawMessageArg: string, headerNameArg: string): string | undefined { const lowerName = `${headerNameArg.toLowerCase()}:`; for (const line of rawMessageArg.split(/\r?\n/)) { if (!line) return undefined; if (line.toLowerCase().startsWith(lowerName)) { return line.slice(lowerName.length).trim(); } } } private sanitizeHeaderKey(valueArg: string): string { if (!/^[a-z0-9-]+$/i.test(valueArg)) { throw new Error(`Invalid header key: ${valueArg}`); } return valueArg; } private isProtectedOutboundHeader(valueArg: string): boolean { return new Set([ 'bcc', 'cc', 'content-disposition', 'content-transfer-encoding', 'content-type', 'date', 'dkim-signature', 'from', 'message-id', 'mime-version', 'received', 'reply-to', 'resent-from', 'resent-sender', 'return-path', 'sender', 'subject', 'to', ]).has(valueArg.toLowerCase()); } private validateReplyTo(valueArg: unknown): string { const invalid = (): never => { throw new MailSubmissionValidationError( 'INVALID_REPLY_TO', 'replyTo must be one bare printable ASCII mailbox address', ); }; if ( typeof valueArg !== 'string' || valueArg.length === 0 || valueArg.length > 254 || !/^[\x21-\x7e]+$/.test(valueArg) || /[(),:;<>"[\]\\]/.test(valueArg) ) { return invalid(); } const parts = valueArg.split('@'); if (parts.length !== 2) return invalid(); const [localPart, domain] = parts; if (!localPart || localPart.length > 64 || !domain) return invalid(); const domainLabels = domain.split('.'); if ( domainLabels.some((labelArg) => ( labelArg.length === 0 || labelArg.length > 63 || !/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(labelArg) )) || !this.emailValidator.isValidFormat(valueArg) ) { return invalid(); } return valueArg; } private sanitizeHeaderValue(valueArg: string): string { return String(valueArg).replace(/[\r\n]+/g, ' ').trim(); } private normalizeNewlines(valueArg: string): string { return valueArg.replace(/\r?\n/g, '\r\n'); } private wrapBase64(valueArg: string): string { return valueArg.match(/.{1,76}/g)?.join('\r\n') || ''; } private buildServiceMailAddressTag(addressArg: string): string { return `serviceMailAddress:${this.normalizeAddress(addressArg)}`; } 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 matchesMailOwner( owner: TMailResourceOwner, filter?: Partial, ): boolean { if (!filter) return true; if (filter.gatewayClientType && filter.gatewayClientType !== owner.gatewayClientType) return false; if (filter.gatewayClientId && filter.gatewayClientId !== owner.gatewayClientId) return false; if (filter.appInstanceId && filter.appInstanceId !== owner.appInstanceId) 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 buildMailOwnerKey(owner: TMailResourceOwner): string { return [ owner.gatewayClientType, owner.gatewayClientId, owner.appInstanceId, ].join(':'); } private buildRouteName(externalKey: string): string { return `workapp-mail-${this.hashExternalKey(externalKey).slice(0, 32)}`; } private buildOutboundRouteName(externalKey: string): string { return `workapp-mail-outbound-${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 isManagedSmtpUsername(username: string): boolean { return isWorkAppManagedSmtpUsername(username); } 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 toMailCredentialPublic( identity: IStoredWorkAppMailIdentity, ): plugins.servezoneInterfaces.data.IMailCredentialPublic { return { id: identity.smtp.username, type: 'smtp', status: identity.enabled && identity.smtp.enabled ? 'active' : 'disabled', username: identity.smtp.username, scopes: [`from:${identity.address}`], createdAt: identity.createdAt, updatedAt: identity.updatedAt, lastRotatedAt: identity.smtpLastRotatedAt || identity.createdAt, }; } private toMailCredentialSecret( identity: IStoredWorkAppMailIdentity, ): plugins.servezoneInterfaces.data.IMailCredentialOneTimeSecret { return { credential: this.toMailCredentialPublic(identity), secret: identity.smtpPassword, secretShownOnce: true, }; } private toMailOwner(ownership: interfaces.data.IWorkAppMailOwnership): TMailResourceOwner & { appInstanceId: string } { return { gatewayClientType: ownership.workHosterType, gatewayClientId: ownership.workHosterId, appInstanceId: ownership.workAppId, }; } private toMailInboundTarget( inbound?: interfaces.data.IWorkAppMailInboundRoute, ): TMailAddressBinding['inboundTarget'] { if (!inbound?.enabled) return undefined; return { type: 'smtpForward', smtpForward: { host: inbound.targetHost, port: inbound.targetPort, preserveHeaders: inbound.preserveHeaders, addHeaders: inbound.addHeaders, }, }; } private toMailAddressBinding( identity: interfaces.data.IWorkAppMailIdentity, ): TMailAddressBinding { return { id: identity.id, owner: this.toMailOwner(identity.ownership), address: identity.address, localPart: identity.localPart, domain: identity.domain, enabled: identity.enabled, status: identity.enabled ? 'active' : 'disabled', inboundTarget: this.getCanonicalInboundTarget(identity as IStoredWorkAppMailIdentity), outboundIdentityId: identity.smtp.enabled ? identity.smtp.username : undefined, ...(identity.smtp.enabled ? { outboundCredential: this.toMailCredentialPublic(identity as IStoredWorkAppMailIdentity), } : {}), recipientPolicy: { mode: 'staticList', staticRecipients: [identity.address], }, createdAt: identity.createdAt, updatedAt: identity.updatedAt, createdBy: identity.createdBy, } as TMailAddressBinding; } private toWorkAppMailBinding( identities: IStoredWorkAppMailIdentity[], ): TWorkAppMailBinding { const [firstIdentity] = identities; const owner = this.toMailOwner(firstIdentity.ownership); const enabledIdentities = identities.filter((identity) => identity.enabled); const smtpIdentities = identities.filter((identity) => identity.smtp.enabled); return { id: `workapp-mail-${this.hashExternalKey(this.buildMailOwnerKey(owner)).slice(0, 32)}`, owner, enabled: enabledIdentities.length > 0, status: enabledIdentities.length > 0 ? 'active' : 'disabled', addressBindingIds: identities.map((identity) => identity.id), outboundIdentityIds: smtpIdentities.map((identity) => identity.smtp.username), defaultFrom: enabledIdentities[0]?.address || firstIdentity.address, inboundTarget: identities.length === 1 ? this.getCanonicalInboundTarget(firstIdentity) : undefined, createdAt: Math.min(...identities.map((identity) => identity.createdAt)), updatedAt: Math.max(...identities.map((identity) => identity.updatedAt)), createdBy: firstIdentity.createdBy, }; } private toPublicIdentity( identity: IStoredWorkAppMailIdentity, ): interfaces.data.IWorkAppMailIdentity { const { smtpPassword, smtpLastRotatedAt, ...publicIdentity } = identity; return publicIdentity; } }