@push.rocks/smartmta 5.1.3 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/changelog.md +15 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +3 -0
  5. package/dist_ts/index.js +4 -0
  6. package/dist_ts/logger.d.ts +17 -0
  7. package/dist_ts/logger.js +76 -0
  8. package/dist_ts/mail/core/classes.bouncemanager.d.ts +185 -0
  9. package/dist_ts/mail/core/classes.bouncemanager.js +569 -0
  10. package/dist_ts/mail/core/classes.email.d.ts +291 -0
  11. package/dist_ts/mail/core/classes.email.js +802 -0
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +61 -0
  13. package/dist_ts/mail/core/classes.emailvalidator.js +184 -0
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +95 -0
  15. package/dist_ts/mail/core/classes.templatemanager.js +240 -0
  16. package/dist_ts/mail/core/index.d.ts +4 -0
  17. package/dist_ts/mail/core/index.js +6 -0
  18. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +163 -0
  19. package/dist_ts/mail/delivery/classes.delivery.queue.js +488 -0
  20. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +160 -0
  21. package/dist_ts/mail/delivery/classes.delivery.system.js +630 -0
  22. package/dist_ts/mail/delivery/classes.unified.rate.limiter.d.ts +200 -0
  23. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +820 -0
  24. package/dist_ts/mail/delivery/index.d.ts +4 -0
  25. package/dist_ts/mail/delivery/index.js +6 -0
  26. package/dist_ts/mail/delivery/interfaces.d.ts +140 -0
  27. package/dist_ts/mail/delivery/interfaces.js +17 -0
  28. package/dist_ts/mail/index.d.ts +7 -0
  29. package/dist_ts/mail/index.js +12 -0
  30. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +25 -0
  31. package/dist_ts/mail/routing/classes.dkim.manager.js +127 -0
  32. package/dist_ts/mail/routing/classes.dns.manager.d.ts +79 -0
  33. package/dist_ts/mail/routing/classes.dns.manager.js +415 -0
  34. package/dist_ts/mail/routing/classes.domain.registry.d.ts +54 -0
  35. package/dist_ts/mail/routing/classes.domain.registry.js +119 -0
  36. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +33 -0
  37. package/dist_ts/mail/routing/classes.email.action.executor.js +137 -0
  38. package/dist_ts/mail/routing/classes.email.router.d.ts +171 -0
  39. package/dist_ts/mail/routing/classes.email.router.js +494 -0
  40. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +241 -0
  41. package/dist_ts/mail/routing/classes.unified.email.server.js +935 -0
  42. package/dist_ts/mail/routing/index.d.ts +7 -0
  43. package/dist_ts/mail/routing/index.js +9 -0
  44. package/dist_ts/mail/routing/interfaces.d.ts +187 -0
  45. package/dist_ts/mail/routing/interfaces.js +2 -0
  46. package/dist_ts/mail/security/classes.dkimcreator.d.ts +72 -0
  47. package/dist_ts/mail/security/classes.dkimcreator.js +360 -0
  48. package/dist_ts/mail/security/classes.spfverifier.d.ts +62 -0
  49. package/dist_ts/mail/security/classes.spfverifier.js +87 -0
  50. package/dist_ts/mail/security/index.d.ts +2 -0
  51. package/dist_ts/mail/security/index.js +4 -0
  52. package/dist_ts/paths.d.ts +14 -0
  53. package/dist_ts/paths.js +39 -0
  54. package/dist_ts/plugins.d.ts +24 -0
  55. package/dist_ts/plugins.js +28 -0
  56. package/dist_ts/security/classes.contentscanner.d.ts +130 -0
  57. package/dist_ts/security/classes.contentscanner.js +338 -0
  58. package/dist_ts/security/classes.ipreputationchecker.d.ts +73 -0
  59. package/dist_ts/security/classes.ipreputationchecker.js +263 -0
  60. package/dist_ts/security/classes.rustsecuritybridge.d.ts +403 -0
  61. package/dist_ts/security/classes.rustsecuritybridge.js +502 -0
  62. package/dist_ts/security/classes.securitylogger.d.ts +140 -0
  63. package/dist_ts/security/classes.securitylogger.js +235 -0
  64. package/dist_ts/security/index.d.ts +4 -0
  65. package/dist_ts/security/index.js +5 -0
  66. package/package.json +6 -1
  67. package/ts/00_commitinfo_data.ts +8 -0
  68. package/ts/index.ts +3 -0
  69. package/ts/logger.ts +91 -0
  70. package/ts/mail/core/classes.bouncemanager.ts +731 -0
  71. package/ts/mail/core/classes.email.ts +942 -0
  72. package/ts/mail/core/classes.emailvalidator.ts +239 -0
  73. package/ts/mail/core/classes.templatemanager.ts +320 -0
  74. package/ts/mail/core/index.ts +5 -0
  75. package/ts/mail/delivery/classes.delivery.queue.ts +645 -0
  76. package/ts/mail/delivery/classes.delivery.system.ts +816 -0
  77. package/ts/mail/delivery/classes.unified.rate.limiter.ts +1053 -0
  78. package/ts/mail/delivery/index.ts +5 -0
  79. package/ts/mail/delivery/interfaces.ts +167 -0
  80. package/ts/mail/index.ts +17 -0
  81. package/ts/mail/routing/classes.dkim.manager.ts +157 -0
  82. package/ts/mail/routing/classes.dns.manager.ts +573 -0
  83. package/ts/mail/routing/classes.domain.registry.ts +139 -0
  84. package/ts/mail/routing/classes.email.action.executor.ts +175 -0
  85. package/ts/mail/routing/classes.email.router.ts +575 -0
  86. package/ts/mail/routing/classes.unified.email.server.ts +1207 -0
  87. package/ts/mail/routing/index.ts +9 -0
  88. package/ts/mail/routing/interfaces.ts +202 -0
  89. package/ts/mail/security/classes.dkimcreator.ts +447 -0
  90. package/ts/mail/security/classes.spfverifier.ts +126 -0
  91. package/ts/mail/security/index.ts +3 -0
  92. package/ts/paths.ts +48 -0
  93. package/ts/plugins.ts +53 -0
  94. package/ts/security/classes.contentscanner.ts +400 -0
  95. package/ts/security/classes.ipreputationchecker.ts +315 -0
  96. package/ts/security/classes.rustsecuritybridge.ts +964 -0
  97. package/ts/security/classes.securitylogger.ts +299 -0
  98. package/ts/security/index.ts +40 -0
@@ -0,0 +1,964 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as paths from '../paths.js';
3
+ import { logger } from '../logger.js';
4
+ import { EventEmitter } from 'events';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // IPC command type map — mirrors the methods in mailer-bin's management mode
8
+ // ---------------------------------------------------------------------------
9
+
10
+ interface IDkimVerificationResult {
11
+ is_valid: boolean;
12
+ domain: string | null;
13
+ selector: string | null;
14
+ status: string;
15
+ details: string | null;
16
+ }
17
+
18
+ interface ISpfResult {
19
+ result: string;
20
+ domain: string;
21
+ ip: string;
22
+ explanation: string | null;
23
+ }
24
+
25
+ interface IDmarcResult {
26
+ passed: boolean;
27
+ policy: string;
28
+ domain: string;
29
+ dkim_result: string;
30
+ spf_result: string;
31
+ action: string;
32
+ details: string | null;
33
+ }
34
+
35
+ interface IEmailSecurityResult {
36
+ dkim: IDkimVerificationResult[];
37
+ spf: ISpfResult | null;
38
+ dmarc: IDmarcResult | null;
39
+ }
40
+
41
+ interface IValidationResult {
42
+ valid: boolean;
43
+ formatValid: boolean;
44
+ score: number;
45
+ error: string | null;
46
+ }
47
+
48
+ interface IBounceDetection {
49
+ bounce_type: string;
50
+ category: string;
51
+ }
52
+
53
+ interface IReputationResult {
54
+ ip: string;
55
+ score: number;
56
+ risk_level: string;
57
+ ip_type: string;
58
+ dnsbl_results: Array<{ server: string; listed: boolean; response: string | null }>;
59
+ listed_count: number;
60
+ total_checked: number;
61
+ }
62
+
63
+ interface IContentScanResult {
64
+ threatScore: number;
65
+ threatType: string | null;
66
+ threatDetails: string | null;
67
+ scannedElements: string[];
68
+ }
69
+
70
+ // --- SMTP Client types ---
71
+
72
+ interface IOutboundEmail {
73
+ from: string;
74
+ to: string[];
75
+ cc?: string[];
76
+ bcc?: string[];
77
+ subject?: string;
78
+ text?: string;
79
+ html?: string;
80
+ headers?: Record<string, string>;
81
+ }
82
+
83
+ interface ISmtpSendResult {
84
+ accepted: string[];
85
+ rejected: string[];
86
+ messageId?: string;
87
+ response: string;
88
+ envelope: { from: string; to: string[] };
89
+ }
90
+
91
+ interface ISmtpSendOptions {
92
+ host: string;
93
+ port: number;
94
+ secure?: boolean;
95
+ domain?: string;
96
+ auth?: { user: string; pass: string; method?: string };
97
+ email: IOutboundEmail;
98
+ dkim?: { domain: string; selector: string; privateKey: string; keyType?: string };
99
+ connectionTimeoutSecs?: number;
100
+ socketTimeoutSecs?: number;
101
+ poolKey?: string;
102
+ maxPoolConnections?: number;
103
+ tlsOpportunistic?: boolean;
104
+ }
105
+
106
+ interface ISmtpSendRawOptions {
107
+ host: string;
108
+ port: number;
109
+ secure?: boolean;
110
+ domain?: string;
111
+ auth?: { user: string; pass: string; method?: string };
112
+ envelopeFrom: string;
113
+ envelopeTo: string[];
114
+ rawMessageBase64: string;
115
+ poolKey?: string;
116
+ }
117
+
118
+ interface ISmtpVerifyOptions {
119
+ host: string;
120
+ port: number;
121
+ secure?: boolean;
122
+ domain?: string;
123
+ auth?: { user: string; pass: string; method?: string };
124
+ }
125
+
126
+ interface ISmtpVerifyResult {
127
+ reachable: boolean;
128
+ greeting?: string;
129
+ capabilities?: string[];
130
+ }
131
+
132
+ interface ISmtpPoolStatus {
133
+ pools: Record<string, { total: number; active: number; idle: number }>;
134
+ }
135
+
136
+ interface IVersionInfo {
137
+ bin: string;
138
+ core: string;
139
+ security: string;
140
+ smtp: string;
141
+ }
142
+
143
+ // --- SMTP Server types ---
144
+
145
+ interface ISmtpServerConfig {
146
+ hostname: string;
147
+ ports: number[];
148
+ securePort?: number;
149
+ tlsCertPem?: string;
150
+ tlsKeyPem?: string;
151
+ additionalTlsCerts?: Array<{ domains: string[]; certPem: string; keyPem: string }>;
152
+ maxMessageSize?: number;
153
+ maxConnections?: number;
154
+ maxRecipients?: number;
155
+ connectionTimeoutSecs?: number;
156
+ dataTimeoutSecs?: number;
157
+ authEnabled?: boolean;
158
+ maxAuthFailures?: number;
159
+ socketTimeoutSecs?: number;
160
+ processingTimeoutSecs?: number;
161
+ rateLimits?: IRateLimitConfig;
162
+ }
163
+
164
+ interface IRateLimitConfig {
165
+ maxConnectionsPerIp?: number;
166
+ maxMessagesPerSender?: number;
167
+ maxAuthFailuresPerIp?: number;
168
+ windowSecs?: number;
169
+ }
170
+
171
+ interface IEmailData {
172
+ type: 'inline' | 'file';
173
+ base64?: string;
174
+ path?: string;
175
+ }
176
+
177
+ interface IEmailReceivedEvent {
178
+ correlationId: string;
179
+ sessionId: string;
180
+ mailFrom: string;
181
+ rcptTo: string[];
182
+ data: IEmailData;
183
+ remoteAddr: string;
184
+ clientHostname: string | null;
185
+ secure: boolean;
186
+ authenticatedUser: string | null;
187
+ securityResults: any | null;
188
+ }
189
+
190
+ interface IAuthRequestEvent {
191
+ correlationId: string;
192
+ sessionId: string;
193
+ username: string;
194
+ password: string;
195
+ remoteAddr: string;
196
+ }
197
+
198
+ interface IScramCredentialRequestEvent {
199
+ correlationId: string;
200
+ sessionId: string;
201
+ username: string;
202
+ remoteAddr: string;
203
+ }
204
+
205
+ /**
206
+ * Type-safe command map for the mailer-bin IPC bridge.
207
+ */
208
+ type TMailerCommands = {
209
+ ping: {
210
+ params: Record<string, never>;
211
+ result: { pong: boolean };
212
+ };
213
+ version: {
214
+ params: Record<string, never>;
215
+ result: IVersionInfo;
216
+ };
217
+ validateEmail: {
218
+ params: { email: string };
219
+ result: IValidationResult;
220
+ };
221
+ detectBounce: {
222
+ params: { smtpResponse?: string; diagnosticCode?: string; statusCode?: string };
223
+ result: IBounceDetection;
224
+ };
225
+ checkIpReputation: {
226
+ params: { ip: string };
227
+ result: IReputationResult;
228
+ };
229
+ verifyDkim: {
230
+ params: { rawMessage: string };
231
+ result: IDkimVerificationResult[];
232
+ };
233
+ signDkim: {
234
+ params: { rawMessage: string; domain: string; selector?: string; privateKey: string; keyType?: string };
235
+ result: { header: string; signedMessage: string };
236
+ };
237
+ checkSpf: {
238
+ params: { ip: string; heloDomain: string; hostname?: string; mailFrom: string };
239
+ result: ISpfResult;
240
+ };
241
+ scanContent: {
242
+ params: {
243
+ subject?: string;
244
+ textBody?: string;
245
+ htmlBody?: string;
246
+ attachmentNames?: string[];
247
+ };
248
+ result: IContentScanResult;
249
+ };
250
+ verifyEmail: {
251
+ params: {
252
+ rawMessage: string;
253
+ ip: string;
254
+ heloDomain: string;
255
+ hostname?: string;
256
+ mailFrom: string;
257
+ };
258
+ result: IEmailSecurityResult;
259
+ };
260
+ startSmtpServer: {
261
+ params: ISmtpServerConfig;
262
+ result: { started: boolean };
263
+ };
264
+ stopSmtpServer: {
265
+ params: Record<string, never>;
266
+ result: { stopped: boolean; wasRunning?: boolean };
267
+ };
268
+ emailProcessingResult: {
269
+ params: {
270
+ correlationId: string;
271
+ accepted: boolean;
272
+ smtpCode?: number;
273
+ smtpMessage?: string;
274
+ };
275
+ result: { resolved: boolean };
276
+ };
277
+ authResult: {
278
+ params: {
279
+ correlationId: string;
280
+ success: boolean;
281
+ message?: string;
282
+ };
283
+ result: { resolved: boolean };
284
+ };
285
+ scramCredentialResult: {
286
+ params: {
287
+ correlationId: string;
288
+ found: boolean;
289
+ salt?: string;
290
+ iterations?: number;
291
+ storedKey?: string;
292
+ serverKey?: string;
293
+ };
294
+ result: { resolved: boolean };
295
+ };
296
+ configureRateLimits: {
297
+ params: IRateLimitConfig;
298
+ result: { configured: boolean };
299
+ };
300
+ sendEmail: {
301
+ params: ISmtpSendOptions;
302
+ result: ISmtpSendResult;
303
+ };
304
+ sendRawEmail: {
305
+ params: ISmtpSendRawOptions;
306
+ result: ISmtpSendResult;
307
+ };
308
+ verifySmtpConnection: {
309
+ params: ISmtpVerifyOptions;
310
+ result: ISmtpVerifyResult;
311
+ };
312
+ closeSmtpPool: {
313
+ params: { poolKey?: string };
314
+ result: { closed: boolean };
315
+ };
316
+ getSmtpPoolStatus: {
317
+ params: Record<string, never>;
318
+ result: ISmtpPoolStatus;
319
+ };
320
+ };
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Bridge state machine
324
+ // ---------------------------------------------------------------------------
325
+
326
+ export enum BridgeState {
327
+ Idle = 'idle',
328
+ Starting = 'starting',
329
+ Running = 'running',
330
+ Restarting = 'restarting',
331
+ Failed = 'failed',
332
+ Stopped = 'stopped',
333
+ }
334
+
335
+ export interface IBridgeResilienceConfig {
336
+ maxRestartAttempts: number;
337
+ healthCheckIntervalMs: number;
338
+ restartBackoffBaseMs: number;
339
+ restartBackoffMaxMs: number;
340
+ healthCheckTimeoutMs: number;
341
+ }
342
+
343
+ const DEFAULT_RESILIENCE_CONFIG: IBridgeResilienceConfig = {
344
+ maxRestartAttempts: 5,
345
+ healthCheckIntervalMs: 30_000,
346
+ restartBackoffBaseMs: 1_000,
347
+ restartBackoffMaxMs: 30_000,
348
+ healthCheckTimeoutMs: 5_000,
349
+ };
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // RustSecurityBridge — singleton wrapper around smartrust.RustBridge
353
+ // ---------------------------------------------------------------------------
354
+
355
+ /**
356
+ * Bridge between TypeScript and the Rust `mailer-bin` binary.
357
+ *
358
+ * Uses `@push.rocks/smartrust` for JSON-over-stdin/stdout IPC.
359
+ * Singleton — access via `RustSecurityBridge.getInstance()`.
360
+ *
361
+ * Features resilience via auto-restart with exponential backoff,
362
+ * periodic health checks, and a state machine that tracks the
363
+ * bridge lifecycle.
364
+ */
365
+ export class RustSecurityBridge extends EventEmitter {
366
+ private static instance: RustSecurityBridge | null = null;
367
+ private static _resilienceConfig: IBridgeResilienceConfig = { ...DEFAULT_RESILIENCE_CONFIG };
368
+
369
+ private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TMailerCommands>>;
370
+ private _running = false;
371
+ private _state: BridgeState = BridgeState.Idle;
372
+ private _restartAttempts = 0;
373
+ private _restartTimer: ReturnType<typeof setTimeout> | null = null;
374
+ private _healthCheckTimer: ReturnType<typeof setInterval> | null = null;
375
+ private _deliberateStop = false;
376
+ private _smtpServerConfig: ISmtpServerConfig | null = null;
377
+
378
+ /**
379
+ * Map Node.js process.platform / process.arch to the tsrust-style suffix
380
+ * used for cross-compiled binaries, e.g. mailer-bin_linux_amd64.
381
+ */
382
+ private static getPlatformSuffix(): string | null {
383
+ const archMap: Record<string, string> = { x64: 'amd64', arm64: 'arm64' };
384
+ const os = process.platform; // 'linux', 'darwin', 'win32', …
385
+ const arch = archMap[process.arch];
386
+ if (!arch) return null;
387
+ return `${os}_${arch}`;
388
+ }
389
+
390
+ private constructor() {
391
+ super();
392
+
393
+ const suffix = RustSecurityBridge.getPlatformSuffix();
394
+ const localPaths: string[] = [];
395
+
396
+ // dist_rust/ candidates (tsrust cross-compiled output)
397
+ if (suffix) {
398
+ localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', `mailer-bin_${suffix}`));
399
+ }
400
+ localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', 'mailer-bin'));
401
+ // Local dev build paths
402
+ localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'release', 'mailer-bin'));
403
+ localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'debug', 'mailer-bin'));
404
+
405
+ this.bridge = new plugins.smartrust.RustBridge<TMailerCommands>({
406
+ binaryName: 'mailer-bin',
407
+ cliArgs: ['--management'],
408
+ requestTimeoutMs: 30_000,
409
+ readyTimeoutMs: 10_000,
410
+ localPaths,
411
+ searchSystemPath: false,
412
+ });
413
+
414
+ // Forward lifecycle events
415
+ this.bridge.on('ready', () => {
416
+ this._running = true;
417
+ logger.log('info', 'Rust security bridge is ready');
418
+ });
419
+
420
+ this.bridge.on('exit', (code: number | null, signal: string | null) => {
421
+ this._running = false;
422
+ logger.log('warn', `Rust security bridge exited (code=${code}, signal=${signal})`);
423
+
424
+ if (this._deliberateStop) {
425
+ this.setState(BridgeState.Stopped);
426
+ } else if (this._state === BridgeState.Running) {
427
+ // Unexpected exit — attempt restart
428
+ this.attemptRestart();
429
+ }
430
+ });
431
+
432
+ this.bridge.on('stderr', (line: string) => {
433
+ logger.log('debug', `[rust-bridge] ${line}`);
434
+ });
435
+ }
436
+
437
+ // -----------------------------------------------------------------------
438
+ // Static configuration & singleton
439
+ // -----------------------------------------------------------------------
440
+
441
+ /** Get or create the singleton instance. */
442
+ public static getInstance(): RustSecurityBridge {
443
+ if (!RustSecurityBridge.instance) {
444
+ RustSecurityBridge.instance = new RustSecurityBridge();
445
+ }
446
+ return RustSecurityBridge.instance;
447
+ }
448
+
449
+ /** Reset the singleton instance (for testing). */
450
+ public static resetInstance(): void {
451
+ if (RustSecurityBridge.instance) {
452
+ RustSecurityBridge.instance.stopHealthCheck();
453
+ if (RustSecurityBridge.instance._restartTimer) {
454
+ clearTimeout(RustSecurityBridge.instance._restartTimer);
455
+ RustSecurityBridge.instance._restartTimer = null;
456
+ }
457
+ RustSecurityBridge.instance.removeAllListeners();
458
+ }
459
+ RustSecurityBridge.instance = null;
460
+ }
461
+
462
+ /** Configure resilience parameters. Can be called before or after getInstance(). */
463
+ public static configure(config: Partial<IBridgeResilienceConfig>): void {
464
+ RustSecurityBridge._resilienceConfig = {
465
+ ...RustSecurityBridge._resilienceConfig,
466
+ ...config,
467
+ };
468
+ }
469
+
470
+ // -----------------------------------------------------------------------
471
+ // State management
472
+ // -----------------------------------------------------------------------
473
+
474
+ /** Current bridge state. */
475
+ public get state(): BridgeState {
476
+ return this._state;
477
+ }
478
+
479
+ /** Whether the Rust process is currently running and accepting commands. */
480
+ public get running(): boolean {
481
+ return this._running;
482
+ }
483
+
484
+ private setState(newState: BridgeState): void {
485
+ const oldState = this._state;
486
+ if (oldState === newState) return;
487
+ this._state = newState;
488
+ logger.log('info', `Rust bridge state: ${oldState} -> ${newState}`);
489
+ this.emit('stateChange', { oldState, newState });
490
+ }
491
+
492
+ /**
493
+ * Throws a descriptive error if the bridge is not in Running state.
494
+ * Called at the top of every command method.
495
+ */
496
+ private ensureRunning(): void {
497
+ if (this._state === BridgeState.Running && this._running) {
498
+ return;
499
+ }
500
+ switch (this._state) {
501
+ case BridgeState.Idle:
502
+ throw new Error('Rust bridge has not been started yet. Call start() first.');
503
+ case BridgeState.Starting:
504
+ throw new Error('Rust bridge is still starting. Wait for start() to resolve.');
505
+ case BridgeState.Restarting:
506
+ throw new Error('Rust bridge is restarting after a crash. Commands will resume once it recovers.');
507
+ case BridgeState.Failed:
508
+ throw new Error('Rust bridge has failed after exhausting all restart attempts.');
509
+ case BridgeState.Stopped:
510
+ throw new Error('Rust bridge has been stopped. Call start() to restart it.');
511
+ default:
512
+ throw new Error(`Rust bridge is not running (state=${this._state}).`);
513
+ }
514
+ }
515
+
516
+ // -----------------------------------------------------------------------
517
+ // Lifecycle
518
+ // -----------------------------------------------------------------------
519
+
520
+ /**
521
+ * Spawn the Rust binary and wait for the ready signal.
522
+ * @returns `true` if the binary started successfully, `false` otherwise.
523
+ */
524
+ public async start(): Promise<boolean> {
525
+ if (this._running && this._state === BridgeState.Running) {
526
+ return true;
527
+ }
528
+
529
+ this._deliberateStop = false;
530
+ this._restartAttempts = 0;
531
+ this.setState(BridgeState.Starting);
532
+
533
+ try {
534
+ const ok = await this.bridge.spawn();
535
+ this._running = ok;
536
+ if (ok) {
537
+ this.setState(BridgeState.Running);
538
+ this.startHealthCheck();
539
+ logger.log('info', 'Rust security bridge started');
540
+ } else {
541
+ this.setState(BridgeState.Failed);
542
+ logger.log('warn', 'Rust security bridge failed to start (binary not found or timeout)');
543
+ }
544
+ return ok;
545
+ } catch (err) {
546
+ this.setState(BridgeState.Failed);
547
+ logger.log('error', `Failed to start Rust security bridge: ${(err as Error).message}`);
548
+ return false;
549
+ }
550
+ }
551
+
552
+ /** Kill the Rust process deliberately. */
553
+ public async stop(): Promise<void> {
554
+ this._deliberateStop = true;
555
+
556
+ // Cancel any pending restart
557
+ if (this._restartTimer) {
558
+ clearTimeout(this._restartTimer);
559
+ this._restartTimer = null;
560
+ }
561
+
562
+ this.stopHealthCheck();
563
+ this._smtpServerConfig = null;
564
+
565
+ if (!this._running) {
566
+ this.setState(BridgeState.Stopped);
567
+ return;
568
+ }
569
+ try {
570
+ this.bridge.kill();
571
+ this._running = false;
572
+ this.setState(BridgeState.Stopped);
573
+ logger.log('info', 'Rust security bridge stopped');
574
+ } catch (err) {
575
+ logger.log('error', `Error stopping Rust security bridge: ${(err as Error).message}`);
576
+ }
577
+ }
578
+
579
+ // -----------------------------------------------------------------------
580
+ // Auto-restart with exponential backoff
581
+ // -----------------------------------------------------------------------
582
+
583
+ private attemptRestart(): void {
584
+ const config = RustSecurityBridge._resilienceConfig;
585
+ this._restartAttempts++;
586
+
587
+ if (this._restartAttempts > config.maxRestartAttempts) {
588
+ logger.log('error', `Rust bridge exceeded max restart attempts (${config.maxRestartAttempts}). Giving up.`);
589
+ this.setState(BridgeState.Failed);
590
+ return;
591
+ }
592
+
593
+ this.setState(BridgeState.Restarting);
594
+ this.stopHealthCheck();
595
+
596
+ const delay = Math.min(
597
+ config.restartBackoffBaseMs * Math.pow(2, this._restartAttempts - 1),
598
+ config.restartBackoffMaxMs,
599
+ );
600
+
601
+ logger.log('info', `Rust bridge restart attempt ${this._restartAttempts}/${config.maxRestartAttempts} in ${delay}ms`);
602
+
603
+ this._restartTimer = setTimeout(async () => {
604
+ this._restartTimer = null;
605
+
606
+ // Guard: if stop() was called while we were waiting, don't restart
607
+ if (this._deliberateStop) {
608
+ this.setState(BridgeState.Stopped);
609
+ return;
610
+ }
611
+
612
+ try {
613
+ const ok = await this.bridge.spawn();
614
+ this._running = ok;
615
+
616
+ if (ok) {
617
+ logger.log('info', 'Rust bridge restarted successfully');
618
+ this._restartAttempts = 0;
619
+ this.setState(BridgeState.Running);
620
+ this.startHealthCheck();
621
+ await this.restoreAfterRestart();
622
+ } else {
623
+ logger.log('warn', 'Rust bridge restart failed (spawn returned false)');
624
+ this.attemptRestart();
625
+ }
626
+ } catch (err) {
627
+ logger.log('error', `Rust bridge restart failed: ${(err as Error).message}`);
628
+ this.attemptRestart();
629
+ }
630
+ }, delay);
631
+ }
632
+
633
+ /**
634
+ * Restore state after a successful restart:
635
+ * - Re-send startSmtpServer command if the SMTP server was running
636
+ */
637
+ private async restoreAfterRestart(): Promise<void> {
638
+ if (this._smtpServerConfig) {
639
+ try {
640
+ logger.log('info', 'Restoring SMTP server after bridge restart');
641
+ const result = await this.bridge.sendCommand('startSmtpServer', this._smtpServerConfig);
642
+ if (result?.started) {
643
+ logger.log('info', 'SMTP server restored after bridge restart');
644
+ } else {
645
+ logger.log('warn', 'SMTP server failed to restore after bridge restart');
646
+ }
647
+ } catch (err) {
648
+ logger.log('error', `Failed to restore SMTP server after restart: ${(err as Error).message}`);
649
+ }
650
+ }
651
+ }
652
+
653
+ // -----------------------------------------------------------------------
654
+ // Health check
655
+ // -----------------------------------------------------------------------
656
+
657
+ private startHealthCheck(): void {
658
+ this.stopHealthCheck();
659
+ const config = RustSecurityBridge._resilienceConfig;
660
+
661
+ this._healthCheckTimer = setInterval(async () => {
662
+ if (this._state !== BridgeState.Running || !this._running) {
663
+ return;
664
+ }
665
+
666
+ try {
667
+ const pongPromise = this.bridge.sendCommand('ping', {} as any);
668
+ const timeoutPromise = new Promise<never>((_, reject) =>
669
+ setTimeout(() => reject(new Error('Health check timeout')), config.healthCheckTimeoutMs),
670
+ );
671
+ const res = await Promise.race([pongPromise, timeoutPromise]);
672
+ if (!(res as any)?.pong) {
673
+ throw new Error('Health check: unexpected ping response');
674
+ }
675
+ } catch (err) {
676
+ logger.log('warn', `Rust bridge health check failed: ${(err as Error).message}. Killing process to trigger restart.`);
677
+ try {
678
+ this.bridge.kill();
679
+ } catch {
680
+ // Already dead
681
+ }
682
+ // The exit handler will trigger attemptRestart()
683
+ }
684
+ }, config.healthCheckIntervalMs);
685
+ }
686
+
687
+ private stopHealthCheck(): void {
688
+ if (this._healthCheckTimer) {
689
+ clearInterval(this._healthCheckTimer);
690
+ this._healthCheckTimer = null;
691
+ }
692
+ }
693
+
694
+ // -----------------------------------------------------------------------
695
+ // Commands — thin typed wrappers over sendCommand
696
+ // -----------------------------------------------------------------------
697
+
698
+ /** Ping the Rust process. */
699
+ public async ping(): Promise<boolean> {
700
+ this.ensureRunning();
701
+ const res = await this.bridge.sendCommand('ping', {} as any);
702
+ return res?.pong === true;
703
+ }
704
+
705
+ /** Get version information for all Rust crates. */
706
+ public async getVersion(): Promise<IVersionInfo> {
707
+ this.ensureRunning();
708
+ return this.bridge.sendCommand('version', {} as any);
709
+ }
710
+
711
+ /** Validate an email address. */
712
+ public async validateEmail(email: string): Promise<IValidationResult> {
713
+ this.ensureRunning();
714
+ return this.bridge.sendCommand('validateEmail', { email });
715
+ }
716
+
717
+ /** Detect bounce type from SMTP response / diagnostic code. */
718
+ public async detectBounce(opts: {
719
+ smtpResponse?: string;
720
+ diagnosticCode?: string;
721
+ statusCode?: string;
722
+ }): Promise<IBounceDetection> {
723
+ this.ensureRunning();
724
+ return this.bridge.sendCommand('detectBounce', opts);
725
+ }
726
+
727
+ /** Scan email content for threats (phishing, spam, malware, etc.). */
728
+ public async scanContent(opts: {
729
+ subject?: string;
730
+ textBody?: string;
731
+ htmlBody?: string;
732
+ attachmentNames?: string[];
733
+ }): Promise<IContentScanResult> {
734
+ this.ensureRunning();
735
+ return this.bridge.sendCommand('scanContent', opts);
736
+ }
737
+
738
+ /** Check IP reputation via DNSBL. */
739
+ public async checkIpReputation(ip: string): Promise<IReputationResult> {
740
+ this.ensureRunning();
741
+ return this.bridge.sendCommand('checkIpReputation', { ip });
742
+ }
743
+
744
+ /** Verify DKIM signatures on a raw email message. */
745
+ public async verifyDkim(rawMessage: string): Promise<IDkimVerificationResult[]> {
746
+ this.ensureRunning();
747
+ return this.bridge.sendCommand('verifyDkim', { rawMessage });
748
+ }
749
+
750
+ /** Sign an email with DKIM (RSA or Ed25519). */
751
+ public async signDkim(opts: {
752
+ rawMessage: string;
753
+ domain: string;
754
+ selector?: string;
755
+ privateKey: string;
756
+ keyType?: string;
757
+ }): Promise<{ header: string; signedMessage: string }> {
758
+ this.ensureRunning();
759
+ return this.bridge.sendCommand('signDkim', opts);
760
+ }
761
+
762
+ /** Check SPF for a sender. */
763
+ public async checkSpf(opts: {
764
+ ip: string;
765
+ heloDomain: string;
766
+ hostname?: string;
767
+ mailFrom: string;
768
+ }): Promise<ISpfResult> {
769
+ this.ensureRunning();
770
+ return this.bridge.sendCommand('checkSpf', opts);
771
+ }
772
+
773
+ /**
774
+ * Compound email security verification: DKIM + SPF + DMARC in one IPC call.
775
+ *
776
+ * This is the preferred method for inbound email verification — it avoids
777
+ * 3 sequential round-trips and correctly passes raw mail-auth types internally.
778
+ */
779
+ public async verifyEmail(opts: {
780
+ rawMessage: string;
781
+ ip: string;
782
+ heloDomain: string;
783
+ hostname?: string;
784
+ mailFrom: string;
785
+ }): Promise<IEmailSecurityResult> {
786
+ this.ensureRunning();
787
+ return this.bridge.sendCommand('verifyEmail', opts);
788
+ }
789
+
790
+ // -----------------------------------------------------------------------
791
+ // SMTP Client — outbound email delivery via Rust
792
+ // -----------------------------------------------------------------------
793
+
794
+ /** Send a structured email via the Rust SMTP client. */
795
+ public async sendOutboundEmail(opts: ISmtpSendOptions): Promise<ISmtpSendResult> {
796
+ this.ensureRunning();
797
+ return this.bridge.sendCommand('sendEmail', opts);
798
+ }
799
+
800
+ /** Send a pre-formatted raw email via the Rust SMTP client. */
801
+ public async sendRawEmail(opts: ISmtpSendRawOptions): Promise<ISmtpSendResult> {
802
+ this.ensureRunning();
803
+ return this.bridge.sendCommand('sendRawEmail', opts);
804
+ }
805
+
806
+ /** Verify connectivity to an SMTP server. */
807
+ public async verifySmtpConnection(opts: ISmtpVerifyOptions): Promise<ISmtpVerifyResult> {
808
+ this.ensureRunning();
809
+ return this.bridge.sendCommand('verifySmtpConnection', opts);
810
+ }
811
+
812
+ /** Close a specific connection pool (or all pools if no key is given). */
813
+ public async closeSmtpPool(poolKey?: string): Promise<void> {
814
+ this.ensureRunning();
815
+ await this.bridge.sendCommand('closeSmtpPool', poolKey ? { poolKey } : ({} as any));
816
+ }
817
+
818
+ /** Get status of all SMTP client connection pools. */
819
+ public async getSmtpPoolStatus(): Promise<ISmtpPoolStatus> {
820
+ this.ensureRunning();
821
+ return this.bridge.sendCommand('getSmtpPoolStatus', {} as any);
822
+ }
823
+
824
+ // -----------------------------------------------------------------------
825
+ // SMTP Server lifecycle
826
+ // -----------------------------------------------------------------------
827
+
828
+ /**
829
+ * Start the Rust SMTP server.
830
+ * The server will listen on the configured ports and emit events for
831
+ * emailReceived and authRequest that must be handled by the caller.
832
+ */
833
+ public async startSmtpServer(config: ISmtpServerConfig): Promise<boolean> {
834
+ this.ensureRunning();
835
+ this._smtpServerConfig = config;
836
+ const result = await this.bridge.sendCommand('startSmtpServer', config);
837
+ return result?.started === true;
838
+ }
839
+
840
+ /** Stop the Rust SMTP server. */
841
+ public async stopSmtpServer(): Promise<void> {
842
+ this.ensureRunning();
843
+ this._smtpServerConfig = null;
844
+ await this.bridge.sendCommand('stopSmtpServer', {} as any);
845
+ }
846
+
847
+ /**
848
+ * Send the result of email processing back to the Rust SMTP server.
849
+ * This resolves a pending correlation-ID callback, allowing the Rust
850
+ * server to send the SMTP response to the client.
851
+ */
852
+ public async sendEmailProcessingResult(opts: {
853
+ correlationId: string;
854
+ accepted: boolean;
855
+ smtpCode?: number;
856
+ smtpMessage?: string;
857
+ }): Promise<void> {
858
+ this.ensureRunning();
859
+ await this.bridge.sendCommand('emailProcessingResult', opts);
860
+ }
861
+
862
+ /**
863
+ * Send the result of authentication validation back to the Rust SMTP server.
864
+ */
865
+ public async sendAuthResult(opts: {
866
+ correlationId: string;
867
+ success: boolean;
868
+ message?: string;
869
+ }): Promise<void> {
870
+ this.ensureRunning();
871
+ await this.bridge.sendCommand('authResult', opts);
872
+ }
873
+
874
+ /**
875
+ * Send SCRAM credentials back to the Rust SMTP server.
876
+ * Values (salt, storedKey, serverKey) must be base64-encoded.
877
+ */
878
+ public async sendScramCredentialResult(opts: {
879
+ correlationId: string;
880
+ found: boolean;
881
+ salt?: string;
882
+ iterations?: number;
883
+ storedKey?: string;
884
+ serverKey?: string;
885
+ }): Promise<void> {
886
+ this.ensureRunning();
887
+ await this.bridge.sendCommand('scramCredentialResult', opts);
888
+ }
889
+
890
+ /** Update rate limit configuration at runtime. */
891
+ public async configureRateLimits(config: IRateLimitConfig): Promise<void> {
892
+ this.ensureRunning();
893
+ await this.bridge.sendCommand('configureRateLimits', config);
894
+ }
895
+
896
+ // -----------------------------------------------------------------------
897
+ // Event registration — delegates to the underlying bridge EventEmitter
898
+ // -----------------------------------------------------------------------
899
+
900
+ /**
901
+ * Register a handler for emailReceived events from the Rust SMTP server.
902
+ * These events fire when a complete email has been received and needs processing.
903
+ */
904
+ public onEmailReceived(handler: (data: IEmailReceivedEvent) => void): void {
905
+ this.bridge.on('management:emailReceived', handler);
906
+ }
907
+
908
+ /**
909
+ * Register a handler for authRequest events from the Rust SMTP server.
910
+ * The handler must call sendAuthResult() with the correlationId.
911
+ */
912
+ public onAuthRequest(handler: (data: IAuthRequestEvent) => void): void {
913
+ this.bridge.on('management:authRequest', handler);
914
+ }
915
+
916
+ /**
917
+ * Register a handler for scramCredentialRequest events from the Rust SMTP server.
918
+ * The handler must call sendScramCredentialResult() with the correlationId.
919
+ */
920
+ public onScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void {
921
+ this.bridge.on('management:scramCredentialRequest', handler);
922
+ }
923
+
924
+ /** Remove an emailReceived event handler. */
925
+ public offEmailReceived(handler: (data: IEmailReceivedEvent) => void): void {
926
+ this.bridge.off('management:emailReceived', handler);
927
+ }
928
+
929
+ /** Remove an authRequest event handler. */
930
+ public offAuthRequest(handler: (data: IAuthRequestEvent) => void): void {
931
+ this.bridge.off('management:authRequest', handler);
932
+ }
933
+
934
+ /** Remove a scramCredentialRequest event handler. */
935
+ public offScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void {
936
+ this.bridge.off('management:scramCredentialRequest', handler);
937
+ }
938
+ }
939
+
940
+ // Re-export interfaces for consumers
941
+ export type {
942
+ IDkimVerificationResult,
943
+ ISpfResult,
944
+ IDmarcResult,
945
+ IEmailSecurityResult,
946
+ IValidationResult,
947
+ IBounceDetection,
948
+ IContentScanResult,
949
+ IReputationResult as IRustReputationResult,
950
+ IVersionInfo,
951
+ ISmtpServerConfig,
952
+ IRateLimitConfig,
953
+ IEmailData,
954
+ IEmailReceivedEvent,
955
+ IAuthRequestEvent,
956
+ IScramCredentialRequestEvent,
957
+ IOutboundEmail,
958
+ ISmtpSendResult,
959
+ ISmtpSendOptions,
960
+ ISmtpSendRawOptions,
961
+ ISmtpVerifyOptions,
962
+ ISmtpVerifyResult,
963
+ ISmtpPoolStatus,
964
+ };