import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { logger } from '../logger.js'; import { EventEmitter } from 'events'; // --------------------------------------------------------------------------- // IPC command type map — mirrors the methods in mailer-bin's management mode // --------------------------------------------------------------------------- interface IDkimVerificationResult { is_valid: boolean; domain: string | null; selector: string | null; status: string; details: string | null; } interface ISpfResult { result: string; domain: string; ip: string; explanation: string | null; } interface IDmarcResult { passed: boolean; policy: string; domain: string; dkim_result: string; spf_result: string; action: string; details: string | null; } interface IEmailSecurityResult { dkim: IDkimVerificationResult[]; spf: ISpfResult | null; dmarc: IDmarcResult | null; } interface IValidationResult { valid: boolean; formatValid: boolean; score: number; error: string | null; } interface IBounceDetection { bounce_type: string; category: string; } interface IReputationResult { ip: string; score: number; risk_level: string; ip_type: string; dnsbl_results: Array<{ server: string; listed: boolean; response: string | null }>; listed_count: number; total_checked: number; } interface IContentScanResult { threatScore: number; threatType: string | null; threatDetails: string | null; scannedElements: string[]; } // --- SMTP Client types --- interface IOutboundEmail { from: string; to: string[]; cc?: string[]; bcc?: string[]; subject?: string; text?: string; html?: string; headers?: Record; } interface ISmtpSendResult { accepted: string[]; rejected: string[]; messageId?: string; response: string; envelope: { from: string; to: string[] }; } interface ISmtpSendOptions { host: string; port: number; secure?: boolean; domain?: string; auth?: { user: string; pass: string; method?: string }; email: IOutboundEmail; dkim?: { domain: string; selector: string; privateKey: string; keyType?: string }; connectionTimeoutSecs?: number; socketTimeoutSecs?: number; poolKey?: string; maxPoolConnections?: number; tlsOpportunistic?: boolean; } interface ISmtpSendRawOptions { host: string; port: number; secure?: boolean; domain?: string; auth?: { user: string; pass: string; method?: string }; envelopeFrom: string; envelopeTo: string[]; rawMessageBase64: string; poolKey?: string; } interface ISmtpVerifyOptions { host: string; port: number; secure?: boolean; domain?: string; auth?: { user: string; pass: string; method?: string }; } interface ISmtpVerifyResult { reachable: boolean; greeting?: string; capabilities?: string[]; } interface ISmtpPoolStatus { pools: Record; } interface IVersionInfo { bin: string; core: string; security: string; smtp: string; } // --- SMTP Server types --- interface ISmtpServerConfig { hostname: string; ports: number[]; securePort?: number; tlsCertPem?: string; tlsKeyPem?: string; additionalTlsCerts?: Array<{ domains: string[]; certPem: string; keyPem: string }>; maxMessageSize?: number; maxConnections?: number; maxRecipients?: number; connectionTimeoutSecs?: number; dataTimeoutSecs?: number; authEnabled?: boolean; maxAuthFailures?: number; socketTimeoutSecs?: number; processingTimeoutSecs?: number; rateLimits?: IRateLimitConfig; } interface IRateLimitConfig { maxConnectionsPerIp?: number; maxMessagesPerSender?: number; maxAuthFailuresPerIp?: number; windowSecs?: number; } interface IEmailData { type: 'inline' | 'file'; base64?: string; path?: string; } interface IEmailReceivedEvent { correlationId: string; sessionId: string; mailFrom: string; rcptTo: string[]; data: IEmailData; remoteAddr: string; clientHostname: string | null; secure: boolean; authenticatedUser: string | null; securityResults: any | null; } interface IAuthRequestEvent { correlationId: string; sessionId: string; username: string; password: string; remoteAddr: string; } interface IScramCredentialRequestEvent { correlationId: string; sessionId: string; username: string; remoteAddr: string; } /** * Type-safe command map for the mailer-bin IPC bridge. */ type TMailerCommands = { ping: { params: Record; result: { pong: boolean }; }; version: { params: Record; result: IVersionInfo; }; validateEmail: { params: { email: string }; result: IValidationResult; }; detectBounce: { params: { smtpResponse?: string; diagnosticCode?: string; statusCode?: string }; result: IBounceDetection; }; checkIpReputation: { params: { ip: string }; result: IReputationResult; }; verifyDkim: { params: { rawMessage: string }; result: IDkimVerificationResult[]; }; signDkim: { params: { rawMessage: string; domain: string; selector?: string; privateKey: string; keyType?: string }; result: { header: string; signedMessage: string }; }; checkSpf: { params: { ip: string; heloDomain: string; hostname?: string; mailFrom: string }; result: ISpfResult; }; scanContent: { params: { subject?: string; textBody?: string; htmlBody?: string; attachmentNames?: string[]; }; result: IContentScanResult; }; verifyEmail: { params: { rawMessage: string; ip: string; heloDomain: string; hostname?: string; mailFrom: string; }; result: IEmailSecurityResult; }; startSmtpServer: { params: ISmtpServerConfig; result: { started: boolean }; }; stopSmtpServer: { params: Record; result: { stopped: boolean; wasRunning?: boolean }; }; emailProcessingResult: { params: { correlationId: string; accepted: boolean; smtpCode?: number; smtpMessage?: string; }; result: { resolved: boolean }; }; authResult: { params: { correlationId: string; success: boolean; message?: string; }; result: { resolved: boolean }; }; scramCredentialResult: { params: { correlationId: string; found: boolean; salt?: string; iterations?: number; storedKey?: string; serverKey?: string; }; result: { resolved: boolean }; }; configureRateLimits: { params: IRateLimitConfig; result: { configured: boolean }; }; sendEmail: { params: ISmtpSendOptions; result: ISmtpSendResult; }; sendRawEmail: { params: ISmtpSendRawOptions; result: ISmtpSendResult; }; verifySmtpConnection: { params: ISmtpVerifyOptions; result: ISmtpVerifyResult; }; closeSmtpPool: { params: { poolKey?: string }; result: { closed: boolean }; }; getSmtpPoolStatus: { params: Record; result: ISmtpPoolStatus; }; }; // --------------------------------------------------------------------------- // Bridge state machine // --------------------------------------------------------------------------- export enum BridgeState { Idle = 'idle', Starting = 'starting', Running = 'running', Restarting = 'restarting', Failed = 'failed', Stopped = 'stopped', } export interface IBridgeResilienceConfig { maxRestartAttempts: number; healthCheckIntervalMs: number; restartBackoffBaseMs: number; restartBackoffMaxMs: number; healthCheckTimeoutMs: number; } const DEFAULT_RESILIENCE_CONFIG: IBridgeResilienceConfig = { maxRestartAttempts: 5, healthCheckIntervalMs: 30_000, restartBackoffBaseMs: 1_000, restartBackoffMaxMs: 30_000, healthCheckTimeoutMs: 5_000, }; // --------------------------------------------------------------------------- // RustSecurityBridge — singleton wrapper around smartrust.RustBridge // --------------------------------------------------------------------------- /** * Bridge between TypeScript and the Rust `mailer-bin` binary. * * Uses `@push.rocks/smartrust` for JSON-over-stdin/stdout IPC. * Singleton — access via `RustSecurityBridge.getInstance()`. * * Features resilience via auto-restart with exponential backoff, * periodic health checks, and a state machine that tracks the * bridge lifecycle. */ export class RustSecurityBridge extends EventEmitter { private static instance: RustSecurityBridge | null = null; private static _resilienceConfig: IBridgeResilienceConfig = { ...DEFAULT_RESILIENCE_CONFIG }; private bridge: InstanceType>; private _running = false; private _state: BridgeState = BridgeState.Idle; private _restartAttempts = 0; private _restartTimer: ReturnType | null = null; private _healthCheckTimer: ReturnType | null = null; private _deliberateStop = false; private _smtpServerConfig: ISmtpServerConfig | null = null; private constructor() { super(); this.bridge = new plugins.smartrust.RustBridge({ binaryName: 'mailer-bin', cliArgs: ['--management'], requestTimeoutMs: 30_000, readyTimeoutMs: 10_000, localPaths: [ plugins.path.join(paths.packageDir, 'dist_rust', 'mailer-bin'), plugins.path.join(paths.packageDir, 'rust', 'target', 'release', 'mailer-bin'), plugins.path.join(paths.packageDir, 'rust', 'target', 'debug', 'mailer-bin'), ], searchSystemPath: false, }); // Forward lifecycle events this.bridge.on('ready', () => { this._running = true; logger.log('info', 'Rust security bridge is ready'); }); this.bridge.on('exit', (code: number | null, signal: string | null) => { this._running = false; logger.log('warn', `Rust security bridge exited (code=${code}, signal=${signal})`); if (this._deliberateStop) { this.setState(BridgeState.Stopped); } else if (this._state === BridgeState.Running) { // Unexpected exit — attempt restart this.attemptRestart(); } }); this.bridge.on('stderr', (line: string) => { logger.log('debug', `[rust-bridge] ${line}`); }); } // ----------------------------------------------------------------------- // Static configuration & singleton // ----------------------------------------------------------------------- /** Get or create the singleton instance. */ public static getInstance(): RustSecurityBridge { if (!RustSecurityBridge.instance) { RustSecurityBridge.instance = new RustSecurityBridge(); } return RustSecurityBridge.instance; } /** Reset the singleton instance (for testing). */ public static resetInstance(): void { if (RustSecurityBridge.instance) { RustSecurityBridge.instance.stopHealthCheck(); if (RustSecurityBridge.instance._restartTimer) { clearTimeout(RustSecurityBridge.instance._restartTimer); RustSecurityBridge.instance._restartTimer = null; } RustSecurityBridge.instance.removeAllListeners(); } RustSecurityBridge.instance = null; } /** Configure resilience parameters. Can be called before or after getInstance(). */ public static configure(config: Partial): void { RustSecurityBridge._resilienceConfig = { ...RustSecurityBridge._resilienceConfig, ...config, }; } // ----------------------------------------------------------------------- // State management // ----------------------------------------------------------------------- /** Current bridge state. */ public get state(): BridgeState { return this._state; } /** Whether the Rust process is currently running and accepting commands. */ public get running(): boolean { return this._running; } private setState(newState: BridgeState): void { const oldState = this._state; if (oldState === newState) return; this._state = newState; logger.log('info', `Rust bridge state: ${oldState} -> ${newState}`); this.emit('stateChange', { oldState, newState }); } /** * Throws a descriptive error if the bridge is not in Running state. * Called at the top of every command method. */ private ensureRunning(): void { if (this._state === BridgeState.Running && this._running) { return; } switch (this._state) { case BridgeState.Idle: throw new Error('Rust bridge has not been started yet. Call start() first.'); case BridgeState.Starting: throw new Error('Rust bridge is still starting. Wait for start() to resolve.'); case BridgeState.Restarting: throw new Error('Rust bridge is restarting after a crash. Commands will resume once it recovers.'); case BridgeState.Failed: throw new Error('Rust bridge has failed after exhausting all restart attempts.'); case BridgeState.Stopped: throw new Error('Rust bridge has been stopped. Call start() to restart it.'); default: throw new Error(`Rust bridge is not running (state=${this._state}).`); } } // ----------------------------------------------------------------------- // Lifecycle // ----------------------------------------------------------------------- /** * Spawn the Rust binary and wait for the ready signal. * @returns `true` if the binary started successfully, `false` otherwise. */ public async start(): Promise { if (this._running && this._state === BridgeState.Running) { return true; } this._deliberateStop = false; this._restartAttempts = 0; this.setState(BridgeState.Starting); try { const ok = await this.bridge.spawn(); this._running = ok; if (ok) { this.setState(BridgeState.Running); this.startHealthCheck(); logger.log('info', 'Rust security bridge started'); } else { this.setState(BridgeState.Failed); logger.log('warn', 'Rust security bridge failed to start (binary not found or timeout)'); } return ok; } catch (err) { this.setState(BridgeState.Failed); logger.log('error', `Failed to start Rust security bridge: ${(err as Error).message}`); return false; } } /** Kill the Rust process deliberately. */ public async stop(): Promise { this._deliberateStop = true; // Cancel any pending restart if (this._restartTimer) { clearTimeout(this._restartTimer); this._restartTimer = null; } this.stopHealthCheck(); this._smtpServerConfig = null; if (!this._running) { this.setState(BridgeState.Stopped); return; } try { this.bridge.kill(); this._running = false; this.setState(BridgeState.Stopped); logger.log('info', 'Rust security bridge stopped'); } catch (err) { logger.log('error', `Error stopping Rust security bridge: ${(err as Error).message}`); } } // ----------------------------------------------------------------------- // Auto-restart with exponential backoff // ----------------------------------------------------------------------- private attemptRestart(): void { const config = RustSecurityBridge._resilienceConfig; this._restartAttempts++; if (this._restartAttempts > config.maxRestartAttempts) { logger.log('error', `Rust bridge exceeded max restart attempts (${config.maxRestartAttempts}). Giving up.`); this.setState(BridgeState.Failed); return; } this.setState(BridgeState.Restarting); this.stopHealthCheck(); const delay = Math.min( config.restartBackoffBaseMs * Math.pow(2, this._restartAttempts - 1), config.restartBackoffMaxMs, ); logger.log('info', `Rust bridge restart attempt ${this._restartAttempts}/${config.maxRestartAttempts} in ${delay}ms`); this._restartTimer = setTimeout(async () => { this._restartTimer = null; // Guard: if stop() was called while we were waiting, don't restart if (this._deliberateStop) { this.setState(BridgeState.Stopped); return; } try { const ok = await this.bridge.spawn(); this._running = ok; if (ok) { logger.log('info', 'Rust bridge restarted successfully'); this._restartAttempts = 0; this.setState(BridgeState.Running); this.startHealthCheck(); await this.restoreAfterRestart(); } else { logger.log('warn', 'Rust bridge restart failed (spawn returned false)'); this.attemptRestart(); } } catch (err) { logger.log('error', `Rust bridge restart failed: ${(err as Error).message}`); this.attemptRestart(); } }, delay); } /** * Restore state after a successful restart: * - Re-send startSmtpServer command if the SMTP server was running */ private async restoreAfterRestart(): Promise { if (this._smtpServerConfig) { try { logger.log('info', 'Restoring SMTP server after bridge restart'); const result = await this.bridge.sendCommand('startSmtpServer', this._smtpServerConfig); if (result?.started) { logger.log('info', 'SMTP server restored after bridge restart'); } else { logger.log('warn', 'SMTP server failed to restore after bridge restart'); } } catch (err) { logger.log('error', `Failed to restore SMTP server after restart: ${(err as Error).message}`); } } } // ----------------------------------------------------------------------- // Health check // ----------------------------------------------------------------------- private startHealthCheck(): void { this.stopHealthCheck(); const config = RustSecurityBridge._resilienceConfig; this._healthCheckTimer = setInterval(async () => { if (this._state !== BridgeState.Running || !this._running) { return; } try { const pongPromise = this.bridge.sendCommand('ping', {} as any); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Health check timeout')), config.healthCheckTimeoutMs), ); const res = await Promise.race([pongPromise, timeoutPromise]); if (!(res as any)?.pong) { throw new Error('Health check: unexpected ping response'); } } catch (err) { logger.log('warn', `Rust bridge health check failed: ${(err as Error).message}. Killing process to trigger restart.`); try { this.bridge.kill(); } catch { // Already dead } // The exit handler will trigger attemptRestart() } }, config.healthCheckIntervalMs); } private stopHealthCheck(): void { if (this._healthCheckTimer) { clearInterval(this._healthCheckTimer); this._healthCheckTimer = null; } } // ----------------------------------------------------------------------- // Commands — thin typed wrappers over sendCommand // ----------------------------------------------------------------------- /** Ping the Rust process. */ public async ping(): Promise { this.ensureRunning(); const res = await this.bridge.sendCommand('ping', {} as any); return res?.pong === true; } /** Get version information for all Rust crates. */ public async getVersion(): Promise { this.ensureRunning(); return this.bridge.sendCommand('version', {} as any); } /** Validate an email address. */ public async validateEmail(email: string): Promise { this.ensureRunning(); return this.bridge.sendCommand('validateEmail', { email }); } /** Detect bounce type from SMTP response / diagnostic code. */ public async detectBounce(opts: { smtpResponse?: string; diagnosticCode?: string; statusCode?: string; }): Promise { this.ensureRunning(); return this.bridge.sendCommand('detectBounce', opts); } /** Scan email content for threats (phishing, spam, malware, etc.). */ public async scanContent(opts: { subject?: string; textBody?: string; htmlBody?: string; attachmentNames?: string[]; }): Promise { this.ensureRunning(); return this.bridge.sendCommand('scanContent', opts); } /** Check IP reputation via DNSBL. */ public async checkIpReputation(ip: string): Promise { this.ensureRunning(); return this.bridge.sendCommand('checkIpReputation', { ip }); } /** Verify DKIM signatures on a raw email message. */ public async verifyDkim(rawMessage: string): Promise { this.ensureRunning(); return this.bridge.sendCommand('verifyDkim', { rawMessage }); } /** Sign an email with DKIM (RSA or Ed25519). */ public async signDkim(opts: { rawMessage: string; domain: string; selector?: string; privateKey: string; keyType?: string; }): Promise<{ header: string; signedMessage: string }> { this.ensureRunning(); return this.bridge.sendCommand('signDkim', opts); } /** Check SPF for a sender. */ public async checkSpf(opts: { ip: string; heloDomain: string; hostname?: string; mailFrom: string; }): Promise { this.ensureRunning(); return this.bridge.sendCommand('checkSpf', opts); } /** * Compound email security verification: DKIM + SPF + DMARC in one IPC call. * * This is the preferred method for inbound email verification — it avoids * 3 sequential round-trips and correctly passes raw mail-auth types internally. */ public async verifyEmail(opts: { rawMessage: string; ip: string; heloDomain: string; hostname?: string; mailFrom: string; }): Promise { this.ensureRunning(); return this.bridge.sendCommand('verifyEmail', opts); } // ----------------------------------------------------------------------- // SMTP Client — outbound email delivery via Rust // ----------------------------------------------------------------------- /** Send a structured email via the Rust SMTP client. */ public async sendOutboundEmail(opts: ISmtpSendOptions): Promise { this.ensureRunning(); return this.bridge.sendCommand('sendEmail', opts); } /** Send a pre-formatted raw email via the Rust SMTP client. */ public async sendRawEmail(opts: ISmtpSendRawOptions): Promise { this.ensureRunning(); return this.bridge.sendCommand('sendRawEmail', opts); } /** Verify connectivity to an SMTP server. */ public async verifySmtpConnection(opts: ISmtpVerifyOptions): Promise { this.ensureRunning(); return this.bridge.sendCommand('verifySmtpConnection', opts); } /** Close a specific connection pool (or all pools if no key is given). */ public async closeSmtpPool(poolKey?: string): Promise { this.ensureRunning(); await this.bridge.sendCommand('closeSmtpPool', poolKey ? { poolKey } : ({} as any)); } /** Get status of all SMTP client connection pools. */ public async getSmtpPoolStatus(): Promise { this.ensureRunning(); return this.bridge.sendCommand('getSmtpPoolStatus', {} as any); } // ----------------------------------------------------------------------- // SMTP Server lifecycle // ----------------------------------------------------------------------- /** * Start the Rust SMTP server. * The server will listen on the configured ports and emit events for * emailReceived and authRequest that must be handled by the caller. */ public async startSmtpServer(config: ISmtpServerConfig): Promise { this.ensureRunning(); this._smtpServerConfig = config; const result = await this.bridge.sendCommand('startSmtpServer', config); return result?.started === true; } /** Stop the Rust SMTP server. */ public async stopSmtpServer(): Promise { this.ensureRunning(); this._smtpServerConfig = null; await this.bridge.sendCommand('stopSmtpServer', {} as any); } /** * Send the result of email processing back to the Rust SMTP server. * This resolves a pending correlation-ID callback, allowing the Rust * server to send the SMTP response to the client. */ public async sendEmailProcessingResult(opts: { correlationId: string; accepted: boolean; smtpCode?: number; smtpMessage?: string; }): Promise { this.ensureRunning(); await this.bridge.sendCommand('emailProcessingResult', opts); } /** * Send the result of authentication validation back to the Rust SMTP server. */ public async sendAuthResult(opts: { correlationId: string; success: boolean; message?: string; }): Promise { this.ensureRunning(); await this.bridge.sendCommand('authResult', opts); } /** * Send SCRAM credentials back to the Rust SMTP server. * Values (salt, storedKey, serverKey) must be base64-encoded. */ public async sendScramCredentialResult(opts: { correlationId: string; found: boolean; salt?: string; iterations?: number; storedKey?: string; serverKey?: string; }): Promise { this.ensureRunning(); await this.bridge.sendCommand('scramCredentialResult', opts); } /** Update rate limit configuration at runtime. */ public async configureRateLimits(config: IRateLimitConfig): Promise { this.ensureRunning(); await this.bridge.sendCommand('configureRateLimits', config); } // ----------------------------------------------------------------------- // Event registration — delegates to the underlying bridge EventEmitter // ----------------------------------------------------------------------- /** * Register a handler for emailReceived events from the Rust SMTP server. * These events fire when a complete email has been received and needs processing. */ public onEmailReceived(handler: (data: IEmailReceivedEvent) => void): void { this.bridge.on('management:emailReceived', handler); } /** * Register a handler for authRequest events from the Rust SMTP server. * The handler must call sendAuthResult() with the correlationId. */ public onAuthRequest(handler: (data: IAuthRequestEvent) => void): void { this.bridge.on('management:authRequest', handler); } /** * Register a handler for scramCredentialRequest events from the Rust SMTP server. * The handler must call sendScramCredentialResult() with the correlationId. */ public onScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void { this.bridge.on('management:scramCredentialRequest', handler); } /** Remove an emailReceived event handler. */ public offEmailReceived(handler: (data: IEmailReceivedEvent) => void): void { this.bridge.off('management:emailReceived', handler); } /** Remove an authRequest event handler. */ public offAuthRequest(handler: (data: IAuthRequestEvent) => void): void { this.bridge.off('management:authRequest', handler); } /** Remove a scramCredentialRequest event handler. */ public offScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void { this.bridge.off('management:scramCredentialRequest', handler); } } // Re-export interfaces for consumers export type { IDkimVerificationResult, ISpfResult, IDmarcResult, IEmailSecurityResult, IValidationResult, IBounceDetection, IContentScanResult, IReputationResult as IRustReputationResult, IVersionInfo, ISmtpServerConfig, IRateLimitConfig, IEmailData, IEmailReceivedEvent, IAuthRequestEvent, IScramCredentialRequestEvent, IOutboundEmail, ISmtpSendResult, ISmtpSendOptions, ISmtpSendRawOptions, ISmtpVerifyOptions, ISmtpVerifyResult, ISmtpPoolStatus, };