@push.rocks/smartmta 5.3.0 → 5.3.3

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 (38) hide show
  1. package/changelog.md +23 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/mail/core/classes.bouncemanager.d.ts +7 -1
  4. package/dist_ts/mail/core/classes.bouncemanager.js +25 -5
  5. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +4 -2
  6. package/dist_ts/mail/delivery/classes.delivery.queue.js +30 -14
  7. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +13 -1
  8. package/dist_ts/mail/index.d.ts +1 -0
  9. package/dist_ts/mail/index.js +2 -1
  10. package/dist_ts/mail/interfaces.storage.d.ts +7 -0
  11. package/dist_ts/mail/interfaces.storage.js +4 -0
  12. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +2 -1
  13. package/dist_ts/mail/routing/classes.dkim.manager.js +17 -8
  14. package/dist_ts/mail/routing/classes.dns.manager.d.ts +1 -5
  15. package/dist_ts/mail/routing/classes.dns.manager.js +2 -2
  16. package/dist_ts/mail/routing/classes.email.router.d.ts +2 -1
  17. package/dist_ts/mail/routing/classes.email.router.js +6 -5
  18. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +17 -3
  19. package/dist_ts/mail/routing/classes.unified.email.server.js +32 -8
  20. package/dist_ts/mail/security/classes.dkimcreator.d.ts +10 -5
  21. package/dist_ts/mail/security/classes.dkimcreator.js +91 -70
  22. package/dist_ts/security/classes.ipreputationchecker.d.ts +4 -3
  23. package/dist_ts/security/classes.ipreputationchecker.js +4 -3
  24. package/package.json +11 -11
  25. package/readme.hints.md +1 -1
  26. package/readme.md +17 -0
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/mail/core/classes.bouncemanager.ts +32 -9
  29. package/ts/mail/delivery/classes.delivery.queue.ts +40 -21
  30. package/ts/mail/delivery/classes.unified.rate.limiter.ts +15 -1
  31. package/ts/mail/index.ts +2 -1
  32. package/ts/mail/interfaces.storage.ts +13 -0
  33. package/ts/mail/routing/classes.dkim.manager.ts +24 -11
  34. package/ts/mail/routing/classes.dns.manager.ts +3 -8
  35. package/ts/mail/routing/classes.email.router.ts +7 -6
  36. package/ts/mail/routing/classes.unified.email.server.ts +52 -12
  37. package/ts/mail/security/classes.dkimcreator.ts +115 -91
  38. package/ts/security/classes.ipreputationchecker.ts +7 -6
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import * as paths from '../../paths.js';
3
3
  import { logger } from '../../logger.js';
4
+ import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
4
5
  import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
5
6
  import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
6
7
  import { LRUCache } from 'lru-cache';
@@ -88,7 +89,10 @@ export class BounceManager {
88
89
 
89
90
  // Store of bounced emails
90
91
  private bounceStore: BounceRecord[] = [];
91
-
92
+
93
+ // Periodic cleanup timer for old bounce records
94
+ private cleanupInterval?: NodeJS.Timeout;
95
+
92
96
  // Cache of recently bounced email addresses to avoid sending to known bad addresses
93
97
  private bounceCache: LRUCache<string, {
94
98
  lastBounce: number;
@@ -104,13 +108,13 @@ export class BounceManager {
104
108
  expiresAt?: number; // undefined means permanent
105
109
  }> = new Map();
106
110
 
107
- private storageManager?: any; // StorageManager instance
111
+ private storageManager?: IStorageManagerLike;
108
112
 
109
113
  constructor(options?: {
110
114
  retryStrategy?: Partial<RetryStrategy>;
111
115
  maxCacheSize?: number;
112
116
  cacheTTL?: number;
113
- storageManager?: any;
117
+ storageManager?: IStorageManagerLike;
114
118
  }) {
115
119
  // Set retry strategy with defaults
116
120
  if (options?.retryStrategy) {
@@ -135,6 +139,15 @@ export class BounceManager {
135
139
  this.loadSuppressionList().catch(error => {
136
140
  logger.log('error', `Failed to load suppression list on startup: ${error.message}`);
137
141
  });
142
+
143
+ // Start periodic cleanup of old bounce records (every 1 hour, removes records older than 7 days)
144
+ this.cleanupInterval = setInterval(() => {
145
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
146
+ const removed = this.clearOldBounceRecords(sevenDaysAgo);
147
+ if (removed > 0) {
148
+ logger.log('info', `Auto-cleanup removed ${removed} old bounce records`);
149
+ }
150
+ }, 60 * 60 * 1000);
138
151
  }
139
152
 
140
153
  /**
@@ -540,7 +553,7 @@ export class BounceManager {
540
553
  try {
541
554
  const suppressionData = JSON.stringify(Array.from(this.suppressionList.entries()));
542
555
 
543
- if (this.storageManager) {
556
+ if (hasStorageManagerMethods(this.storageManager, ['set'])) {
544
557
  // Use storage manager
545
558
  await this.storageManager.set('/email/bounces/suppression-list.json', suppressionData);
546
559
  } else {
@@ -562,7 +575,7 @@ export class BounceManager {
562
575
  let entries = null;
563
576
  let needsMigration = false;
564
577
 
565
- if (this.storageManager) {
578
+ if (hasStorageManagerMethods(this.storageManager, ['get'])) {
566
579
  // Try to load from storage manager first
567
580
  const suppressionData = await this.storageManager.get('/email/bounces/suppression-list.json');
568
581
 
@@ -624,7 +637,7 @@ export class BounceManager {
624
637
  try {
625
638
  const bounceData = JSON.stringify(bounce, null, 2);
626
639
 
627
- if (this.storageManager) {
640
+ if (hasStorageManagerMethods(this.storageManager, ['set'])) {
628
641
  // Use storage manager
629
642
  await this.storageManager.set(`/email/bounces/records/${bounce.id}.json`, bounceData);
630
643
  } else {
@@ -717,7 +730,7 @@ export class BounceManager {
717
730
  */
718
731
  public clearOldBounceRecords(olderThan: number): number {
719
732
  let removed = 0;
720
-
733
+
721
734
  this.bounceStore = this.bounceStore.filter(bounce => {
722
735
  if (bounce.timestamp < olderThan) {
723
736
  removed++;
@@ -725,7 +738,17 @@ export class BounceManager {
725
738
  }
726
739
  return true;
727
740
  });
728
-
741
+
729
742
  return removed;
730
743
  }
731
- }
744
+
745
+ /**
746
+ * Stop the bounce manager and clear cleanup timers
747
+ */
748
+ public stop(): void {
749
+ if (this.cleanupInterval) {
750
+ clearInterval(this.cleanupInterval);
751
+ this.cleanupInterval = undefined;
752
+ }
753
+ }
754
+ }
@@ -18,7 +18,7 @@ export interface IQueueItem {
18
18
  id: string;
19
19
  processingMode: EmailProcessingMode;
20
20
  processingResult: any;
21
- route: IEmailRoute;
21
+ route?: IEmailRoute;
22
22
  status: QueueItemStatus;
23
23
  attempts: number;
24
24
  nextAttempt: Date;
@@ -78,6 +78,7 @@ export class UnifiedDeliveryQueue extends EventEmitter {
78
78
  private options: Required<IQueueOptions>;
79
79
  private queue: Map<string, IQueueItem> = new Map();
80
80
  private checkTimer?: NodeJS.Timeout;
81
+ private cleanupTimer?: NodeJS.Timeout;
81
82
  private stats: IQueueStats;
82
83
  private processing: boolean = false;
83
84
  private totalProcessed: number = 0;
@@ -158,8 +159,19 @@ export class UnifiedDeliveryQueue extends EventEmitter {
158
159
  if (this.checkTimer) {
159
160
  clearInterval(this.checkTimer);
160
161
  }
161
-
162
+
162
163
  this.checkTimer = setInterval(() => this.processQueue(), this.options.checkInterval);
164
+
165
+ // Start periodic cleanup of delivered/failed items (every 30 minutes)
166
+ if (this.cleanupTimer) {
167
+ clearInterval(this.cleanupTimer);
168
+ }
169
+ this.cleanupTimer = setInterval(() => {
170
+ this.cleanupOldItems(24 * 60 * 60 * 1000).catch((err) => {
171
+ logger.log('error', `Auto-cleanup failed: ${err.message}`);
172
+ });
173
+ }, 30 * 60 * 1000);
174
+
163
175
  this.processing = true;
164
176
  this.stats.processingActive = true;
165
177
  this.emit('processingStarted');
@@ -174,7 +186,11 @@ export class UnifiedDeliveryQueue extends EventEmitter {
174
186
  clearInterval(this.checkTimer);
175
187
  this.checkTimer = undefined;
176
188
  }
177
-
189
+ if (this.cleanupTimer) {
190
+ clearInterval(this.cleanupTimer);
191
+ this.cleanupTimer = undefined;
192
+ }
193
+
178
194
  this.processing = false;
179
195
  this.stats.processingActive = false;
180
196
  this.emit('processingStopped');
@@ -221,7 +237,7 @@ export class UnifiedDeliveryQueue extends EventEmitter {
221
237
  * @param mode Processing mode
222
238
  * @param route Email route
223
239
  */
224
- public async enqueue(processingResult: any, mode: EmailProcessingMode, route: IEmailRoute): Promise<string> {
240
+ public async enqueue(processingResult: any, mode: EmailProcessingMode, route?: IEmailRoute): Promise<string> {
225
241
  // Check if queue is full
226
242
  if (this.queue.size >= this.options.maxQueueSize) {
227
243
  throw new Error('Queue is full');
@@ -268,6 +284,10 @@ export class UnifiedDeliveryQueue extends EventEmitter {
268
284
  public getItem(id: string): IQueueItem | undefined {
269
285
  return this.queue.get(id);
270
286
  }
287
+
288
+ public listItems(): IQueueItem[] {
289
+ return Array.from(this.queue.values()).map((item) => ({ ...item }));
290
+ }
271
291
 
272
292
  /**
273
293
  * Mark an item as being processed
@@ -590,19 +610,24 @@ export class UnifiedDeliveryQueue extends EventEmitter {
590
610
  */
591
611
  public async cleanupOldItems(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> {
592
612
  const cutoff = new Date(Date.now() - maxAge);
593
- let removedCount = 0;
594
-
595
- // Find old items
613
+
614
+ // Collect IDs first to avoid modifying the Map during iteration
615
+ const idsToRemove: string[] = [];
596
616
  for (const item of this.queue.values()) {
597
617
  if (['delivered', 'failed'].includes(item.status) && item.updatedAt < cutoff) {
598
- // Remove item
599
- await this.removeItem(item.id);
600
- removedCount++;
618
+ idsToRemove.push(item.id);
601
619
  }
602
620
  }
603
-
604
- logger.log('info', `Cleaned up ${removedCount} old items`);
605
- return removedCount;
621
+
622
+ // Remove collected items
623
+ for (const id of idsToRemove) {
624
+ await this.removeItem(id);
625
+ }
626
+
627
+ if (idsToRemove.length > 0) {
628
+ logger.log('info', `Cleaned up ${idsToRemove.length} old items from delivery queue`);
629
+ }
630
+ return idsToRemove.length;
606
631
  }
607
632
 
608
633
  /**
@@ -611,15 +636,9 @@ export class UnifiedDeliveryQueue extends EventEmitter {
611
636
  public async shutdown(): Promise<void> {
612
637
  logger.log('info', 'Shutting down UnifiedDeliveryQueue');
613
638
 
614
- // Stop processing
639
+ // Stop processing (clears both check and cleanup timers)
615
640
  this.stopProcessing();
616
641
 
617
- // Clear the check timer to prevent memory leaks
618
- if (this.checkTimer) {
619
- clearInterval(this.checkTimer);
620
- this.checkTimer = undefined;
621
- }
622
-
623
642
  // If using disk storage, make sure all items are persisted
624
643
  if (this.options.storageType === 'disk') {
625
644
  const pendingWrites: Promise<void>[] = [];
@@ -642,4 +661,4 @@ export class UnifiedDeliveryQueue extends EventEmitter {
642
661
  this.emit('shutdown');
643
662
  logger.log('info', 'UnifiedDeliveryQueue shut down successfully');
644
663
  }
645
- }
664
+ }
@@ -231,7 +231,21 @@ export class UnifiedRateLimiter extends EventEmitter {
231
231
  this.domainCounters.delete(key);
232
232
  }
233
233
  }
234
-
234
+
235
+ // Clean stale stats.byIp entries for IPs that no longer have active counters or blocks
236
+ for (const ip of Object.keys(this.stats.byIp)) {
237
+ if (!this.ipCounters.has(ip) && !(this.config.blocks && ip in this.config.blocks)) {
238
+ delete this.stats.byIp[ip];
239
+ }
240
+ }
241
+
242
+ // Clean stale stats.byPattern entries for patterns that no longer have active counters
243
+ for (const pattern of Object.keys(this.stats.byPattern)) {
244
+ if (!this.patternCounters.has(pattern)) {
245
+ delete this.stats.byPattern[pattern];
246
+ }
247
+ }
248
+
235
249
  // Update statistics
236
250
  this.updateStats();
237
251
  }
package/ts/mail/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // Export all mail modules for simplified imports
2
+ export * from './interfaces.storage.js';
2
3
  export * from './routing/index.js';
3
4
  export * from './security/index.js';
4
5
 
@@ -14,4 +15,4 @@ import { Email } from './core/classes.email.js';
14
15
  // Re-export commonly used classes
15
16
  export {
16
17
  Email,
17
- };
18
+ };
@@ -0,0 +1,13 @@
1
+ export interface IStorageManagerLike {
2
+ get?(key: string): Promise<string | null>;
3
+ set?(key: string, value: string): Promise<void>;
4
+ list?(prefix: string): Promise<string[]>;
5
+ delete?(key: string): Promise<void>;
6
+ }
7
+
8
+ export function hasStorageManagerMethods<T extends keyof IStorageManagerLike>(
9
+ storageManager: IStorageManagerLike | undefined,
10
+ methods: T[],
11
+ ): storageManager is IStorageManagerLike & Required<Pick<IStorageManagerLike, T>> {
12
+ return !!storageManager && methods.every((method) => typeof storageManager[method] === 'function');
13
+ }
@@ -1,12 +1,13 @@
1
1
  import { logger } from '../../logger.js';
2
2
  import { DKIMCreator } from '../security/classes.dkimcreator.js';
3
+ import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
3
4
  import { DomainRegistry } from './classes.domain.registry.js';
4
5
  import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
5
6
  import { Email } from '../core/classes.email.js';
6
7
 
7
8
  /** External DcRouter interface shape used by DkimManager */
8
9
  interface DcRouter {
9
- storageManager: any;
10
+ storageManager?: IStorageManagerLike;
10
11
  dnsServer?: any;
11
12
  }
12
13
 
@@ -39,11 +40,19 @@ export class DkimManager {
39
40
  let keyPair: { privateKey: string; publicKey: string };
40
41
 
41
42
  try {
42
- keyPair = await this.dkimCreator.readDKIMKeys(domain);
43
+ keyPair = selector === 'default'
44
+ ? await this.dkimCreator.readDKIMKeys(domain)
45
+ : await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
43
46
  logger.log('info', `Using existing DKIM keys for domain: ${domain}`);
44
- } catch (error) {
45
- keyPair = await this.dkimCreator.createDKIMKeys();
46
- await this.dkimCreator.createAndStoreDKIMKeys(domain);
47
+ } catch {
48
+ await this.dkimCreator.handleDKIMKeysForSelector(
49
+ domain,
50
+ selector,
51
+ domainConfig.dkim?.keySize || 2048,
52
+ );
53
+ keyPair = selector === 'default'
54
+ ? await this.dkimCreator.readDKIMKeys(domain)
55
+ : await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
47
56
  logger.log('info', `Generated new DKIM keys for domain: ${domain}`);
48
57
  }
49
58
 
@@ -106,10 +115,12 @@ export class DkimManager {
106
115
 
107
116
  logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
108
117
 
109
- await this.dcRouter.storageManager.set(
110
- `/email/dkim/${domain}/public.key`,
111
- keyPair.publicKey
112
- );
118
+ if (hasStorageManagerMethods(this.dcRouter.storageManager, ['set'])) {
119
+ await this.dcRouter.storageManager.set(
120
+ `/email/dkim/${domain}/public.key`,
121
+ keyPair.publicKey
122
+ );
123
+ }
113
124
  }
114
125
 
115
126
  this.dkimCreator.cleanupOldKeys(domain, 30).catch(error => {
@@ -127,8 +138,10 @@ export class DkimManager {
127
138
 
128
139
  async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> {
129
140
  try {
130
- await this.dkimCreator.handleDKIMKeysForDomain(domain);
131
- const { privateKey } = await this.dkimCreator.readDKIMKeys(domain);
141
+ await this.dkimCreator.handleDKIMKeysForSelector(domain, selector);
142
+ const { privateKey } = selector === 'default'
143
+ ? await this.dkimCreator.readDKIMKeys(domain)
144
+ : await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
132
145
  const rawEmail = email.toRFC822String();
133
146
 
134
147
  // Detect key type from PEM header
@@ -1,5 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { IEmailDomainConfig } from './interfaces.js';
3
+ import type { IStorageManagerLike } from '../interfaces.storage.js';
3
4
  import { logger } from '../../logger.js';
4
5
  /** External DcRouter interface shape used by DnsManager */
5
6
  interface IDcRouterLike {
@@ -8,12 +9,6 @@ interface IDcRouterLike {
8
9
  options?: { dnsNsDomains?: string[]; dnsScopes?: string[] };
9
10
  }
10
11
 
11
- /** External StorageManager interface shape used by DnsManager */
12
- interface IStorageManagerLike {
13
- get(key: string): Promise<string | null>;
14
- set(key: string, value: string): Promise<void>;
15
- }
16
-
17
12
  /**
18
13
  * DNS validation result
19
14
  */
@@ -528,7 +523,7 @@ export class DnsManager {
528
523
 
529
524
  try {
530
525
  // Get DKIM DNS record from DKIMCreator
531
- const dnsRecord = await dkimCreator.getDNSRecordForDomain(domain);
526
+ const dnsRecord = await dkimCreator.getDNSRecordForDomain(domain, selector);
532
527
 
533
528
  // For internal-dns domains, register the DNS handler
534
529
  if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
@@ -570,4 +565,4 @@ export class DnsManager {
570
565
  }
571
566
  }
572
567
  }
573
- }
568
+ }
@@ -1,5 +1,6 @@
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
4
  import type { IEmailRoute, IEmailMatch, IEmailAction, IEmailContext } from './interfaces.js';
4
5
  import type { Email } from '../core/classes.email.js';
5
6
 
@@ -9,7 +10,7 @@ import type { Email } from '../core/classes.email.js';
9
10
  export class EmailRouter extends EventEmitter {
10
11
  private routes: IEmailRoute[];
11
12
  private patternCache: Map<string, boolean> = new Map();
12
- private storageManager?: any; // StorageManager instance
13
+ private storageManager?: IStorageManagerLike;
13
14
  private persistChanges: boolean;
14
15
 
15
16
  /**
@@ -18,7 +19,7 @@ export class EmailRouter extends EventEmitter {
18
19
  * @param options Router options
19
20
  */
20
21
  constructor(routes: IEmailRoute[], options?: {
21
- storageManager?: any;
22
+ storageManager?: IStorageManagerLike;
22
23
  persistChanges?: boolean;
23
24
  }) {
24
25
  super();
@@ -27,7 +28,7 @@ export class EmailRouter extends EventEmitter {
27
28
  this.persistChanges = options?.persistChanges ?? !!this.storageManager;
28
29
 
29
30
  // If storage manager is provided, try to load persisted routes
30
- if (this.storageManager) {
31
+ if (hasStorageManagerMethods(this.storageManager, ['get'])) {
31
32
  this.loadRoutes({ merge: true }).catch(error => {
32
33
  console.error(`Failed to load persisted routes: ${error.message}`);
33
34
  });
@@ -394,7 +395,7 @@ export class EmailRouter extends EventEmitter {
394
395
  * Save current routes to storage
395
396
  */
396
397
  public async saveRoutes(): Promise<void> {
397
- if (!this.storageManager) {
398
+ if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
398
399
  this.emit('persistenceWarning', 'Cannot save routes: StorageManager not configured');
399
400
  return;
400
401
  }
@@ -425,7 +426,7 @@ export class EmailRouter extends EventEmitter {
425
426
  merge?: boolean; // Merge with existing routes
426
427
  replace?: boolean; // Replace existing routes
427
428
  }): Promise<IEmailRoute[]> {
428
- if (!this.storageManager) {
429
+ if (!hasStorageManagerMethods(this.storageManager, ['get'])) {
429
430
  this.emit('persistenceWarning', 'Cannot load routes: StorageManager not configured');
430
431
  return [];
431
432
  }
@@ -572,4 +573,4 @@ export class EmailRouter extends EventEmitter {
572
573
  public getRoute(name: string): IEmailRoute | undefined {
573
574
  return this.routes.find(r => r.name === name);
574
575
  }
575
- }
576
+ }
@@ -8,17 +8,18 @@ import {
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
12
  import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
12
- import type { IEmailReceivedEvent, IAuthRequestEvent, IEmailData } from '../../security/classes.rustsecuritybridge.js';
13
+ import type { IEmailReceivedEvent, IAuthRequestEvent } from '../../security/classes.rustsecuritybridge.js';
13
14
  import { EmailRouter } from './classes.email.router.js';
14
- import type { IEmailRoute, IEmailAction, IEmailContext, IEmailDomainConfig } from './interfaces.js';
15
+ import type { IEmailRoute, IEmailContext, IEmailDomainConfig } from './interfaces.js';
15
16
  import { Email } from '../core/classes.email.js';
16
17
  import { DomainRegistry } from './classes.domain.registry.js';
17
18
  import { DnsManager } from './classes.dns.manager.js';
18
19
  import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
19
20
  import type { ISmtpSendResult, IOutboundEmail } from '../../security/classes.rustsecuritybridge.js';
20
- import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
21
- import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
21
+ import { MultiModeDeliverySystem, type IDeliveryStats, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
22
+ import { UnifiedDeliveryQueue, type IQueueItem, type IQueueOptions, type IQueueStats } from '../delivery/classes.delivery.queue.js';
22
23
  import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
23
24
  import { SmtpState } from '../delivery/interfaces.js';
24
25
  import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
@@ -28,7 +29,7 @@ import { DkimManager } from './classes.dkim.manager.js';
28
29
 
29
30
  /** External DcRouter interface shape used by UnifiedEmailServer */
30
31
  interface DcRouter {
31
- storageManager: any;
32
+ storageManager: IStorageManagerLike;
32
33
  dnsServer?: any;
33
34
  options?: any;
34
35
  }
@@ -49,11 +50,14 @@ export interface IExtendedSmtpSession extends ISmtpSession {
49
50
  export interface IUnifiedEmailServerOptions {
50
51
  // Base server options
51
52
  ports: number[];
53
+ /** Public SMTP hostname used for greeting/banner and as the default outbound identity. */
52
54
  hostname: string;
53
55
  domains: IEmailDomainConfig[]; // Domain configurations
54
56
  banner?: string;
55
57
  debug?: boolean;
56
58
  useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
59
+ /** Persist router changes back into storage when a storage manager is available. */
60
+ persistRoutes?: boolean;
57
61
 
58
62
  // Authentication options
59
63
  auth?: {
@@ -92,6 +96,8 @@ export interface IUnifiedEmailServerOptions {
92
96
 
93
97
  // Outbound settings
94
98
  outbound?: {
99
+ /** Override the SMTP identity used for outbound delivery. Defaults to `hostname`. */
100
+ hostname?: string;
95
101
  maxConnections?: number;
96
102
  connectionTimeout?: number;
97
103
  socketTimeout?: number;
@@ -99,6 +105,9 @@ export interface IUnifiedEmailServerOptions {
99
105
  defaultFrom?: string;
100
106
  };
101
107
 
108
+ // Delivery queue
109
+ queue?: IQueueOptions;
110
+
102
111
  // Rate limiting (global limits, can be overridden per domain)
103
112
  rateLimits?: IHierarchicalRateLimits;
104
113
  }
@@ -206,7 +215,7 @@ export class UnifiedEmailServer extends EventEmitter {
206
215
  // Initialize email router with routes and storage manager
207
216
  this.emailRouter = new EmailRouter(options.routes || [], {
208
217
  storageManager: dcRouter.storageManager,
209
- persistChanges: true
218
+ persistChanges: options.persistRoutes ?? hasStorageManagerMethods(dcRouter.storageManager, ['get', 'set'])
210
219
  });
211
220
 
212
221
  // Initialize rate limiter
@@ -226,7 +235,8 @@ export class UnifiedEmailServer extends EventEmitter {
226
235
  storageType: 'memory', // Default to memory storage
227
236
  maxRetries: 3,
228
237
  baseRetryDelay: 300000, // 5 minutes
229
- maxRetryDelay: 3600000 // 1 hour
238
+ maxRetryDelay: 3600000, // 1 hour
239
+ ...options.queue,
230
240
  };
231
241
 
232
242
  this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
@@ -277,6 +287,14 @@ export class UnifiedEmailServer extends EventEmitter {
277
287
  // We'll create the SMTP servers during the start() method
278
288
  }
279
289
 
290
+ private getAdvertisedHostname(): string {
291
+ return this.options.hostname;
292
+ }
293
+
294
+ private getOutboundHostname(): string {
295
+ return this.options.outbound?.hostname || this.options.hostname;
296
+ }
297
+
280
298
  /**
281
299
  * Send an outbound email via the Rust SMTP client.
282
300
  * Uses connection pooling in the Rust binary for efficiency.
@@ -314,7 +332,7 @@ export class UnifiedEmailServer extends EventEmitter {
314
332
  host,
315
333
  port,
316
334
  secure: port === 465,
317
- domain: this.options.hostname,
335
+ domain: this.getOutboundHostname(),
318
336
  auth: options?.auth,
319
337
  email: outboundEmail,
320
338
  dkim,
@@ -455,7 +473,7 @@ export class UnifiedEmailServer extends EventEmitter {
455
473
  const securePort = (this.options.ports as number[]).find(p => p === 465);
456
474
 
457
475
  const started = await this.rustBridge.startSmtpServer({
458
- hostname: this.options.hostname,
476
+ hostname: this.getAdvertisedHostname(),
459
477
  ports: smtpPorts,
460
478
  securePort: securePort,
461
479
  tlsCertPem,
@@ -518,6 +536,9 @@ export class UnifiedEmailServer extends EventEmitter {
518
536
  logger.log('info', 'Email delivery queue shut down');
519
537
  }
520
538
 
539
+ this.bounceManager.stop();
540
+ logger.log('info', 'Bounce manager stopped');
541
+
521
542
  // Close all Rust SMTP client connection pools
522
543
  try {
523
544
  await this.rustBridge.closeSmtpPool();
@@ -973,6 +994,10 @@ export class UnifiedEmailServer extends EventEmitter {
973
994
  this.emailRouter.updateRoutes(routes);
974
995
  }
975
996
 
997
+ public getEmailRoutes(): IEmailRoute[] {
998
+ return this.emailRouter.getRoutes();
999
+ }
1000
+
976
1001
  /**
977
1002
  * Get server statistics
978
1003
  */
@@ -980,6 +1005,22 @@ export class UnifiedEmailServer extends EventEmitter {
980
1005
  return { ...this.stats };
981
1006
  }
982
1007
 
1008
+ public getQueueStats(): IQueueStats {
1009
+ return this.deliveryQueue.getStats();
1010
+ }
1011
+
1012
+ public getQueueItems(): IQueueItem[] {
1013
+ return this.deliveryQueue.listItems();
1014
+ }
1015
+
1016
+ public getQueueItem(id: string): IQueueItem | undefined {
1017
+ return this.deliveryQueue.getItem(id);
1018
+ }
1019
+
1020
+ public getDeliveryStats(): IDeliveryStats {
1021
+ return this.deliverySystem.getStats();
1022
+ }
1023
+
983
1024
  /**
984
1025
  * Get domain registry
985
1026
  */
@@ -1039,11 +1080,10 @@ export class UnifiedEmailServer extends EventEmitter {
1039
1080
  // Sign with DKIM if configured
1040
1081
  if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
1041
1082
  const domain = email.from.split('@')[1];
1042
- await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'mta');
1083
+ await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'default');
1043
1084
  }
1044
1085
 
1045
- const id = plugins.uuid.v4();
1046
- await this.deliveryQueue.enqueue(email, mode, route);
1086
+ const id = await this.deliveryQueue.enqueue(email, mode, route);
1047
1087
 
1048
1088
  logger.log('info', `Email queued with ID: ${id}`);
1049
1089
  return id;