import * as plugins from '../plugins.js'; import * as interfaces from '../../ts_interfaces/index.js'; import { appState } from './shared.js'; import { getActionContext } from './login.js'; import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js'; // ============================================================================ // Email Domains State // ============================================================================ export type TEmailDomainOperationName = | 'create' | 'update' | 'delete' | 'validate' | 'provision' | 'reconcile' | 'rotate'; export interface IEmailDomainOperationState extends interfaces.requests.IEmailDomainActionResult { operation: TEmailDomainOperationName; completedAt: number; domain?: interfaces.data.IEmailDomain; } export interface IEmailDomainsState { domains: interfaces.data.IEmailDomain[]; settings: interfaces.data.IEmailServerSettings | null; isLoading: boolean; error: string | null; lastOperation: IEmailDomainOperationState | null; lastUpdated: number; } export const emailDomainsStatePart = await appState.getStatePart( 'emailDomains', { domains: [], settings: null, isLoading: false, error: null, lastOperation: null, lastUpdated: 0, }, 'soft', ); const createOperationState = ( operationArg: TEmailDomainOperationName, responseArg: interfaces.requests.IEmailDomainActionResult & { domain?: interfaces.data.IEmailDomain; }, ): IEmailDomainOperationState => ({ operation: operationArg, completedAt: Date.now(), ...responseArg, }); const createFailedOperationState = ( operationArg: TEmailDomainOperationName, errorArg: unknown, fallbackMessageArg: string, ): IEmailDomainOperationState => ({ operation: operationArg, completedAt: Date.now(), success: false, message: errorArg instanceof Error ? errorArg.message : fallbackMessageArg, code: 'EMAIL_DOMAIN_REQUEST_FAILED', errors: [ { code: 'EMAIL_DOMAIN_REQUEST_FAILED', message: errorArg instanceof Error ? errorArg.message : fallbackMessageArg, retryable: true, }, ], }); export const formatEmailDomainOperationMessage = ( operationArg: IEmailDomainOperationState, fallbackMessageArg: string, ): string => { const primaryMessage = operationArg.message || fallbackMessageArg; const messageParts = [ operationArg.code ? `${primaryMessage} [${operationArg.code}]` : primaryMessage, ]; for (const error of operationArg.errors || []) { const location = error.recordKey ? `${error.recordKey}: ` : ''; const detail = `${location}${error.message} [${error.code}]`; const duplicatesPrimary = !error.recordKey && error.message === primaryMessage && error.code === operationArg.code; if (!duplicatesPrimary && !messageParts.includes(detail)) { messageParts.push(detail); } } if (operationArg.lifecycleStatus && operationArg.lifecycleStatus !== 'active') { messageParts.push(`Lifecycle: ${operationArg.lifecycleStatus}`); } if (operationArg.capability === 'unsupported' && !messageParts.some((part) => part.includes('unsupported'))) { messageParts.push('Required capability is unsupported'); } if (operationArg.retryAt) { messageParts.push(`Retry scheduled for ${operationArg.retryAt}`); } return messageParts.join(' — '); }; const applyOperationState = ( stateArg: IEmailDomainsState, operationArg: IEmailDomainOperationState, fallbackMessageArg: string, ): IEmailDomainsState => ({ ...stateArg, error: operationArg.success ? null : formatEmailDomainOperationMessage(operationArg, fallbackMessageArg), lastOperation: operationArg, }); export const fetchEmailDomainsAction = emailDomainsStatePart.createAction( async (statePartArg): Promise => { const context = getActionContext(); const currentState = statePartArg.getState()!; if (!context.identity) return currentState; const ticket = captureRealtimeTicket('emailDomains'); try { const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetEmailDomains >('/typedrequest', 'getEmailDomains'); const settingsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetEmailServerSettings >('/typedrequest', 'getEmailServerSettings'); const response = await request.fire({ identity: context.identity }); const settingsResponse = await settingsRequest.fire({ identity: context.identity }); if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!; return { ...statePartArg.getState()!, domains: response.domains, settings: settingsResponse.settings, isLoading: false, error: null, lastUpdated: Date.now(), }; } catch (error: unknown) { if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!; return { ...statePartArg.getState()!, isLoading: false, error: error instanceof Error ? error.message : 'Failed to fetch email domains', }; } }, ); export const createEmailDomainAction = emailDomainsStatePart.createAction<{ linkedDomainId: string; subdomain?: string; dkimSelector?: string; dkimKeySize?: number; rotateKeys?: boolean; rotationIntervalDays?: number; remoteIngress?: interfaces.data.IEmailDomainRemoteIngress; }>(async (statePartArg, args, actionContext) => { const context = getActionContext(); const currentState = statePartArg.getState()!; if (!context.identity) { const operation = createFailedOperationState('create', new Error('Not authenticated'), 'Not authenticated'); return applyOperationState(currentState, operation, 'Failed to create email domain'); } try { const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_CreateEmailDomain >('/typedrequest', 'createEmailDomain'); const response = await request.fire({ identity: context.identity, ...args }); const operation = createOperationState('create', response); const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null); return applyOperationState( refreshedState, operation, response.success ? 'Email domain created' : 'Failed to create email domain', ); } catch (error: unknown) { const operation = createFailedOperationState('create', error, 'Failed to create email domain'); const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null); return applyOperationState(refreshedState, operation, 'Failed to create email domain'); } }); export const updateEmailDomainAction = emailDomainsStatePart.createAction<{ id: string; rotateKeys?: boolean; rotationIntervalDays?: number; rateLimits?: interfaces.data.IEmailDomain['rateLimits']; remoteIngress?: interfaces.data.IEmailDomainRemoteIngress | null; }>(async (statePartArg, args, actionContext) => { const context = getActionContext(); const currentState = statePartArg.getState()!; if (!context.identity) { const operation = createFailedOperationState( 'update', new Error('Not authenticated'), 'Not authenticated', ); return applyOperationState(currentState, operation, 'Failed to update email domain'); } try { const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_UpdateEmailDomain >('/typedrequest', 'updateEmailDomain'); const response = await request.fire({ identity: context.identity, ...args }); const operation = createOperationState('update', response); if (!response.success) { return applyOperationState(currentState, operation, 'Failed to update email domain'); } const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null); return applyOperationState(refreshedState, operation, 'Email domain updated'); } catch (error: unknown) { const operation = createFailedOperationState('update', error, 'Failed to update email domain'); return applyOperationState(currentState, operation, 'Failed to update email domain'); } }); export const updateEmailServerSettingsAction = emailDomainsStatePart.createAction< interfaces.data.TEmailServerSettingsUpdate >(async (statePartArg, settings, actionContext) => { const context = getActionContext(); const currentState = statePartArg.getState()!; try { const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_UpdateEmailServerSettings >('/typedrequest', 'updateEmailServerSettings'); const response = await request.fire({ identity: context.identity!, settings }); if (!response.success) { return currentState; } return await actionContext!.dispatch(fetchEmailDomainsAction, null); } catch { return currentState; } }); export const deleteEmailDomainAction = emailDomainsStatePart.createAction( async (statePartArg, id, actionContext) => { const context = getActionContext(); const currentState = statePartArg.getState()!; if (!context.identity) { const operation = createFailedOperationState('delete', new Error('Not authenticated'), 'Not authenticated'); return applyOperationState(currentState, operation, 'Failed to delete email domain'); } try { const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_DeleteEmailDomain >('/typedrequest', 'deleteEmailDomain'); const response = await request.fire({ identity: context.identity, id }); const operation = createOperationState('delete', response); if (!response.success) { return applyOperationState(currentState, operation, 'Failed to delete email domain'); } const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null); return applyOperationState(refreshedState, operation, 'Email domain deleted'); } catch (error: unknown) { const operation = createFailedOperationState('delete', error, 'Failed to delete email domain'); return applyOperationState(currentState, operation, 'Failed to delete email domain'); } }, ); export const validateEmailDomainAction = emailDomainsStatePart.createAction( async (statePartArg, id) => { const context = getActionContext(); const currentState = statePartArg.getState()!; if (!context.identity) { const operation = createFailedOperationState('validate', new Error('Not authenticated'), 'Not authenticated'); return applyOperationState(currentState, operation, 'Failed to validate email domain DNS'); } const ticket = captureRealtimeTicket('emailDomains'); try { const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_ValidateEmailDomain >('/typedrequest', 'validateEmailDomain'); const response = await request.fire({ identity: context.identity, id }); const operation = createOperationState('validate', response); let latestState = statePartArg.getState()!; if (response.domain && isRealtimeTicketCurrent(ticket)) { const existingIndex = latestState.domains.findIndex((domain) => domain.id === response.domain!.id); const domains = [...latestState.domains]; if (existingIndex >= 0) { domains[existingIndex] = response.domain; } else { domains.push(response.domain); } latestState = { ...latestState, domains, lastUpdated: Date.now(), }; } return applyOperationState( latestState, operation, response.success ? 'Email domain DNS validated' : 'Failed to validate email domain DNS', ); } catch (error: unknown) { const operation = createFailedOperationState('validate', error, 'Failed to validate email domain DNS'); return applyOperationState( statePartArg.getState()!, operation, 'Failed to validate email domain DNS', ); } }, ); export const provisionEmailDomainDnsAction = emailDomainsStatePart.createAction( async (statePartArg, id, actionContext) => { const context = getActionContext(); const currentState = statePartArg.getState()!; if (!context.identity) { const operation = createFailedOperationState('provision', new Error('Not authenticated'), 'Not authenticated'); return applyOperationState(currentState, operation, 'Failed to provision email domain DNS'); } try { const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_ProvisionEmailDomainDns >('/typedrequest', 'provisionEmailDomainDns'); const response = await request.fire({ identity: context.identity, id }); const operation = createOperationState('provision', response); if (!response.success) { return applyOperationState(currentState, operation, 'Failed to provision email domain DNS'); } const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null); return applyOperationState(refreshedState, operation, 'Email domain DNS provisioned'); } catch (error: unknown) { const operation = createFailedOperationState('provision', error, 'Failed to provision email domain DNS'); return applyOperationState(currentState, operation, 'Failed to provision email domain DNS'); } }, ); // ============================================================================ // Email Domain Standalone Functions // ============================================================================ export async function fetchEmailDomainsSnapshot(): Promise { const context = getActionContext(); if (!context.identity) throw new Error('Not authenticated'); const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetEmailDomains >('/typedrequest', 'getEmailDomains'); const response = await request.fire({ identity: context.identity }); return response.domains; } export async function fetchEmailDomainSnapshot( idArg: string, ): Promise { const context = getActionContext(); if (!context.identity) throw new Error('Not authenticated'); const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetEmailDomain >('/typedrequest', 'getEmailDomain'); const response = await request.fire({ identity: context.identity, id: idArg }); return response.domain; } export async function fetchEmailDomainDnsRecords(id: string) { const context = getActionContext(); const request = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetEmailDomainDnsRecords >('/typedrequest', 'getEmailDomainDnsRecords'); return request.fire({ identity: context.identity!, id }); }