import * as plugins from '../plugins.js'; import type { IWorkAppMailInboundRoute, IWorkAppMailOwnership, } from '../../ts_interfaces/data/workhoster.js'; import type { TGatewayClientType } from '../../ts_interfaces/data/route-management.js'; export interface IStoredWorkAppMailIdentity { id: string; externalKey: string; ownership: IWorkAppMailOwnership; address: string; localPart: string; domain: string; enabled: boolean; displayName?: string; inbound?: IWorkAppMailInboundRoute; inboundTarget?: plugins.servezoneInterfaces.data.IMailInboundTarget; smtp: { enabled: boolean; username: string; }; createdAt: number; updatedAt: number; createdBy: string; smtpPassword: string; smtpLastRotatedAt?: number; } export interface IGatewayMailDomainOwner { gatewayClientType: TGatewayClientType; gatewayClientId: string; } export interface IGatewayMailDomainGroup extends IGatewayMailDomainOwner { id: string; domain: string; identities: IStoredWorkAppMailIdentity[]; } const gatewayClientTypes = new Set(['onebox', 'cloudly', 'custom']); const requireTrimmedString = (valueArg: unknown, fieldNameArg: string): string => { const value = typeof valueArg === 'string' ? valueArg.trim() : ''; if (!value) throw new Error(`${fieldNameArg} is required`); return value; }; const requireTimestamp = (valueArg: unknown, fieldNameArg: string): number => { if (!Number.isSafeInteger(valueArg) || Number(valueArg) < 0) { throw new Error(`${fieldNameArg} must be a non-negative safe integer`); } return Number(valueArg); }; export function normalizeGatewayMailDomain(domainArg: unknown): string { const candidate = typeof domainArg === 'string' ? domainArg.trim().toLowerCase().replace(/\.+$/, '') : ''; if (!candidate || candidate.includes('@') || !candidate.includes('.')) { throw new Error(`Invalid email domain: ${String(domainArg || '')}`); } const ascii = plugins.url.domainToASCII(candidate); if (!ascii || ascii.includes('@') || !ascii.includes('.')) { throw new Error(`Invalid email domain: ${String(domainArg || '')}`); } return ascii; } export function normalizeGatewayMailDomainOwner( ownerArg: { gatewayClientType?: unknown; gatewayClientId?: unknown; }, ): IGatewayMailDomainOwner { const gatewayClientType = ownerArg?.gatewayClientType as TGatewayClientType; if (!gatewayClientTypes.has(gatewayClientType)) { throw new Error(`Invalid gateway client type: ${String(ownerArg?.gatewayClientType || '')}`); } return { gatewayClientType, gatewayClientId: requireTrimmedString(ownerArg.gatewayClientId, 'gatewayClientId'), }; } export function gatewayMailOwnerFromLegacyOwnership( ownershipArg: IWorkAppMailOwnership, ): IGatewayMailDomainOwner & { appInstanceId: string } { const owner = normalizeGatewayMailDomainOwner({ gatewayClientType: ownershipArg?.workHosterType, gatewayClientId: ownershipArg?.workHosterId, }); return { ...owner, appInstanceId: requireTrimmedString(ownershipArg?.workAppId, 'appInstanceId'), }; } export function createGatewayMailDomainId( ownerArg: IGatewayMailDomainOwner, domainArg: string, ): string { const owner = normalizeGatewayMailDomainOwner(ownerArg); const domain = normalizeGatewayMailDomain(domainArg); const digest = plugins.crypto.createHash('sha256') .update(JSON.stringify([owner.gatewayClientType, owner.gatewayClientId, domain])) .digest('hex'); return `gateway-mail-domain-${digest}`; } export function normalizeStoredWorkAppMailIdentity( identityArg: unknown, ): IStoredWorkAppMailIdentity { if (!identityArg || typeof identityArg !== 'object' || Array.isArray(identityArg)) { throw new Error('Stored WorkApp mail identity must be an object'); } const identity = identityArg as Record; const owner = gatewayMailOwnerFromLegacyOwnership(identity.ownership); const domain = normalizeGatewayMailDomain(identity.domain); const localPart = requireTrimmedString(identity.localPart, 'localPart').toLowerCase(); if (localPart.includes('@') || /\s/.test(localPart)) { throw new Error(`Invalid email local part: ${identity.localPart}`); } const rawAddress = requireTrimmedString(identity.address, 'address').toLowerCase(); const [addressLocalPart, addressDomain, addressExtra] = rawAddress.split('@'); if (!addressLocalPart || !addressDomain || addressExtra) { throw new Error(`Invalid stored mail identity address: ${identity.address}`); } const address = `${addressLocalPart}@${normalizeGatewayMailDomain(addressDomain)}`; if (address !== `${localPart}@${domain}`) { throw new Error('Stored mail identity address, localPart, and domain do not match'); } if (!identity.smtp || typeof identity.smtp !== 'object' || Array.isArray(identity.smtp)) { throw new Error('Stored mail identity smtp configuration is required'); } if (typeof identity.enabled !== 'boolean' || typeof identity.smtp.enabled !== 'boolean') { throw new Error('Stored mail identity enabled flags must be boolean'); } const createdAt = requireTimestamp(identity.createdAt, 'createdAt'); const updatedAt = requireTimestamp(identity.updatedAt, 'updatedAt'); const smtpLastRotatedAt = identity.smtpLastRotatedAt === undefined ? undefined : requireTimestamp(identity.smtpLastRotatedAt, 'smtpLastRotatedAt'); const ownership: IWorkAppMailOwnership = { workHosterType: owner.gatewayClientType, workHosterId: owner.gatewayClientId, workAppId: owner.appInstanceId, }; const externalKey = [ owner.gatewayClientType, owner.gatewayClientId, owner.appInstanceId, address, ].join(':'); return { id: requireTrimmedString(identity.id, 'id'), externalKey, ownership, address, localPart, domain, enabled: identity.enabled, ...(identity.displayName === undefined ? {} : { displayName: String(identity.displayName) }), ...(identity.inbound === undefined ? {} : { inbound: structuredClone(identity.inbound) }), ...(identity.inboundTarget === undefined ? {} : { inboundTarget: structuredClone(identity.inboundTarget) }), smtp: { enabled: identity.smtp.enabled, username: requireTrimmedString(identity.smtp.username, 'smtp.username'), }, createdAt, updatedAt, createdBy: requireTrimmedString(identity.createdBy, 'createdBy'), smtpPassword: requireTrimmedString(identity.smtpPassword, 'smtpPassword'), ...(smtpLastRotatedAt === undefined ? {} : { smtpLastRotatedAt }), }; } export function groupGatewayMailIdentities( identitiesArg: unknown[], ): IGatewayMailDomainGroup[] { const groups = new Map(); const identityIds = new Set(); const externalKeys = new Set(); for (const identityArg of identitiesArg) { const identity = normalizeStoredWorkAppMailIdentity(identityArg); if (identityIds.has(identity.id)) { throw new Error(`Duplicate stored mail identity id: ${identity.id}`); } if (externalKeys.has(identity.externalKey)) { throw new Error(`Duplicate stored mail identity externalKey: ${identity.externalKey}`); } identityIds.add(identity.id); externalKeys.add(identity.externalKey); const owner = gatewayMailOwnerFromLegacyOwnership(identity.ownership); const id = createGatewayMailDomainId(owner, identity.domain); const group = groups.get(id) || { id, gatewayClientType: owner.gatewayClientType, gatewayClientId: owner.gatewayClientId, domain: identity.domain, identities: [], }; group.identities.push(identity); groups.set(id, group); } return Array.from(groups.values()) .map((groupArg) => ({ ...groupArg, identities: groupArg.identities .sort((leftArg, rightArg) => leftArg.externalKey.localeCompare(rightArg.externalKey)), })) .sort((leftArg, rightArg) => leftArg.id.localeCompare(rightArg.id)); }