@push.rocks/smartmta 5.3.1 → 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 (36) hide show
  1. package/changelog.md +15 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/mail/core/classes.bouncemanager.d.ts +2 -1
  4. package/dist_ts/mail/core/classes.bouncemanager.js +6 -5
  5. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +3 -2
  6. package/dist_ts/mail/delivery/classes.delivery.queue.js +4 -1
  7. package/dist_ts/mail/index.d.ts +1 -0
  8. package/dist_ts/mail/index.js +2 -1
  9. package/dist_ts/mail/interfaces.storage.d.ts +7 -0
  10. package/dist_ts/mail/interfaces.storage.js +4 -0
  11. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +2 -1
  12. package/dist_ts/mail/routing/classes.dkim.manager.js +17 -8
  13. package/dist_ts/mail/routing/classes.dns.manager.d.ts +1 -5
  14. package/dist_ts/mail/routing/classes.dns.manager.js +2 -2
  15. package/dist_ts/mail/routing/classes.email.router.d.ts +2 -1
  16. package/dist_ts/mail/routing/classes.email.router.js +6 -5
  17. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +17 -3
  18. package/dist_ts/mail/routing/classes.unified.email.server.js +32 -8
  19. package/dist_ts/mail/security/classes.dkimcreator.d.ts +10 -5
  20. package/dist_ts/mail/security/classes.dkimcreator.js +91 -70
  21. package/dist_ts/security/classes.ipreputationchecker.d.ts +4 -3
  22. package/dist_ts/security/classes.ipreputationchecker.js +4 -3
  23. package/package.json +11 -11
  24. package/readme.hints.md +1 -1
  25. package/readme.md +17 -0
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/mail/core/classes.bouncemanager.ts +7 -6
  28. package/ts/mail/delivery/classes.delivery.queue.ts +7 -3
  29. package/ts/mail/index.ts +2 -1
  30. package/ts/mail/interfaces.storage.ts +13 -0
  31. package/ts/mail/routing/classes.dkim.manager.ts +24 -11
  32. package/ts/mail/routing/classes.dns.manager.ts +3 -8
  33. package/ts/mail/routing/classes.email.router.ts +7 -6
  34. package/ts/mail/routing/classes.unified.email.server.ts +52 -12
  35. package/ts/mail/security/classes.dkimcreator.ts +115 -91
  36. package/ts/security/classes.ipreputationchecker.ts +7 -6
@@ -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;
@@ -237,7 +237,7 @@ export class UnifiedDeliveryQueue extends EventEmitter {
237
237
  * @param mode Processing mode
238
238
  * @param route Email route
239
239
  */
240
- public async enqueue(processingResult: any, mode: EmailProcessingMode, route: IEmailRoute): Promise<string> {
240
+ public async enqueue(processingResult: any, mode: EmailProcessingMode, route?: IEmailRoute): Promise<string> {
241
241
  // Check if queue is full
242
242
  if (this.queue.size >= this.options.maxQueueSize) {
243
243
  throw new Error('Queue is full');
@@ -284,6 +284,10 @@ export class UnifiedDeliveryQueue extends EventEmitter {
284
284
  public getItem(id: string): IQueueItem | undefined {
285
285
  return this.queue.get(id);
286
286
  }
287
+
288
+ public listItems(): IQueueItem[] {
289
+ return Array.from(this.queue.values()).map((item) => ({ ...item }));
290
+ }
287
291
 
288
292
  /**
289
293
  * Mark an item as being processed
@@ -657,4 +661,4 @@ export class UnifiedDeliveryQueue extends EventEmitter {
657
661
  this.emit('shutdown');
658
662
  logger.log('info', 'UnifiedDeliveryQueue shut down successfully');
659
663
  }
660
- }
664
+ }
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;