@push.rocks/smartmta 7.0.0 → 8.0.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 (76) hide show
  1. package/changelog.md +36 -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 +11 -0
  10. package/dist_ts/mail/core/classes.bouncemanager.js +120 -38
  11. package/dist_ts/mail/core/classes.email.js +14 -12
  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/delivery/classes.delivery.queue.d.ts +77 -2
  15. package/dist_ts/mail/delivery/classes.delivery.queue.js +551 -46
  16. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
  17. package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
  18. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
  19. package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
  20. package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
  21. package/dist_ts/mail/delivery/index.d.ts +1 -0
  22. package/dist_ts/mail/delivery/index.js +2 -1
  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/index.d.ts +1 -0
  26. package/dist_ts/mail/index.js +1 -1
  27. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +8 -4
  28. package/dist_ts/mail/routing/classes.dkim.manager.js +46 -29
  29. package/dist_ts/mail/routing/classes.dns.manager.d.ts +5 -3
  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 +3 -0
  34. package/dist_ts/mail/routing/classes.email.router.js +15 -9
  35. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +4 -0
  36. package/dist_ts/mail/routing/classes.unified.email.server.js +61 -40
  37. package/dist_ts/mail/security/classes.dkimcreator.d.ts +7 -0
  38. package/dist_ts/mail/security/classes.dkimcreator.js +33 -8
  39. package/dist_ts/mail/security/classes.spfverifier.js +5 -3
  40. package/dist_ts/security/classes.contentscanner.js +14 -11
  41. package/dist_ts/security/classes.ipreputationchecker.d.ts +3 -0
  42. package/dist_ts/security/classes.ipreputationchecker.js +20 -11
  43. package/dist_ts/security/classes.rustsecuritybridge.d.ts +49 -1
  44. package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
  45. package/dist_ts/security/classes.securitylogger.js +7 -5
  46. package/dist_ts/security/index.d.ts +1 -1
  47. package/dist_ts/security/index.js +2 -2
  48. package/package.json +8 -8
  49. package/readme.hints.md +4 -3
  50. package/readme.md +41 -13
  51. package/readme.plan.md +6 -0
  52. package/ts/00_commitinfo_data.ts +1 -1
  53. package/ts/functions.errors.ts +8 -0
  54. package/ts/index.ts +3 -0
  55. package/ts/mail/core/classes.bouncemanager.ts +157 -45
  56. package/ts/mail/core/classes.email.ts +19 -13
  57. package/ts/mail/core/classes.emailvalidator.ts +9 -7
  58. package/ts/mail/delivery/classes.delivery.queue.ts +740 -58
  59. package/ts/mail/delivery/classes.delivery.system.ts +583 -170
  60. package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
  61. package/ts/mail/delivery/functions.safe-observers.ts +45 -0
  62. package/ts/mail/delivery/index.ts +1 -0
  63. package/ts/mail/delivery/interfaces.ts +27 -1
  64. package/ts/mail/index.ts +1 -0
  65. package/ts/mail/routing/classes.dkim.manager.ts +62 -37
  66. package/ts/mail/routing/classes.dns.manager.ts +36 -13
  67. package/ts/mail/routing/classes.email.action.executor.ts +64 -17
  68. package/ts/mail/routing/classes.email.router.ts +15 -8
  69. package/ts/mail/routing/classes.unified.email.server.ts +90 -44
  70. package/ts/mail/security/classes.dkimcreator.ts +50 -7
  71. package/ts/mail/security/classes.spfverifier.ts +4 -2
  72. package/ts/security/classes.contentscanner.ts +14 -11
  73. package/ts/security/classes.ipreputationchecker.ts +21 -10
  74. package/ts/security/classes.rustsecuritybridge.ts +269 -3
  75. package/ts/security/classes.securitylogger.ts +6 -4
  76. package/ts/security/index.ts +6 -1
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { EventEmitter } from 'events';
3
3
  import { logger } from '../../logger.js';
4
+ import { getErrorMessage } from '../../functions.errors.js';
4
5
  import {
5
6
  SecurityLogger,
6
7
  SecurityLogLevel,
@@ -24,6 +25,7 @@ import { SmtpState } from '../delivery/interfaces.js';
24
25
  import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
25
26
  import { EmailActionExecutor } from './classes.email.action.executor.js';
26
27
  import { DkimManager } from './classes.dkim.manager.js';
28
+ import { callObserverSafely, emitSafely } from '../delivery/functions.safe-observers.js';
27
29
 
28
30
 
29
31
  /** External DcRouter interface shape used by UnifiedEmailServer */
@@ -104,6 +106,8 @@ export interface IUnifiedEmailServerOptions {
104
106
  domains: IEmailDomainConfig[]; // Domain configurations
105
107
  banner?: string;
106
108
  debug?: boolean;
109
+ /** Automatic for standalone use; caller-managed keeps DNS/key ownership with the embedding app. */
110
+ dkimKeyProvisioning?: 'automatic' | 'caller-managed';
107
111
  useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
108
112
  /** Persist router changes back into storage when a storage manager is available. */
109
113
  persistRoutes?: boolean;
@@ -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(),
@@ -767,9 +811,9 @@ export class UnifiedEmailServer extends EventEmitter {
767
811
  }
768
812
 
769
813
  logger.log('info', 'UnifiedEmailServer stopped successfully');
770
- this.emit('stopped');
814
+ await emitSafely(this, 'stopped');
771
815
  } catch (error) {
772
- logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`);
816
+ logger.log('error', `Error stopping UnifiedEmailServer: ${getErrorMessage(error)}`);
773
817
  throw error;
774
818
  }
775
819
  }
@@ -1567,18 +1611,12 @@ export class UnifiedEmailServer extends EventEmitter {
1567
1611
  }
1568
1612
  }
1569
1613
 
1570
- // Sign with DKIM if configured
1571
- if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
1572
- const domain = email.from.split('@')[1];
1573
- await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'default');
1574
- }
1575
-
1576
1614
  const id = await this.deliveryQueue.enqueue(email, mode, route);
1577
1615
 
1578
1616
  logger.log('info', `Email queued with ID: ${id}`);
1579
1617
  return id;
1580
1618
  } catch (error) {
1581
- logger.log('error', `Failed to send email: ${error.message}`);
1619
+ logger.log('error', `Failed to send email: ${getErrorMessage(error)}`);
1582
1620
  throw error;
1583
1621
  }
1584
1622
  }
@@ -1599,9 +1637,9 @@ export class UnifiedEmailServer extends EventEmitter {
1599
1637
  bounceCategory: bounceRecord.bounceCategory
1600
1638
  });
1601
1639
 
1602
- this.emit('bounceProcessed', bounceRecord);
1640
+ await emitSafely(this, 'bounceProcessed', bounceRecord);
1603
1641
 
1604
- SecurityLogger.getInstance().logEvent({
1642
+ await callObserverSafely('bounce security log', () => SecurityLogger.getInstance().logEvent({
1605
1643
  level: SecurityLogLevel.INFO,
1606
1644
  type: SecurityEventType.EMAIL_VALIDATION,
1607
1645
  message: `Bounce notification processed for recipient`,
@@ -1612,7 +1650,7 @@ export class UnifiedEmailServer extends EventEmitter {
1612
1650
  bounceCategory: bounceRecord.bounceCategory
1613
1651
  },
1614
1652
  success: true
1615
- });
1653
+ }));
1616
1654
 
1617
1655
  return true;
1618
1656
  } else {
@@ -1620,15 +1658,16 @@ export class UnifiedEmailServer extends EventEmitter {
1620
1658
  return false;
1621
1659
  }
1622
1660
  } catch (error) {
1623
- logger.log('error', `Error processing bounce notification: ${error.message}`);
1661
+ const errorMessage = getErrorMessage(error);
1662
+ logger.log('error', `Error processing bounce notification: ${errorMessage}`);
1624
1663
 
1625
- SecurityLogger.getInstance().logEvent({
1664
+ await callObserverSafely('bounce failure security log', () => SecurityLogger.getInstance().logEvent({
1626
1665
  level: SecurityLogLevel.ERROR,
1627
1666
  type: SecurityEventType.EMAIL_VALIDATION,
1628
1667
  message: 'Failed to process bounce notification',
1629
- details: { error: error.message, subject: bounceEmail.subject },
1668
+ details: { error: errorMessage, subject: bounceEmail.subject },
1630
1669
  success: false
1631
- });
1670
+ }));
1632
1671
 
1633
1672
  return false;
1634
1673
  }
@@ -1637,7 +1676,13 @@ export class UnifiedEmailServer extends EventEmitter {
1637
1676
  public async processSmtpFailure(
1638
1677
  recipient: string,
1639
1678
  smtpResponse: string,
1640
- 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
+ } = {}
1641
1686
  ): Promise<boolean> {
1642
1687
  logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`);
1643
1688
 
@@ -1648,9 +1693,9 @@ export class UnifiedEmailServer extends EventEmitter {
1648
1693
  bounceType: bounceRecord.bounceType
1649
1694
  });
1650
1695
 
1651
- this.emit('bounceProcessed', bounceRecord);
1696
+ await emitSafely(this, 'bounceProcessed', bounceRecord);
1652
1697
 
1653
- SecurityLogger.getInstance().logEvent({
1698
+ await callObserverSafely('SMTP failure security log', () => SecurityLogger.getInstance().logEvent({
1654
1699
  level: SecurityLogLevel.INFO,
1655
1700
  type: SecurityEventType.EMAIL_VALIDATION,
1656
1701
  message: `SMTP failure processed for recipient`,
@@ -1662,19 +1707,20 @@ export class UnifiedEmailServer extends EventEmitter {
1662
1707
  smtpResponse
1663
1708
  },
1664
1709
  success: true
1665
- });
1710
+ }));
1666
1711
 
1667
1712
  return true;
1668
1713
  } catch (error) {
1669
- logger.log('error', `Error processing SMTP failure: ${error.message}`);
1714
+ const errorMessage = getErrorMessage(error);
1715
+ logger.log('error', `Error processing SMTP failure: ${errorMessage}`);
1670
1716
 
1671
- SecurityLogger.getInstance().logEvent({
1717
+ await callObserverSafely('SMTP failure security log', () => SecurityLogger.getInstance().logEvent({
1672
1718
  level: SecurityLogLevel.ERROR,
1673
1719
  type: SecurityEventType.EMAIL_VALIDATION,
1674
1720
  message: 'Failed to process SMTP failure',
1675
- details: { recipient, smtpResponse, error: error.message },
1721
+ details: { recipient, smtpResponse, error: errorMessage },
1676
1722
  success: false
1677
- });
1723
+ }));
1678
1724
 
1679
1725
  return false;
1680
1726
  }
@@ -14,6 +14,14 @@ export interface IDkimKeyMetadata {
14
14
  keySize: number;
15
15
  }
16
16
 
17
+ export type TDkimKeyType = 'rsa' | 'ed25519';
18
+
19
+ export interface IDkimSigningMaterial {
20
+ privateKey: string;
21
+ publicKey: string;
22
+ keyType: TDkimKeyType;
23
+ }
24
+
17
25
  export class DKIMCreator {
18
26
  constructor(private readonly storageManager: IStorageManager) {}
19
27
 
@@ -86,6 +94,44 @@ export class DKIMCreator {
86
94
  return { privateKey, publicKey };
87
95
  }
88
96
 
97
+ public async readValidatedDKIMKeysForSelector(
98
+ domain: string,
99
+ selector: string,
100
+ ): Promise<IDkimSigningMaterial> {
101
+ const keys = await this.readDKIMKeysForSelector(domain, selector);
102
+ let privateKeyObject: plugins.crypto.KeyObject;
103
+ let publicKeyObject: plugins.crypto.KeyObject;
104
+ try {
105
+ privateKeyObject = plugins.crypto.createPrivateKey(keys.privateKey);
106
+ publicKeyObject = plugins.crypto.createPublicKey(keys.publicKey);
107
+ } catch (error) {
108
+ throw new Error(
109
+ `Invalid DKIM key material for domain ${domain} with selector ${selector}: ${(error as Error).message}`,
110
+ );
111
+ }
112
+
113
+ const keyType = privateKeyObject.asymmetricKeyType;
114
+ if (keyType !== 'rsa' && keyType !== 'ed25519') {
115
+ throw new Error(
116
+ `Unsupported DKIM key type for domain ${domain} with selector ${selector}: ${keyType || 'unknown'}`,
117
+ );
118
+ }
119
+ if (publicKeyObject.asymmetricKeyType !== keyType) {
120
+ throw new Error(`DKIM public/private key type mismatch for domain ${domain} with selector ${selector}`);
121
+ }
122
+
123
+ const derivedPublicKey = plugins.crypto.createPublicKey(keys.privateKey).export({
124
+ type: 'spki',
125
+ format: 'der',
126
+ }) as Buffer;
127
+ const suppliedPublicKey = publicKeyObject.export({ type: 'spki', format: 'der' }) as Buffer;
128
+ if (!derivedPublicKey.equals(suppliedPublicKey)) {
129
+ throw new Error(`DKIM public/private key mismatch for domain ${domain} with selector ${selector}`);
130
+ }
131
+
132
+ return { ...keys, keyType };
133
+ }
134
+
89
135
  public async createDKIMKeys(
90
136
  keySize = 2048,
91
137
  ): Promise<{ privateKey: string; publicKey: string }> {
@@ -186,20 +232,17 @@ export class DKIMCreator {
186
232
  domain: string,
187
233
  selector: string,
188
234
  ): Promise<plugins.tsclass.network.IDnsRecord> {
189
- const keys = await this.readDKIMKeysForSelector(domain, selector);
190
- const keyContents = keys.publicKey
235
+ const material = await this.readValidatedDKIMKeysForSelector(domain, selector);
236
+ const keyContents = material.publicKey
191
237
  .replace('-----BEGIN PUBLIC KEY-----', '')
192
238
  .replace('-----END PUBLIC KEY-----', '')
193
239
  .replace(/\s/g, '');
194
- const keyAlgo = keys.privateKey.includes('ED25519') || keys.publicKey.length < 200
195
- ? 'ed25519'
196
- : 'rsa';
197
240
 
198
241
  return {
199
242
  name: `${selector}._domainkey.${domain}`,
200
243
  type: 'TXT',
201
- dnsSecEnabled: null,
202
- value: `v=DKIM1; h=sha256; k=${keyAlgo}; p=${keyContents}`,
244
+ dnsSecEnabled: false,
245
+ value: `v=DKIM1; h=sha256; k=${material.keyType}; p=${keyContents}`,
203
246
  };
204
247
  }
205
248
 
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../../logger.js';
2
+ import { getErrorMessage } from '../../functions.errors.js';
2
3
 
3
4
  /**
4
5
  * SPF result qualifiers
@@ -116,9 +117,10 @@ export class SpfVerifier {
116
117
 
117
118
  return spfRecord;
118
119
  } catch (error) {
119
- logger.log('error', `Error parsing SPF record: ${error.message}`, {
120
+ const errorMessage = getErrorMessage(error);
121
+ logger.log('error', `Error parsing SPF record: ${errorMessage}`, {
120
122
  record,
121
- error: error.message
123
+ error: errorMessage
122
124
  });
123
125
  return null;
124
126
  }
@@ -5,6 +5,8 @@ import type { IAttachment } from '../mail/core/classes.email.js';
5
5
  import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
6
6
  import { RustSecurityBridge } from './classes.rustsecuritybridge.js';
7
7
  import { LRUCache } from 'lru-cache';
8
+ import { getErrorMessage, getErrorStack } from '../functions.errors.js';
9
+ import { callObserverSafely } from '../mail/delivery/functions.safe-observers.js';
8
10
 
9
11
  /**
10
12
  * Scan result information
@@ -179,9 +181,10 @@ export class ContentScanner {
179
181
 
180
182
  return result;
181
183
  } catch (error) {
182
- logger.log('error', `Error scanning email: ${error.message}`, {
184
+ const errorMessage = getErrorMessage(error);
185
+ logger.log('error', `Error scanning email: ${errorMessage}`, {
183
186
  messageId: email.getMessageId(),
184
- error: error.stack
187
+ error: getErrorStack(error)
185
188
  });
186
189
 
187
190
  // Return a safe default with error indication
@@ -191,7 +194,7 @@ export class ContentScanner {
191
194
  scannedElements: ['error'],
192
195
  timestamp: Date.now(),
193
196
  threatType: 'scan_error',
194
- threatDetails: `Scan error: ${error.message}`
197
+ threatDetails: `Scan error: ${errorMessage}`
195
198
  };
196
199
  }
197
200
  }
@@ -300,7 +303,7 @@ export class ContentScanner {
300
303
  .replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, '') // Remove control chars
301
304
  .replace(/\uFFFD/g, ''); // Remove replacement char
302
305
  } catch (error) {
303
- logger.log('warn', `Error extracting text from buffer: ${error.message}`);
306
+ logger.log('warn', `Error extracting text from buffer: ${getErrorMessage(error)}`);
304
307
  return '';
305
308
  }
306
309
  }
@@ -340,7 +343,7 @@ export class ContentScanner {
340
343
  * @param result The scan result
341
344
  */
342
345
  private logHighThreatFound(email: Email, result: IScanResult): void {
343
- SecurityLogger.getInstance().logEvent({
346
+ void callObserverSafely('content scan security log', () => SecurityLogger.getInstance().logEvent({
344
347
  level: SecurityLogLevel.ERROR,
345
348
  type: SecurityEventType.MALWARE,
346
349
  message: `High threat content detected in email from ${email.from} to ${email.to.join(', ')}`,
@@ -353,8 +356,8 @@ export class ContentScanner {
353
356
  subject: email.subject
354
357
  },
355
358
  success: false,
356
- domain: email.getFromDomain()
357
- });
359
+ domain: email.getFromDomain() || undefined
360
+ }));
358
361
  }
359
362
 
360
363
  /**
@@ -363,7 +366,7 @@ export class ContentScanner {
363
366
  * @param result The scan result
364
367
  */
365
368
  private logThreatFound(email: Email, result: IScanResult): void {
366
- SecurityLogger.getInstance().logEvent({
369
+ void callObserverSafely('content scan security log', () => SecurityLogger.getInstance().logEvent({
367
370
  level: SecurityLogLevel.WARN,
368
371
  type: SecurityEventType.SPAM,
369
372
  message: `Suspicious content detected in email from ${email.from} to ${email.to.join(', ')}`,
@@ -376,8 +379,8 @@ export class ContentScanner {
376
379
  subject: email.subject
377
380
  },
378
381
  success: false,
379
- domain: email.getFromDomain()
380
- });
382
+ domain: email.getFromDomain() || undefined
383
+ }));
381
384
  }
382
385
 
383
386
  /**
@@ -396,4 +399,4 @@ export class ContentScanner {
396
399
  return 'high';
397
400
  }
398
401
  }
399
- }
402
+ }
@@ -1,8 +1,10 @@
1
1
  import { logger } from '../logger.js';
2
+ import { getErrorMessage, getErrorStack } from '../functions.errors.js';
2
3
  import type { IStorageManager } from '../mail/interfaces.storage.js';
3
4
  import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
4
5
  import { RustSecurityBridge } from './classes.rustsecuritybridge.js';
5
6
  import { LRUCache } from 'lru-cache';
7
+ import { callObserverSafely } from '../mail/delivery/functions.safe-observers.js';
6
8
 
7
9
  /**
8
10
  * Reputation check result information
@@ -66,6 +68,7 @@ export class IPReputationChecker {
66
68
  private reputationCache: LRUCache<string, IReputationResult>;
67
69
  private options: Required<IIPReputationOptions>;
68
70
  private storageManager?: IStorageManager;
71
+ private initializationPromise: Promise<void>;
69
72
 
70
73
  private static readonly DEFAULT_OPTIONS: Required<IIPReputationOptions> = {
71
74
  maxCacheSize: 10000,
@@ -92,11 +95,16 @@ export class IPReputationChecker {
92
95
  ttl: this.options.cacheTTL,
93
96
  });
94
97
 
95
- if (this.options.persistCache) {
96
- this.loadCache().catch(error => {
97
- logger.log('error', `Failed to load IP reputation cache during initialization: ${error.message}`);
98
- });
99
- }
98
+ const hydrationPromise = this.options.persistCache
99
+ ? this.loadCache()
100
+ : Promise.resolve();
101
+ hydrationPromise.catch(() => undefined);
102
+ this.initializationPromise = hydrationPromise;
103
+ }
104
+
105
+ /** Wait until the managed reputation cache has been hydrated. */
106
+ public async initialize(): Promise<void> {
107
+ await this.initializationPromise;
100
108
  }
101
109
 
102
110
  public static getInstance(options: IIPReputationOptions = {}, storageManager?: IStorageManager): IPReputationChecker {
@@ -111,6 +119,8 @@ export class IPReputationChecker {
111
119
  */
112
120
  public async checkReputation(ip: string): Promise<IReputationResult> {
113
121
  try {
122
+ await this.initialize();
123
+
114
124
  if (!this.isValidIPAddress(ip)) {
115
125
  logger.log('warn', `Invalid IP address format: ${ip}`);
116
126
  return this.createErrorResult(ip, 'Invalid IP address format');
@@ -153,11 +163,12 @@ export class IPReputationChecker {
153
163
  this.logReputationCheck(ip, result);
154
164
  return result;
155
165
  } catch (error) {
156
- logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, {
166
+ const errorMessage = getErrorMessage(error);
167
+ logger.log('error', `Error checking IP reputation for ${ip}: ${errorMessage}`, {
157
168
  ip,
158
- stack: error.stack
169
+ stack: getErrorStack(error)
159
170
  });
160
- const errorResult = this.createErrorResult(ip, error.message);
171
+ const errorResult = this.createErrorResult(ip, errorMessage);
161
172
  // Cache error results to avoid repeated failing lookups
162
173
  this.reputationCache.set(ip, errorResult);
163
174
  return errorResult;
@@ -187,7 +198,7 @@ export class IPReputationChecker {
187
198
  logLevel = SecurityLogLevel.WARN;
188
199
  }
189
200
 
190
- SecurityLogger.getInstance().logEvent({
201
+ void callObserverSafely('IP reputation security log', () => SecurityLogger.getInstance().logEvent({
191
202
  level: logLevel,
192
203
  type: SecurityEventType.IP_REPUTATION,
193
204
  message: `IP reputation check ${result.isSpam ? 'flagged spam' : 'completed'} for ${ip}`,
@@ -202,7 +213,7 @@ export class IPReputationChecker {
202
213
  blacklists: result.blacklists
203
214
  },
204
215
  success: !result.isSpam
205
- });
216
+ }));
206
217
  }
207
218
 
208
219
  private async saveCache(): Promise<void> {