@push.rocks/smartmta 6.5.2 → 8.0.0

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 (84) hide show
  1. package/changelog.md +40 -0
  2. package/dist_rust/mailer-bin_linux_amd64 +0 -0
  3. package/dist_rust/mailer-bin_linux_arm64 +0 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/functions.errors.d.ts +3 -0
  6. package/dist_ts/functions.errors.js +8 -0
  7. package/dist_ts/index.d.ts +3 -0
  8. package/dist_ts/index.js +4 -1
  9. package/dist_ts/mail/core/classes.bouncemanager.d.ts +16 -12
  10. package/dist_ts/mail/core/classes.bouncemanager.js +146 -129
  11. package/dist_ts/mail/core/classes.email.js +15 -13
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +3 -3
  13. package/dist_ts/mail/core/classes.emailvalidator.js +7 -5
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
  15. package/dist_ts/mail/core/classes.templatemanager.js +35 -51
  16. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +92 -22
  17. package/dist_ts/mail/delivery/classes.delivery.queue.js +738 -151
  18. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
  19. package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
  20. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
  21. package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
  22. package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
  23. package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
  24. package/dist_ts/mail/delivery/interfaces.js +1 -1
  25. package/dist_ts/mail/interfaces.storage.d.ts +37 -6
  26. package/dist_ts/mail/interfaces.storage.js +33 -3
  27. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +10 -6
  28. package/dist_ts/mail/routing/classes.dkim.manager.js +47 -31
  29. package/dist_ts/mail/routing/classes.dns.manager.d.ts +7 -5
  30. package/dist_ts/mail/routing/classes.dns.manager.js +22 -11
  31. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +2 -1
  32. package/dist_ts/mail/routing/classes.email.action.executor.js +45 -16
  33. package/dist_ts/mail/routing/classes.email.router.d.ts +5 -2
  34. package/dist_ts/mail/routing/classes.email.router.js +17 -12
  35. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +12 -6
  36. package/dist_ts/mail/routing/classes.unified.email.server.js +69 -78
  37. package/dist_ts/mail/routing/interfaces.d.ts +0 -2
  38. package/dist_ts/mail/security/classes.dkimcreator.d.ts +22 -45
  39. package/dist_ts/mail/security/classes.dkimcreator.js +93 -296
  40. package/dist_ts/mail/security/classes.spfverifier.js +5 -3
  41. package/dist_ts/paths.d.ts +0 -12
  42. package/dist_ts/paths.js +3 -36
  43. package/dist_ts/plugins.d.ts +2 -5
  44. package/dist_ts/plugins.js +3 -6
  45. package/dist_ts/security/classes.contentscanner.js +14 -12
  46. package/dist_ts/security/classes.ipreputationchecker.d.ts +9 -6
  47. package/dist_ts/security/classes.ipreputationchecker.js +42 -93
  48. package/dist_ts/security/classes.rustsecuritybridge.d.ts +52 -4
  49. package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
  50. package/dist_ts/security/classes.securitylogger.js +7 -5
  51. package/dist_ts/security/index.d.ts +1 -1
  52. package/dist_ts/security/index.js +2 -2
  53. package/package.json +8 -9
  54. package/readme.hints.md +4 -3
  55. package/readme.md +50 -18
  56. package/readme.plan.md +6 -0
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/functions.errors.ts +8 -0
  59. package/ts/index.ts +3 -0
  60. package/ts/mail/core/classes.bouncemanager.ts +188 -155
  61. package/ts/mail/core/classes.email.ts +20 -14
  62. package/ts/mail/core/classes.emailvalidator.ts +9 -7
  63. package/ts/mail/core/classes.templatemanager.ts +42 -57
  64. package/ts/mail/delivery/classes.delivery.queue.ts +996 -185
  65. package/ts/mail/delivery/classes.delivery.system.ts +583 -170
  66. package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
  67. package/ts/mail/delivery/functions.safe-observers.ts +45 -0
  68. package/ts/mail/delivery/interfaces.ts +27 -1
  69. package/ts/mail/interfaces.storage.ts +64 -10
  70. package/ts/mail/routing/classes.dkim.manager.ts +65 -40
  71. package/ts/mail/routing/classes.dns.manager.ts +39 -16
  72. package/ts/mail/routing/classes.email.action.executor.ts +64 -17
  73. package/ts/mail/routing/classes.email.router.ts +20 -13
  74. package/ts/mail/routing/classes.unified.email.server.ts +107 -86
  75. package/ts/mail/routing/interfaces.ts +0 -2
  76. package/ts/mail/security/classes.dkimcreator.ts +150 -355
  77. package/ts/mail/security/classes.spfverifier.ts +4 -2
  78. package/ts/paths.ts +2 -41
  79. package/ts/plugins.ts +1 -6
  80. package/ts/security/classes.contentscanner.ts +14 -12
  81. package/ts/security/classes.ipreputationchecker.ts +46 -99
  82. package/ts/security/classes.rustsecuritybridge.ts +272 -6
  83. package/ts/security/classes.securitylogger.ts +6 -4
  84. package/ts/security/index.ts +5 -1
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../../logger.js';
2
+ import { getErrorMessage } from '../../functions.errors.js';
2
3
  import {
3
4
  SecurityLogger,
4
5
  SecurityLogLevel,
@@ -8,7 +9,12 @@ import type { IEmailAction, IEmailContext } from './interfaces.js';
8
9
  import { Email } from '../core/classes.email.js';
9
10
  import { BounceManager } from '../core/classes.bouncemanager.js';
10
11
  import { UnifiedDeliveryQueue } from '../delivery/classes.delivery.queue.js';
11
- import type { ISmtpSendResult } from '../../security/classes.rustsecuritybridge.js';
12
+ import { callObserverSafely } from '../delivery/functions.safe-observers.js';
13
+ import {
14
+ SmtpDeliveryError,
15
+ type ISmtpRecipientResult,
16
+ type ISmtpSendResult,
17
+ } from '../../security/classes.rustsecuritybridge.js';
12
18
 
13
19
  /**
14
20
  * Dependencies injected from UnifiedEmailServer to avoid circular imports
@@ -30,6 +36,31 @@ export interface IActionExecutorDeps {
30
36
  export class EmailActionExecutor {
31
37
  constructor(private deps: IActionExecutorDeps) {}
32
38
 
39
+ private async suppressPermanentRcptFailures(
40
+ email: Email,
41
+ results: ISmtpRecipientResult[],
42
+ ): Promise<void> {
43
+ for (const result of results) {
44
+ if (result.accepted || result.responseCode < 500) continue;
45
+ try {
46
+ await this.deps.bounceManager.processSmtpFailure(
47
+ result.recipient,
48
+ `SMTP recipient rejected with ${result.responseCode}`,
49
+ {
50
+ sender: email.from,
51
+ originalEmailId: email.headers['Message-ID'] as string,
52
+ statusCode: String(result.responseCode),
53
+ },
54
+ );
55
+ } catch (error) {
56
+ logger.log(
57
+ 'error',
58
+ `Failed to persist permanent RCPT failure for ${result.recipient}: ${(error as Error).message}`,
59
+ );
60
+ }
61
+ }
62
+ }
63
+
33
64
  async executeAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
34
65
  switch (action.type) {
35
66
  case 'forward':
@@ -72,13 +103,14 @@ export class EmailActionExecutor {
72
103
 
73
104
  try {
74
105
  // Send email via Rust SMTP client
75
- await this.deps.sendOutboundEmail(host, port, email, {
106
+ const result = await this.deps.sendOutboundEmail(host, port, email, {
76
107
  auth: auth as { user: string; pass: string } | undefined,
77
108
  });
109
+ await this.suppressPermanentRcptFailures(email, result.recipientResults || []);
78
110
 
79
111
  logger.log('info', `Successfully forwarded email to ${host}:${port}`);
80
112
 
81
- SecurityLogger.getInstance().logEvent({
113
+ await callObserverSafely('forward success security log', () => SecurityLogger.getInstance().logEvent({
82
114
  level: SecurityLogLevel.INFO,
83
115
  type: SecurityEventType.EMAIL_FORWARDING,
84
116
  message: 'Email forwarded successfully',
@@ -91,11 +123,12 @@ export class EmailActionExecutor {
91
123
  recipients: email.to
92
124
  },
93
125
  success: true
94
- });
126
+ }));
95
127
  } catch (error) {
96
- logger.log('error', `Failed to forward email: ${error.message}`);
128
+ const errorMessage = getErrorMessage(error);
129
+ logger.log('error', `Failed to forward email: ${errorMessage}`);
97
130
 
98
- SecurityLogger.getInstance().logEvent({
131
+ await callObserverSafely('forward failure security log', () => SecurityLogger.getInstance().logEvent({
99
132
  level: SecurityLogLevel.ERROR,
100
133
  type: SecurityEventType.EMAIL_FORWARDING,
101
134
  message: 'Email forwarding failed',
@@ -105,17 +138,31 @@ export class EmailActionExecutor {
105
138
  routeName: context.session.matchedRoute?.name,
106
139
  targetHost: host,
107
140
  targetPort: port,
108
- error: error.message
141
+ error: errorMessage
109
142
  },
110
143
  success: false
111
- });
112
-
113
- // Handle as bounce
114
- for (const recipient of email.getAllRecipients()) {
115
- await this.deps.bounceManager.processSmtpFailure(recipient, error.message, {
116
- sender: email.from,
117
- originalEmailId: email.headers['Message-ID'] as string
118
- });
144
+ }));
145
+
146
+ const smtpError = SmtpDeliveryError.from(error);
147
+ if (
148
+ !smtpError.retryable
149
+ && smtpError.errorType === 'protocol'
150
+ && smtpError.phase === 'rcpt_to'
151
+ && (
152
+ (typeof smtpError.smtpCode === 'number' && smtpError.smtpCode >= 500)
153
+ || smtpError.recipientResults.some(
154
+ (result) => !result.accepted && result.responseCode >= 500,
155
+ )
156
+ )
157
+ ) {
158
+ const permanentResults = smtpError.recipientResults.length > 0
159
+ ? smtpError.recipientResults
160
+ : smtpError.recipients.map((recipient) => ({
161
+ recipient,
162
+ accepted: false,
163
+ responseCode: smtpError.smtpCode!,
164
+ }));
165
+ await this.suppressPermanentRcptFailures(email, permanentResults);
119
166
  }
120
167
  throw error;
121
168
  }
@@ -151,7 +198,7 @@ export class EmailActionExecutor {
151
198
 
152
199
  logger.log('info', `Rejecting email with code ${code}: ${message}`);
153
200
 
154
- SecurityLogger.getInstance().logEvent({
201
+ await callObserverSafely('routing rejection security log', () => SecurityLogger.getInstance().logEvent({
155
202
  level: SecurityLogLevel.WARN,
156
203
  type: SecurityEventType.EMAIL_PROCESSING,
157
204
  message: 'Email rejected by routing rule',
@@ -165,7 +212,7 @@ export class EmailActionExecutor {
165
212
  to: _email.to
166
213
  },
167
214
  success: false
168
- });
215
+ }));
169
216
 
170
217
  // Throw error with SMTP code and message
171
218
  const error = new Error(message);
@@ -1,8 +1,9 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { EventEmitter } from 'node:events';
3
- import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
3
+ import type { IStorageManager } from '../interfaces.storage.js';
4
4
  import type { IEmailRoute, IEmailMatch, IEmailAction, IEmailContext } from './interfaces.js';
5
5
  import type { Email } from '../core/classes.email.js';
6
+ import { getErrorMessage } from '../../functions.errors.js';
6
7
 
7
8
  /**
8
9
  * Email router that evaluates routes and determines actions
@@ -10,8 +11,9 @@ import type { Email } from '../core/classes.email.js';
10
11
  export class EmailRouter extends EventEmitter {
11
12
  private routes: IEmailRoute[];
12
13
  private patternCache: Map<string, boolean> = new Map();
13
- private storageManager?: IStorageManagerLike;
14
+ private storageManager?: IStorageManager;
14
15
  private persistChanges: boolean;
16
+ private initializationPromise: Promise<void>;
15
17
 
16
18
  /**
17
19
  * Create a new email router
@@ -19,7 +21,7 @@ export class EmailRouter extends EventEmitter {
19
21
  * @param options Router options
20
22
  */
21
23
  constructor(routes: IEmailRoute[], options?: {
22
- storageManager?: IStorageManagerLike;
24
+ storageManager?: IStorageManager;
23
25
  persistChanges?: boolean;
24
26
  }) {
25
27
  super();
@@ -27,12 +29,17 @@ export class EmailRouter extends EventEmitter {
27
29
  this.storageManager = options?.storageManager;
28
30
  this.persistChanges = options?.persistChanges ?? !!this.storageManager;
29
31
 
30
- // If storage manager is provided, try to load persisted routes
31
- if (hasStorageManagerMethods(this.storageManager, ['get'])) {
32
- this.loadRoutes({ merge: true }).catch(error => {
33
- console.error(`Failed to load persisted routes: ${error.message}`);
34
- });
35
- }
32
+ // Capture hydration so callers can establish a real readiness barrier before routing.
33
+ const hydrationPromise = this.storageManager
34
+ ? this.loadRoutes({ merge: true }).then(() => undefined)
35
+ : Promise.resolve();
36
+ hydrationPromise.catch(() => undefined);
37
+ this.initializationPromise = hydrationPromise;
38
+ }
39
+
40
+ /** Wait until managed routes have been hydrated. */
41
+ public async initialize(): Promise<void> {
42
+ await this.initializationPromise;
36
43
  }
37
44
 
38
45
  /**
@@ -395,7 +402,7 @@ export class EmailRouter extends EventEmitter {
395
402
  * Save current routes to storage
396
403
  */
397
404
  public async saveRoutes(): Promise<void> {
398
- if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
405
+ if (!this.storageManager) {
399
406
  this.emit('persistenceWarning', 'Cannot save routes: StorageManager not configured');
400
407
  return;
401
408
  }
@@ -413,7 +420,7 @@ export class EmailRouter extends EventEmitter {
413
420
 
414
421
  this.emit('routesPersisted', this.routes.length);
415
422
  } catch (error) {
416
- console.error(`Failed to save routes: ${error.message}`);
423
+ console.error(`Failed to save routes: ${getErrorMessage(error)}`);
417
424
  throw error;
418
425
  }
419
426
  }
@@ -426,7 +433,7 @@ export class EmailRouter extends EventEmitter {
426
433
  merge?: boolean; // Merge with existing routes
427
434
  replace?: boolean; // Replace existing routes
428
435
  }): Promise<IEmailRoute[]> {
429
- if (!hasStorageManagerMethods(this.storageManager, ['get'])) {
436
+ if (!this.storageManager) {
430
437
  this.emit('persistenceWarning', 'Cannot load routes: StorageManager not configured');
431
438
  return [];
432
439
  }
@@ -473,7 +480,7 @@ export class EmailRouter extends EventEmitter {
473
480
 
474
481
  return loadedRoutes;
475
482
  } catch (error) {
476
- console.error(`Failed to load routes: ${error.message}`);
483
+ console.error(`Failed to load routes: ${getErrorMessage(error)}`);
477
484
  throw error;
478
485
  }
479
486
  }
@@ -1,14 +1,14 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import * as paths from '../../paths.js';
3
2
  import { EventEmitter } from 'events';
4
3
  import { logger } from '../../logger.js';
4
+ import { getErrorMessage } from '../../functions.errors.js';
5
5
  import {
6
6
  SecurityLogger,
7
7
  SecurityLogLevel,
8
8
  SecurityEventType
9
9
  } from '../../security/index.js';
10
10
  import { DKIMCreator } from '../security/classes.dkimcreator.js';
11
- import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
11
+ import type { IStorageManager } from '../interfaces.storage.js';
12
12
  import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
13
13
  import type { IEmailReceivedEvent, IRcptToRequestEvent, IAuthRequestEvent, IScramCredentialRequestEvent } from '../../security/classes.rustsecuritybridge.js';
14
14
  import { EmailRouter } from './classes.email.router.js';
@@ -25,11 +25,12 @@ import { SmtpState } from '../delivery/interfaces.js';
25
25
  import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
26
26
  import { EmailActionExecutor } from './classes.email.action.executor.js';
27
27
  import { DkimManager } from './classes.dkim.manager.js';
28
+ import { callObserverSafely, emitSafely } from '../delivery/functions.safe-observers.js';
28
29
 
29
30
 
30
31
  /** External DcRouter interface shape used by UnifiedEmailServer */
31
32
  interface DcRouter {
32
- storageManager: IStorageManagerLike;
33
+ storageManager: IStorageManager;
33
34
  dnsServer?: any;
34
35
  options?: any;
35
36
  }
@@ -105,6 +106,8 @@ export interface IUnifiedEmailServerOptions {
105
106
  domains: IEmailDomainConfig[]; // Domain configurations
106
107
  banner?: string;
107
108
  debug?: boolean;
109
+ /** Automatic for standalone use; caller-managed keeps DNS/key ownership with the embedding app. */
110
+ dkimKeyProvisioning?: 'automatic' | 'caller-managed';
108
111
  useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
109
112
  /** Persist router changes back into storage when a storage manager is available. */
110
113
  persistRoutes?: boolean;
@@ -118,9 +121,9 @@ export interface IUnifiedEmailServerOptions {
118
121
 
119
122
  // TLS options
120
123
  tls?: {
121
- certPath?: string;
122
- keyPath?: string;
123
- caPath?: string;
124
+ certPem?: string;
125
+ keyPem?: string;
126
+ caPem?: string;
124
127
  minVersion?: string;
125
128
  ciphers?: string;
126
129
  };
@@ -146,6 +149,8 @@ export interface IUnifiedEmailServerOptions {
146
149
  recipientValidationTimeoutMs?: number;
147
150
  /** Timeout for message acceptance callbacks. Defaults to 30000 ms. */
148
151
  messageAcceptanceTimeoutMs?: number;
152
+ /** Maximum complete messages concurrently held across Rust and TypeScript. */
153
+ maxConcurrentMessages?: number;
149
154
  /** Require trusted PROXY v1 from the backend proxy before SMTP greeting. */
150
155
  proxyProtocol?: {
151
156
  required?: boolean;
@@ -179,8 +184,8 @@ export interface IUnifiedEmailServerOptions {
179
184
  connectionProxyProvider?: (context: IOutboundConnectionProxyContext) => Promise<IOutboundConnectionProxy | null | undefined>;
180
185
  };
181
186
 
182
- // Delivery queue
183
- queue?: IQueueOptions;
187
+ // Queue persistence must be selected explicitly.
188
+ queue: IQueueOptions;
184
189
 
185
190
  // Rate limiting (global limits, can be overridden per domain)
186
191
  rateLimits?: IHierarchicalRateLimits;
@@ -283,7 +288,7 @@ export class UnifiedEmailServer extends EventEmitter {
283
288
  this.rustBridge = RustSecurityBridge.getInstance();
284
289
 
285
290
  // Initialize DKIM creator with storage manager
286
- this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager);
291
+ this.dkimCreator = new DKIMCreator(dcRouter.storageManager);
287
292
 
288
293
  // Initialize bounce manager with storage manager
289
294
  this.bounceManager = new BounceManager({
@@ -298,7 +303,7 @@ export class UnifiedEmailServer extends EventEmitter {
298
303
  // Initialize email router with routes and storage manager
299
304
  this.emailRouter = new EmailRouter(options.routes || [], {
300
305
  storageManager: dcRouter.storageManager,
301
- persistChanges: options.persistRoutes ?? hasStorageManagerMethods(dcRouter.storageManager, ['get', 'set'])
306
+ persistChanges: options.persistRoutes ?? true
302
307
  });
303
308
 
304
309
  // Initialize rate limiter
@@ -315,7 +320,6 @@ export class UnifiedEmailServer extends EventEmitter {
315
320
 
316
321
  // Initialize delivery components
317
322
  const queueOptions: IQueueOptions = {
318
- storageType: 'memory', // Default to memory storage
319
323
  maxRetries: 3,
320
324
  baseRetryDelay: 300000, // 5 minutes
321
325
  maxRetryDelay: 3600000, // 1 hour
@@ -329,7 +333,16 @@ export class UnifiedEmailServer extends EventEmitter {
329
333
  concurrentDeliveries: 10,
330
334
  processBounces: true,
331
335
  bounceHandler: {
332
- processSmtpFailure: this.processSmtpFailure.bind(this)
336
+ processSmtpFailure: async (recipient, smtpResponse, failureOptions) => {
337
+ const processed = await this.processSmtpFailure(
338
+ recipient,
339
+ smtpResponse,
340
+ failureOptions,
341
+ );
342
+ if (!processed) {
343
+ throw new Error(`SMTP bounce operation failed for ${recipient}`);
344
+ }
345
+ },
333
346
  },
334
347
  onDeliverySuccess: async (_item, _result) => {
335
348
  // Delivery success recorded via delivery system
@@ -346,8 +359,14 @@ export class UnifiedEmailServer extends EventEmitter {
346
359
  });
347
360
 
348
361
  // Initialize DKIM manager
349
- this.dkimManager = new DkimManager(this.dkimCreator, this.domainRegistry, dcRouter, this.rustBridge);
350
- this.dnsManager = new DnsManager(this.dcRouter);
362
+ this.dkimManager = new DkimManager(
363
+ this.dkimCreator,
364
+ this.domainRegistry,
365
+ dcRouter,
366
+ this.rustBridge,
367
+ options.dkimKeyProvisioning || 'automatic',
368
+ );
369
+ this.dnsManager = new DnsManager(this.dcRouter, undefined, options.dkimKeyProvisioning || 'automatic');
351
370
 
352
371
  // Initialize statistics
353
372
  this.stats = {
@@ -406,18 +425,24 @@ export class UnifiedEmailServer extends EventEmitter {
406
425
  tlsOpportunistic?: boolean;
407
426
  }): Promise<ISmtpSendResult> {
408
427
  // Build DKIM config if domain has keys
409
- let dkim: { domain: string; selector: string; privateKey: string } | undefined;
428
+ let dkim: {
429
+ domain: string;
430
+ selector: string;
431
+ privateKey: string;
432
+ keyType: 'rsa' | 'ed25519';
433
+ } | undefined;
410
434
  if (options?.dkimDomain) {
411
- try {
412
- const selector = options.dkimSelector || 'default';
413
- const { privateKey } = await this.dkimCreator.readDKIMKeysForSelector(
414
- options.dkimDomain,
415
- selector,
416
- );
417
- dkim = { domain: options.dkimDomain, selector, privateKey };
418
- } catch (err) {
419
- logger.log('warn', `Failed to read DKIM keys for ${options.dkimDomain}: ${(err as Error).message}`);
420
- }
435
+ const selector = options.dkimSelector || 'default';
436
+ const material = await this.dkimCreator.readValidatedDKIMKeysForSelector(
437
+ options.dkimDomain,
438
+ selector,
439
+ );
440
+ dkim = {
441
+ domain: options.dkimDomain,
442
+ selector,
443
+ privateKey: material.privateKey,
444
+ keyType: material.keyType,
445
+ };
421
446
  }
422
447
 
423
448
  // Serialize the Email to the outbound format
@@ -476,13 +501,14 @@ export class UnifiedEmailServer extends EventEmitter {
476
501
  await this.startDeliveryPipeline();
477
502
  await this.startRustBridge();
478
503
  await this.initializeDkimAndDns();
504
+ await this.startDeliveryProcessing();
479
505
  this.registerBridgeEventHandlers();
480
506
  await this.startSmtpServer();
481
507
  this.startDnsValidation();
482
508
  logger.log('info', 'UnifiedEmailServer started successfully');
483
- this.emit('started');
509
+ await emitSafely(this, 'started');
484
510
  } catch (error) {
485
- logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`);
511
+ logger.log('error', `Failed to start UnifiedEmailServer: ${getErrorMessage(error)}`);
486
512
  try {
487
513
  await this.stop();
488
514
  } catch (cleanupError) {
@@ -493,10 +519,19 @@ export class UnifiedEmailServer extends EventEmitter {
493
519
  }
494
520
 
495
521
  private async startDeliveryPipeline(): Promise<void> {
496
- await this.deliveryQueue.initialize();
522
+ await Promise.all([
523
+ this.bounceManager.initialize(),
524
+ this.emailRouter.initialize(),
525
+ ]);
526
+ logger.log('info', 'Managed email routing and suppression state initialized');
527
+
528
+ await this.deliveryQueue.initialize({ startProcessing: false });
497
529
  logger.log('info', 'Email delivery queue initialized');
530
+ }
498
531
 
532
+ private async startDeliveryProcessing(): Promise<void> {
499
533
  await this.deliverySystem.start();
534
+ this.deliveryQueue.resume();
500
535
  logger.log('info', 'Email delivery system started');
501
536
  }
502
537
 
@@ -509,9 +544,9 @@ export class UnifiedEmailServer extends EventEmitter {
509
544
 
510
545
  this.unregisterBridgeStateHandler();
511
546
  this.bridgeStateChangeHandler = ({ oldState, newState }: { oldState: string; newState: string }) => {
512
- if (newState === 'failed') this.emit('bridgeFailed');
513
- else if (newState === 'restarting') this.emit('bridgeRestarting');
514
- else if (newState === 'running' && oldState === 'restarting') this.emit('bridgeRecovered');
547
+ if (newState === 'failed') void emitSafely(this, 'bridgeFailed');
548
+ else if (newState === 'restarting') void emitSafely(this, 'bridgeRestarting');
549
+ else if (newState === 'running' && oldState === 'restarting') void emitSafely(this, 'bridgeRecovered');
515
550
  };
516
551
  this.rustBridge.on('stateChange', this.bridgeStateChangeHandler);
517
552
  }
@@ -520,6 +555,15 @@ export class UnifiedEmailServer extends EventEmitter {
520
555
  await this.dkimManager.setupDkimForDomains();
521
556
  logger.log('info', 'DKIM configuration completed for all domains');
522
557
 
558
+ if (this.options.dkimKeyProvisioning === 'caller-managed') {
559
+ // Caller-managed mode validates only exact-selector key material. SmartMTA
560
+ // must not register DNS handlers, generate DNS records, rotate keys, or
561
+ // mutate any /email/dns/* or /email/dkim/* managed state.
562
+ this.applyDomainRateLimits();
563
+ logger.log('info', 'Caller-managed DKIM readiness validated without provisioning');
564
+ return;
565
+ }
566
+
523
567
  if (!this.localDnsProvisioned) {
524
568
  await this.dnsManager.provisionLocalDnsRecords(
525
569
  this.domainRegistry.getAllConfigs(),
@@ -668,20 +712,11 @@ export class UnifiedEmailServer extends EventEmitter {
668
712
  }
669
713
 
670
714
  private async startSmtpServer(): Promise<void> {
671
- const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
672
- let tlsCertPem: string | undefined;
673
- let tlsKeyPem: string | undefined;
674
-
675
- if (hasTlsConfig) {
676
- try {
677
- tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
678
- tlsCertPem = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
679
- logger.log('info', 'TLS certificates loaded successfully');
680
- } catch (error) {
681
- logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
682
- }
715
+ const tlsCertPem = this.options.tls?.certPem;
716
+ const tlsKeyPem = this.options.tls?.keyPem;
717
+ if ((tlsCertPem && !tlsKeyPem) || (!tlsCertPem && tlsKeyPem)) {
718
+ throw new Error('SMTP TLS requires both certPem and keyPem');
683
719
  }
684
-
685
720
  const configuredSecurePort = this.options.smtp?.securePort;
686
721
  if (configuredSecurePort !== undefined && !(this.options.ports as number[]).includes(configuredSecurePort)) {
687
722
  throw new Error(`Configured SMTP securePort ${configuredSecurePort} is not included in ports`);
@@ -708,6 +743,7 @@ export class UnifiedEmailServer extends EventEmitter {
708
743
  processingTimeoutSecs: this.options.hooks?.onMessageData
709
744
  ? Math.ceil(this.getMessageAcceptanceTimeoutMs() / 1000) + 1
710
745
  : 30,
746
+ maxConcurrentMessages: this.options.smtp?.maxConcurrentMessages ?? 4,
711
747
  recipientValidationEnabled: this.isRecipientValidationEnabled(),
712
748
  recipientValidationTimeoutSecs: this.options.smtp?.recipientValidationTimeoutMs
713
749
  ? Math.ceil(this.options.smtp.recipientValidationTimeoutMs / 1000)
@@ -775,9 +811,9 @@ export class UnifiedEmailServer extends EventEmitter {
775
811
  }
776
812
 
777
813
  logger.log('info', 'UnifiedEmailServer stopped successfully');
778
- this.emit('stopped');
814
+ await emitSafely(this, 'stopped');
779
815
  } catch (error) {
780
- logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`);
816
+ logger.log('error', `Error stopping UnifiedEmailServer: ${getErrorMessage(error)}`);
781
817
  throw error;
782
818
  }
783
819
  }
@@ -959,24 +995,7 @@ export class UnifiedEmailServer extends EventEmitter {
959
995
  logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`);
960
996
 
961
997
  try {
962
- // Decode the email data
963
- let rawMessageBuffer: Buffer;
964
- if (data.data.type === 'inline' && data.data.base64) {
965
- rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
966
- } else if (data.data.type === 'file' && data.data.path) {
967
- try {
968
- rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
969
- } finally {
970
- try {
971
- plugins.fs.unlinkSync(data.data.path);
972
- } catch {
973
- // Ignore cleanup errors
974
- }
975
- }
976
- } else {
977
- throw new Error('Invalid email data transport');
978
- }
979
-
998
+ const rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
980
999
  // Build a session-like object for processEmailByMode
981
1000
  const session: IExtendedSmtpSession = {
982
1001
  id: data.sessionId || 'rust-' + Math.random().toString(36).substring(2),
@@ -1592,18 +1611,12 @@ export class UnifiedEmailServer extends EventEmitter {
1592
1611
  }
1593
1612
  }
1594
1613
 
1595
- // Sign with DKIM if configured
1596
- if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
1597
- const domain = email.from.split('@')[1];
1598
- await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'default');
1599
- }
1600
-
1601
1614
  const id = await this.deliveryQueue.enqueue(email, mode, route);
1602
1615
 
1603
1616
  logger.log('info', `Email queued with ID: ${id}`);
1604
1617
  return id;
1605
1618
  } catch (error) {
1606
- logger.log('error', `Failed to send email: ${error.message}`);
1619
+ logger.log('error', `Failed to send email: ${getErrorMessage(error)}`);
1607
1620
  throw error;
1608
1621
  }
1609
1622
  }
@@ -1624,9 +1637,9 @@ export class UnifiedEmailServer extends EventEmitter {
1624
1637
  bounceCategory: bounceRecord.bounceCategory
1625
1638
  });
1626
1639
 
1627
- this.emit('bounceProcessed', bounceRecord);
1640
+ await emitSafely(this, 'bounceProcessed', bounceRecord);
1628
1641
 
1629
- SecurityLogger.getInstance().logEvent({
1642
+ await callObserverSafely('bounce security log', () => SecurityLogger.getInstance().logEvent({
1630
1643
  level: SecurityLogLevel.INFO,
1631
1644
  type: SecurityEventType.EMAIL_VALIDATION,
1632
1645
  message: `Bounce notification processed for recipient`,
@@ -1637,7 +1650,7 @@ export class UnifiedEmailServer extends EventEmitter {
1637
1650
  bounceCategory: bounceRecord.bounceCategory
1638
1651
  },
1639
1652
  success: true
1640
- });
1653
+ }));
1641
1654
 
1642
1655
  return true;
1643
1656
  } else {
@@ -1645,15 +1658,16 @@ export class UnifiedEmailServer extends EventEmitter {
1645
1658
  return false;
1646
1659
  }
1647
1660
  } catch (error) {
1648
- logger.log('error', `Error processing bounce notification: ${error.message}`);
1661
+ const errorMessage = getErrorMessage(error);
1662
+ logger.log('error', `Error processing bounce notification: ${errorMessage}`);
1649
1663
 
1650
- SecurityLogger.getInstance().logEvent({
1664
+ await callObserverSafely('bounce failure security log', () => SecurityLogger.getInstance().logEvent({
1651
1665
  level: SecurityLogLevel.ERROR,
1652
1666
  type: SecurityEventType.EMAIL_VALIDATION,
1653
1667
  message: 'Failed to process bounce notification',
1654
- details: { error: error.message, subject: bounceEmail.subject },
1668
+ details: { error: errorMessage, subject: bounceEmail.subject },
1655
1669
  success: false
1656
- });
1670
+ }));
1657
1671
 
1658
1672
  return false;
1659
1673
  }
@@ -1662,7 +1676,13 @@ export class UnifiedEmailServer extends EventEmitter {
1662
1676
  public async processSmtpFailure(
1663
1677
  recipient: string,
1664
1678
  smtpResponse: string,
1665
- options: { sender?: string; originalEmailId?: string; statusCode?: string; headers?: Record<string, string> } = {}
1679
+ options: {
1680
+ operationId?: string;
1681
+ sender?: string;
1682
+ originalEmailId?: string;
1683
+ statusCode?: string;
1684
+ headers?: Record<string, string>;
1685
+ } = {}
1666
1686
  ): Promise<boolean> {
1667
1687
  logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`);
1668
1688
 
@@ -1673,9 +1693,9 @@ export class UnifiedEmailServer extends EventEmitter {
1673
1693
  bounceType: bounceRecord.bounceType
1674
1694
  });
1675
1695
 
1676
- this.emit('bounceProcessed', bounceRecord);
1696
+ await emitSafely(this, 'bounceProcessed', bounceRecord);
1677
1697
 
1678
- SecurityLogger.getInstance().logEvent({
1698
+ await callObserverSafely('SMTP failure security log', () => SecurityLogger.getInstance().logEvent({
1679
1699
  level: SecurityLogLevel.INFO,
1680
1700
  type: SecurityEventType.EMAIL_VALIDATION,
1681
1701
  message: `SMTP failure processed for recipient`,
@@ -1687,19 +1707,20 @@ export class UnifiedEmailServer extends EventEmitter {
1687
1707
  smtpResponse
1688
1708
  },
1689
1709
  success: true
1690
- });
1710
+ }));
1691
1711
 
1692
1712
  return true;
1693
1713
  } catch (error) {
1694
- logger.log('error', `Error processing SMTP failure: ${error.message}`);
1714
+ const errorMessage = getErrorMessage(error);
1715
+ logger.log('error', `Error processing SMTP failure: ${errorMessage}`);
1695
1716
 
1696
- SecurityLogger.getInstance().logEvent({
1717
+ await callObserverSafely('SMTP failure security log', () => SecurityLogger.getInstance().logEvent({
1697
1718
  level: SecurityLogLevel.ERROR,
1698
1719
  type: SecurityEventType.EMAIL_VALIDATION,
1699
1720
  message: 'Failed to process SMTP failure',
1700
- details: { recipient, smtpResponse, error: error.message },
1721
+ details: { recipient, smtpResponse, error: errorMessage },
1701
1722
  success: false
1702
- });
1723
+ }));
1703
1724
 
1704
1725
  return false;
1705
1726
  }
@@ -89,7 +89,6 @@ export interface IEmailAction {
89
89
  mtaOptions?: {
90
90
  domain?: string;
91
91
  allowLocalDelivery?: boolean;
92
- localDeliveryPath?: string;
93
92
  dkimSign?: boolean;
94
93
  dkimOptions?: {
95
94
  domainName: string;
@@ -99,7 +98,6 @@ export interface IEmailAction {
99
98
  smtpBanner?: string;
100
99
  maxConnections?: number;
101
100
  connTimeout?: number;
102
- spoolDir?: string;
103
101
  };
104
102
  /** Content scanning configuration */
105
103
  contentScanning?: boolean;