@push.rocks/smartmta 5.1.3 → 5.2.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 (98) hide show
  1. package/changelog.md +7 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +3 -0
  5. package/dist_ts/index.js +4 -0
  6. package/dist_ts/logger.d.ts +17 -0
  7. package/dist_ts/logger.js +76 -0
  8. package/dist_ts/mail/core/classes.bouncemanager.d.ts +185 -0
  9. package/dist_ts/mail/core/classes.bouncemanager.js +569 -0
  10. package/dist_ts/mail/core/classes.email.d.ts +291 -0
  11. package/dist_ts/mail/core/classes.email.js +802 -0
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +61 -0
  13. package/dist_ts/mail/core/classes.emailvalidator.js +184 -0
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +95 -0
  15. package/dist_ts/mail/core/classes.templatemanager.js +240 -0
  16. package/dist_ts/mail/core/index.d.ts +4 -0
  17. package/dist_ts/mail/core/index.js +6 -0
  18. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +163 -0
  19. package/dist_ts/mail/delivery/classes.delivery.queue.js +488 -0
  20. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +160 -0
  21. package/dist_ts/mail/delivery/classes.delivery.system.js +630 -0
  22. package/dist_ts/mail/delivery/classes.unified.rate.limiter.d.ts +200 -0
  23. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +820 -0
  24. package/dist_ts/mail/delivery/index.d.ts +4 -0
  25. package/dist_ts/mail/delivery/index.js +6 -0
  26. package/dist_ts/mail/delivery/interfaces.d.ts +140 -0
  27. package/dist_ts/mail/delivery/interfaces.js +17 -0
  28. package/dist_ts/mail/index.d.ts +7 -0
  29. package/dist_ts/mail/index.js +12 -0
  30. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +25 -0
  31. package/dist_ts/mail/routing/classes.dkim.manager.js +127 -0
  32. package/dist_ts/mail/routing/classes.dns.manager.d.ts +79 -0
  33. package/dist_ts/mail/routing/classes.dns.manager.js +415 -0
  34. package/dist_ts/mail/routing/classes.domain.registry.d.ts +54 -0
  35. package/dist_ts/mail/routing/classes.domain.registry.js +119 -0
  36. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +33 -0
  37. package/dist_ts/mail/routing/classes.email.action.executor.js +137 -0
  38. package/dist_ts/mail/routing/classes.email.router.d.ts +171 -0
  39. package/dist_ts/mail/routing/classes.email.router.js +494 -0
  40. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +241 -0
  41. package/dist_ts/mail/routing/classes.unified.email.server.js +935 -0
  42. package/dist_ts/mail/routing/index.d.ts +7 -0
  43. package/dist_ts/mail/routing/index.js +9 -0
  44. package/dist_ts/mail/routing/interfaces.d.ts +187 -0
  45. package/dist_ts/mail/routing/interfaces.js +2 -0
  46. package/dist_ts/mail/security/classes.dkimcreator.d.ts +72 -0
  47. package/dist_ts/mail/security/classes.dkimcreator.js +360 -0
  48. package/dist_ts/mail/security/classes.spfverifier.d.ts +62 -0
  49. package/dist_ts/mail/security/classes.spfverifier.js +87 -0
  50. package/dist_ts/mail/security/index.d.ts +2 -0
  51. package/dist_ts/mail/security/index.js +4 -0
  52. package/dist_ts/paths.d.ts +14 -0
  53. package/dist_ts/paths.js +39 -0
  54. package/dist_ts/plugins.d.ts +24 -0
  55. package/dist_ts/plugins.js +28 -0
  56. package/dist_ts/security/classes.contentscanner.d.ts +130 -0
  57. package/dist_ts/security/classes.contentscanner.js +338 -0
  58. package/dist_ts/security/classes.ipreputationchecker.d.ts +73 -0
  59. package/dist_ts/security/classes.ipreputationchecker.js +263 -0
  60. package/dist_ts/security/classes.rustsecuritybridge.d.ts +398 -0
  61. package/dist_ts/security/classes.rustsecuritybridge.js +484 -0
  62. package/dist_ts/security/classes.securitylogger.d.ts +140 -0
  63. package/dist_ts/security/classes.securitylogger.js +235 -0
  64. package/dist_ts/security/index.d.ts +4 -0
  65. package/dist_ts/security/index.js +5 -0
  66. package/package.json +6 -1
  67. package/ts/00_commitinfo_data.ts +8 -0
  68. package/ts/index.ts +3 -0
  69. package/ts/logger.ts +91 -0
  70. package/ts/mail/core/classes.bouncemanager.ts +731 -0
  71. package/ts/mail/core/classes.email.ts +942 -0
  72. package/ts/mail/core/classes.emailvalidator.ts +239 -0
  73. package/ts/mail/core/classes.templatemanager.ts +320 -0
  74. package/ts/mail/core/index.ts +5 -0
  75. package/ts/mail/delivery/classes.delivery.queue.ts +645 -0
  76. package/ts/mail/delivery/classes.delivery.system.ts +816 -0
  77. package/ts/mail/delivery/classes.unified.rate.limiter.ts +1053 -0
  78. package/ts/mail/delivery/index.ts +5 -0
  79. package/ts/mail/delivery/interfaces.ts +167 -0
  80. package/ts/mail/index.ts +17 -0
  81. package/ts/mail/routing/classes.dkim.manager.ts +157 -0
  82. package/ts/mail/routing/classes.dns.manager.ts +573 -0
  83. package/ts/mail/routing/classes.domain.registry.ts +139 -0
  84. package/ts/mail/routing/classes.email.action.executor.ts +175 -0
  85. package/ts/mail/routing/classes.email.router.ts +575 -0
  86. package/ts/mail/routing/classes.unified.email.server.ts +1207 -0
  87. package/ts/mail/routing/index.ts +9 -0
  88. package/ts/mail/routing/interfaces.ts +202 -0
  89. package/ts/mail/security/classes.dkimcreator.ts +447 -0
  90. package/ts/mail/security/classes.spfverifier.ts +126 -0
  91. package/ts/mail/security/index.ts +3 -0
  92. package/ts/paths.ts +48 -0
  93. package/ts/plugins.ts +53 -0
  94. package/ts/security/classes.contentscanner.ts +400 -0
  95. package/ts/security/classes.ipreputationchecker.ts +315 -0
  96. package/ts/security/classes.rustsecuritybridge.ts +943 -0
  97. package/ts/security/classes.securitylogger.ts +299 -0
  98. package/ts/security/index.ts +40 -0
@@ -0,0 +1,816 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { EventEmitter } from 'node:events';
3
+ import { logger } from '../../logger.js';
4
+ import {
5
+ SecurityLogger,
6
+ SecurityLogLevel,
7
+ SecurityEventType
8
+ } from '../../security/index.js';
9
+ import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
10
+ import { Email } from '../core/classes.email.js';
11
+ import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
12
+ import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
13
+
14
+ const dns = plugins.dns;
15
+
16
+ /**
17
+ * Delivery status enumeration
18
+ */
19
+ export enum DeliveryStatus {
20
+ PENDING = 'pending',
21
+ DELIVERING = 'delivering',
22
+ DELIVERED = 'delivered',
23
+ DEFERRED = 'deferred',
24
+ FAILED = 'failed'
25
+ }
26
+
27
+ /**
28
+ * Delivery handler interface
29
+ */
30
+ export interface IDeliveryHandler {
31
+ deliver(item: IQueueItem): Promise<any>;
32
+ }
33
+
34
+ /**
35
+ * Delivery options
36
+ */
37
+ export interface IMultiModeDeliveryOptions {
38
+ // Connection options
39
+ connectionPoolSize?: number;
40
+ socketTimeout?: number;
41
+
42
+ // Delivery behavior
43
+ concurrentDeliveries?: number;
44
+ sendTimeout?: number;
45
+
46
+ // TLS options
47
+ verifyCertificates?: boolean;
48
+ tlsMinVersion?: string;
49
+
50
+ // Mode-specific handlers
51
+ forwardHandler?: IDeliveryHandler;
52
+ deliveryHandler?: IDeliveryHandler;
53
+ processHandler?: IDeliveryHandler;
54
+
55
+ // Rate limiting
56
+ globalRateLimit?: number;
57
+ perPatternRateLimit?: Record<string, number>;
58
+
59
+ // Bounce handling
60
+ processBounces?: boolean;
61
+ bounceHandler?: {
62
+ processSmtpFailure: (recipient: string, smtpResponse: string, options: any) => Promise<any>;
63
+ };
64
+
65
+ // Event hooks
66
+ onDeliveryStart?: (item: IQueueItem) => Promise<void>;
67
+ onDeliverySuccess?: (item: IQueueItem, result: any) => Promise<void>;
68
+ onDeliveryFailed?: (item: IQueueItem, error: string) => Promise<void>;
69
+ }
70
+
71
+ /**
72
+ * Delivery system statistics
73
+ */
74
+ export interface IDeliveryStats {
75
+ activeDeliveries: number;
76
+ totalSuccessful: number;
77
+ totalFailed: number;
78
+ avgDeliveryTime: number;
79
+ byMode: {
80
+ forward: {
81
+ successful: number;
82
+ failed: number;
83
+ };
84
+ mta: {
85
+ successful: number;
86
+ failed: number;
87
+ };
88
+ process: {
89
+ successful: number;
90
+ failed: number;
91
+ };
92
+ };
93
+ rateLimiting: {
94
+ currentRate: number;
95
+ globalLimit: number;
96
+ throttled: number;
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Handles delivery for all email processing modes
102
+ */
103
+ export class MultiModeDeliverySystem extends EventEmitter {
104
+ private queue: UnifiedDeliveryQueue;
105
+ private options: Required<IMultiModeDeliveryOptions>;
106
+ private stats: IDeliveryStats;
107
+ private deliveryTimes: number[] = [];
108
+ private activeDeliveries: Set<string> = new Set();
109
+ private running: boolean = false;
110
+ private throttled: boolean = false;
111
+ private rateLimitLastCheck: number = Date.now();
112
+ private rateLimitCounter: number = 0;
113
+ private emailServer?: UnifiedEmailServer;
114
+
115
+ /**
116
+ * Create a new multi-mode delivery system
117
+ * @param queue Unified delivery queue
118
+ * @param options Delivery options
119
+ * @param emailServer Optional reference to unified email server for outbound delivery
120
+ */
121
+ constructor(queue: UnifiedDeliveryQueue, options: IMultiModeDeliveryOptions, emailServer?: UnifiedEmailServer) {
122
+ super();
123
+
124
+ this.queue = queue;
125
+ this.emailServer = emailServer;
126
+
127
+ // Set default options
128
+ this.options = {
129
+ connectionPoolSize: options.connectionPoolSize || 10,
130
+ socketTimeout: options.socketTimeout || 30000, // 30 seconds
131
+ concurrentDeliveries: options.concurrentDeliveries || 10,
132
+ sendTimeout: options.sendTimeout || 60000, // 1 minute
133
+ verifyCertificates: options.verifyCertificates !== false, // Default to true
134
+ tlsMinVersion: options.tlsMinVersion || 'TLSv1.2',
135
+ forwardHandler: options.forwardHandler || {
136
+ deliver: this.handleForwardDelivery.bind(this)
137
+ },
138
+ deliveryHandler: options.deliveryHandler || {
139
+ deliver: this.handleMtaDelivery.bind(this)
140
+ },
141
+ processHandler: options.processHandler || {
142
+ deliver: this.handleProcessDelivery.bind(this)
143
+ },
144
+ globalRateLimit: options.globalRateLimit || 100, // 100 emails per minute
145
+ perPatternRateLimit: options.perPatternRateLimit || {},
146
+ processBounces: options.processBounces !== false, // Default to true
147
+ bounceHandler: options.bounceHandler || null,
148
+ onDeliveryStart: options.onDeliveryStart || (async () => {}),
149
+ onDeliverySuccess: options.onDeliverySuccess || (async () => {}),
150
+ onDeliveryFailed: options.onDeliveryFailed || (async () => {})
151
+ };
152
+
153
+ // Initialize statistics
154
+ this.stats = {
155
+ activeDeliveries: 0,
156
+ totalSuccessful: 0,
157
+ totalFailed: 0,
158
+ avgDeliveryTime: 0,
159
+ byMode: {
160
+ forward: {
161
+ successful: 0,
162
+ failed: 0
163
+ },
164
+ mta: {
165
+ successful: 0,
166
+ failed: 0
167
+ },
168
+ process: {
169
+ successful: 0,
170
+ failed: 0
171
+ }
172
+ },
173
+ rateLimiting: {
174
+ currentRate: 0,
175
+ globalLimit: this.options.globalRateLimit,
176
+ throttled: 0
177
+ }
178
+ };
179
+
180
+ // Set up event listeners
181
+ this.queue.on('itemsReady', this.processItems.bind(this));
182
+ }
183
+
184
+ /**
185
+ * Start the delivery system
186
+ */
187
+ public async start(): Promise<void> {
188
+ logger.log('info', 'Starting MultiModeDeliverySystem');
189
+
190
+ if (this.running) {
191
+ logger.log('warn', 'MultiModeDeliverySystem is already running');
192
+ return;
193
+ }
194
+
195
+ this.running = true;
196
+
197
+ // Emit started event
198
+ this.emit('started');
199
+ logger.log('info', 'MultiModeDeliverySystem started successfully');
200
+ }
201
+
202
+ /**
203
+ * Stop the delivery system
204
+ */
205
+ public async stop(): Promise<void> {
206
+ logger.log('info', 'Stopping MultiModeDeliverySystem');
207
+
208
+ if (!this.running) {
209
+ logger.log('warn', 'MultiModeDeliverySystem is already stopped');
210
+ return;
211
+ }
212
+
213
+ this.running = false;
214
+
215
+ // Wait for active deliveries to complete
216
+ if (this.activeDeliveries.size > 0) {
217
+ logger.log('info', `Waiting for ${this.activeDeliveries.size} active deliveries to complete`);
218
+
219
+ // Wait for a maximum of 30 seconds
220
+ await new Promise<void>(resolve => {
221
+ const checkInterval = setInterval(() => {
222
+ if (this.activeDeliveries.size === 0) {
223
+ clearInterval(checkInterval);
224
+ clearTimeout(forceTimeout);
225
+ resolve();
226
+ }
227
+ }, 1000);
228
+
229
+ // Force resolve after 30 seconds
230
+ const forceTimeout = setTimeout(() => {
231
+ clearInterval(checkInterval);
232
+ resolve();
233
+ }, 30000);
234
+ });
235
+ }
236
+
237
+ // Emit stopped event
238
+ this.emit('stopped');
239
+ logger.log('info', 'MultiModeDeliverySystem stopped successfully');
240
+ }
241
+
242
+ /**
243
+ * Process ready items from the queue
244
+ * @param items Queue items ready for processing
245
+ */
246
+ private async processItems(items: IQueueItem[]): Promise<void> {
247
+ if (!this.running) {
248
+ return;
249
+ }
250
+
251
+ // Check if we're already at max concurrent deliveries
252
+ if (this.activeDeliveries.size >= this.options.concurrentDeliveries) {
253
+ logger.log('debug', `Already at max concurrent deliveries (${this.activeDeliveries.size})`);
254
+ return;
255
+ }
256
+
257
+ // Check rate limiting
258
+ if (this.checkRateLimit()) {
259
+ logger.log('debug', 'Rate limit exceeded, throttling deliveries');
260
+ return;
261
+ }
262
+
263
+ // Calculate how many more deliveries we can start
264
+ const availableSlots = this.options.concurrentDeliveries - this.activeDeliveries.size;
265
+ const itemsToProcess = items.slice(0, availableSlots);
266
+
267
+ if (itemsToProcess.length === 0) {
268
+ return;
269
+ }
270
+
271
+ logger.log('info', `Processing ${itemsToProcess.length} items for delivery`);
272
+
273
+ // Process each item
274
+ for (const item of itemsToProcess) {
275
+ // Mark as processing
276
+ await this.queue.markProcessing(item.id);
277
+
278
+ // Add to active deliveries
279
+ this.activeDeliveries.add(item.id);
280
+ this.stats.activeDeliveries = this.activeDeliveries.size;
281
+
282
+ // Deliver asynchronously
283
+ this.deliverItem(item).catch(err => {
284
+ logger.log('error', `Unhandled error in delivery: ${err.message}`);
285
+ });
286
+ }
287
+
288
+ // Update statistics
289
+ this.emit('statsUpdated', this.stats);
290
+ }
291
+
292
+ /**
293
+ * Deliver an item from the queue
294
+ * @param item Queue item to deliver
295
+ */
296
+ private async deliverItem(item: IQueueItem): Promise<void> {
297
+ const startTime = Date.now();
298
+
299
+ try {
300
+ // Call delivery start hook
301
+ await this.options.onDeliveryStart(item);
302
+
303
+ // Emit delivery start event
304
+ this.emit('deliveryStart', item);
305
+ logger.log('info', `Starting delivery of item ${item.id}, mode: ${item.processingMode}`);
306
+
307
+ // Choose the appropriate handler based on mode
308
+ let result: any;
309
+
310
+ switch (item.processingMode) {
311
+ case 'forward':
312
+ result = await this.options.forwardHandler.deliver(item);
313
+ break;
314
+
315
+ case 'mta':
316
+ result = await this.options.deliveryHandler.deliver(item);
317
+ break;
318
+
319
+ case 'process':
320
+ result = await this.options.processHandler.deliver(item);
321
+ break;
322
+
323
+ default:
324
+ throw new Error(`Unknown processing mode: ${item.processingMode}`);
325
+ }
326
+
327
+ // Mark as delivered
328
+ await this.queue.markDelivered(item.id);
329
+
330
+ // Update statistics
331
+ this.stats.totalSuccessful++;
332
+ this.stats.byMode[item.processingMode].successful++;
333
+
334
+ // Calculate delivery time
335
+ const deliveryTime = Date.now() - startTime;
336
+ this.deliveryTimes.push(deliveryTime);
337
+ this.updateDeliveryTimeStats();
338
+
339
+ // Call delivery success hook
340
+ await this.options.onDeliverySuccess(item, result);
341
+
342
+ // Emit delivery success event
343
+ this.emit('deliverySuccess', item, result);
344
+ logger.log('info', `Item ${item.id} delivered successfully in ${deliveryTime}ms`);
345
+
346
+ SecurityLogger.getInstance().logEvent({
347
+ level: SecurityLogLevel.INFO,
348
+ type: SecurityEventType.EMAIL_DELIVERY,
349
+ message: 'Email delivery successful',
350
+ details: {
351
+ itemId: item.id,
352
+ mode: item.processingMode,
353
+ routeName: item.route?.name || 'unknown',
354
+ deliveryTime
355
+ },
356
+ success: true
357
+ });
358
+ } catch (error: any) {
359
+ // Calculate delivery attempt time even for failures
360
+ const deliveryTime = Date.now() - startTime;
361
+
362
+ // Mark as failed
363
+ await this.queue.markFailed(item.id, error.message);
364
+
365
+ // Update statistics
366
+ this.stats.totalFailed++;
367
+ this.stats.byMode[item.processingMode].failed++;
368
+
369
+ // Call delivery failed hook
370
+ await this.options.onDeliveryFailed(item, error.message);
371
+
372
+ // Process as bounce if enabled and we have a bounce handler
373
+ if (this.options.processBounces && this.options.bounceHandler) {
374
+ try {
375
+ const email = item.processingResult as Email;
376
+
377
+ // Extract recipient and error message
378
+ // For multiple recipients, we'd need more sophisticated parsing
379
+ const recipient = email.to.length > 0 ? email.to[0] : '';
380
+
381
+ if (recipient) {
382
+ logger.log('info', `Processing delivery failure as bounce for recipient ${recipient}`);
383
+
384
+ // Process SMTP failure through bounce handler
385
+ await this.options.bounceHandler.processSmtpFailure(
386
+ recipient,
387
+ error.message,
388
+ {
389
+ sender: email.from,
390
+ originalEmailId: item.id,
391
+ headers: email.headers
392
+ }
393
+ );
394
+
395
+ logger.log('info', `Bounce record created for failed delivery to ${recipient}`);
396
+ }
397
+ } catch (bounceError) {
398
+ logger.log('error', `Failed to process bounce: ${bounceError.message}`);
399
+ }
400
+ }
401
+
402
+ // Emit delivery failed event
403
+ this.emit('deliveryFailed', item, error);
404
+ logger.log('error', `Item ${item.id} delivery failed: ${error.message}`);
405
+
406
+ SecurityLogger.getInstance().logEvent({
407
+ level: SecurityLogLevel.ERROR,
408
+ type: SecurityEventType.EMAIL_DELIVERY,
409
+ message: 'Email delivery failed',
410
+ details: {
411
+ itemId: item.id,
412
+ mode: item.processingMode,
413
+ routeName: item.route?.name || 'unknown',
414
+ error: error.message,
415
+ deliveryTime
416
+ },
417
+ success: false
418
+ });
419
+ } finally {
420
+ // Remove from active deliveries
421
+ this.activeDeliveries.delete(item.id);
422
+ this.stats.activeDeliveries = this.activeDeliveries.size;
423
+
424
+ // Update statistics
425
+ this.emit('statsUpdated', this.stats);
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Default handler for forward mode delivery
431
+ * @param item Queue item
432
+ */
433
+ private async handleForwardDelivery(item: IQueueItem): Promise<any> {
434
+ logger.log('info', `Forward delivery for item ${item.id}`);
435
+
436
+ const email = item.processingResult as Email;
437
+ const route = item.route;
438
+
439
+ // Get target server information
440
+ const targetServer = route?.action.forward?.host;
441
+ const targetPort = route?.action.forward?.port || 25;
442
+
443
+ if (!targetServer) {
444
+ throw new Error('No target server configured for forward mode');
445
+ }
446
+
447
+ logger.log('info', `Forwarding email to ${targetServer}:${targetPort}`);
448
+
449
+ try {
450
+ if (!this.emailServer) {
451
+ throw new Error('No email server available for forward delivery');
452
+ }
453
+
454
+ // Build DKIM options from route config
455
+ const dkimDomain = item.route?.action.options?.mtaOptions?.dkimSign
456
+ ? (item.route.action.options.mtaOptions.dkimOptions?.domainName || email.from.split('@')[1])
457
+ : undefined;
458
+ const dkimSelector = item.route?.action.options?.mtaOptions?.dkimOptions?.keySelector || 'default';
459
+
460
+ // Build auth options from route forward config
461
+ const auth = route?.action.forward?.auth as { user: string; pass: string } | undefined;
462
+
463
+ // Send via Rust SMTP client
464
+ const result = await this.emailServer.sendOutboundEmail(targetServer, targetPort, email, {
465
+ auth,
466
+ dkimDomain,
467
+ dkimSelector,
468
+ });
469
+
470
+ logger.log('info', `Email forwarded successfully to ${targetServer}:${targetPort}`);
471
+
472
+ return {
473
+ targetServer,
474
+ targetPort,
475
+ recipients: result.accepted.length,
476
+ messageId: result.messageId,
477
+ rejectedRecipients: result.rejected,
478
+ };
479
+ } catch (error: any) {
480
+ logger.log('error', `Failed to forward email: ${error.message}`);
481
+ throw error;
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Resolve MX records for a domain, sorted by priority (lowest first).
487
+ * Falls back to the domain itself as an A record per RFC 5321.
488
+ */
489
+ private async resolveMxForDomain(domain: string): Promise<Array<{ exchange: string; priority: number }>> {
490
+ const resolver = new dns.promises.Resolver();
491
+ try {
492
+ const mxRecords = await resolver.resolveMx(domain);
493
+ return mxRecords.sort((a, b) => a.priority - b.priority);
494
+ } catch (err) {
495
+ logger.log('warn', `No MX records for ${domain}, falling back to A record`);
496
+ return [{ exchange: domain, priority: 0 }];
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Group recipient addresses by their domain part.
502
+ */
503
+ private groupRecipientsByDomain(recipients: string[]): Map<string, string[]> {
504
+ const groups = new Map<string, string[]>();
505
+ for (const rcpt of recipients) {
506
+ const domain = rcpt.split('@')[1]?.toLowerCase();
507
+ if (!domain) continue;
508
+ const list = groups.get(domain) || [];
509
+ list.push(rcpt);
510
+ groups.set(domain, list);
511
+ }
512
+ return groups;
513
+ }
514
+
515
+ /**
516
+ * Default handler for MTA mode delivery
517
+ * @param item Queue item
518
+ */
519
+ private async handleMtaDelivery(item: IQueueItem): Promise<any> {
520
+ logger.log('info', `MTA delivery for item ${item.id}`);
521
+
522
+ const email = item.processingResult as Email;
523
+
524
+ if (!this.emailServer) {
525
+ throw new Error('No email server available for MTA delivery');
526
+ }
527
+
528
+ // Build DKIM options from route config
529
+ const dkimDomain = item.route?.action.options?.mtaOptions?.dkimSign
530
+ ? (item.route.action.options.mtaOptions.dkimOptions?.domainName || email.from.split('@')[1])
531
+ : undefined;
532
+ const dkimSelector = item.route?.action.options?.mtaOptions?.dkimOptions?.keySelector || 'default';
533
+
534
+ const allRecipients = email.getAllRecipients();
535
+ if (allRecipients.length === 0) {
536
+ throw new Error('No recipients specified for MTA delivery');
537
+ }
538
+
539
+ const domainGroups = this.groupRecipientsByDomain(allRecipients);
540
+ const results: Array<{ domain: string; success: boolean; error?: string; accepted?: string[]; rejected?: string[] }> = [];
541
+
542
+ for (const [domain, recipients] of domainGroups) {
543
+ const mxHosts = await this.resolveMxForDomain(domain);
544
+ let delivered = false;
545
+ let lastError: string | undefined;
546
+
547
+ for (const mx of mxHosts) {
548
+ try {
549
+ logger.log('info', `MTA: trying MX ${mx.exchange}:25 for domain ${domain} (priority ${mx.priority})`);
550
+
551
+ // Create a temporary Email scoped to this domain's recipients
552
+ const domainEmail = new Email({
553
+ from: email.from,
554
+ to: recipients.filter(r => email.to.includes(r)),
555
+ cc: recipients.filter(r => (email.cc || []).includes(r)),
556
+ bcc: recipients.filter(r => (email.bcc || []).includes(r)),
557
+ subject: email.subject,
558
+ text: email.text,
559
+ html: email.html,
560
+ });
561
+
562
+ const result = await this.emailServer.sendOutboundEmail(mx.exchange, 25, domainEmail, {
563
+ dkimDomain,
564
+ dkimSelector,
565
+ });
566
+
567
+ results.push({
568
+ domain,
569
+ success: true,
570
+ accepted: result.accepted,
571
+ rejected: result.rejected,
572
+ });
573
+ delivered = true;
574
+ logger.log('info', `MTA: delivered to ${domain} via ${mx.exchange}`);
575
+ break;
576
+ } catch (err: any) {
577
+ lastError = err.message;
578
+ logger.log('warn', `MTA: MX ${mx.exchange} failed for ${domain}: ${err.message}`);
579
+ }
580
+ }
581
+
582
+ if (!delivered) {
583
+ results.push({ domain, success: false, error: lastError });
584
+ logger.log('error', `MTA: all MX hosts failed for ${domain}`);
585
+ }
586
+ }
587
+
588
+ const allFailed = results.every(r => !r.success);
589
+ if (allFailed) {
590
+ const summary = results.map(r => `${r.domain}: ${r.error}`).join('; ');
591
+ throw new Error(`MTA delivery failed for all domains: ${summary}`);
592
+ }
593
+
594
+ return {
595
+ recipients: allRecipients.length,
596
+ domainResults: results,
597
+ };
598
+ }
599
+
600
+ /**
601
+ * Default handler for process mode delivery
602
+ * @param item Queue item
603
+ */
604
+ private async handleProcessDelivery(item: IQueueItem): Promise<any> {
605
+ logger.log('info', `Process delivery for item ${item.id}`);
606
+
607
+ const email = item.processingResult as Email;
608
+ const route = item.route;
609
+
610
+ try {
611
+ // Apply content scanning if enabled
612
+ if (route?.action.options?.contentScanning && route?.action.options?.scanners && route.action.options.scanners.length > 0) {
613
+ logger.log('info', 'Performing content scanning');
614
+
615
+ // Apply each scanner
616
+ for (const scanner of route.action.options.scanners) {
617
+ switch (scanner.type) {
618
+ case 'spam':
619
+ logger.log('info', 'Scanning for spam content');
620
+ // Implement spam scanning
621
+ break;
622
+
623
+ case 'virus':
624
+ logger.log('info', 'Scanning for virus content');
625
+ // Implement virus scanning
626
+ break;
627
+
628
+ case 'attachment':
629
+ logger.log('info', 'Scanning attachments');
630
+
631
+ // Check for blocked extensions
632
+ if (scanner.blockedExtensions && scanner.blockedExtensions.length > 0) {
633
+ for (const attachment of email.attachments) {
634
+ const ext = this.getFileExtension(attachment.filename);
635
+ if (scanner.blockedExtensions.includes(ext)) {
636
+ if (scanner.action === 'reject') {
637
+ throw new Error(`Blocked attachment type: ${ext}`);
638
+ } else { // tag
639
+ email.addHeader('X-Attachment-Warning', `Potentially unsafe attachment: ${attachment.filename}`);
640
+ }
641
+ }
642
+ }
643
+ }
644
+ break;
645
+ }
646
+ }
647
+ }
648
+
649
+ // Apply transformations if defined
650
+ if (route?.action.options?.transformations && route?.action.options?.transformations.length > 0) {
651
+ logger.log('info', 'Applying email transformations');
652
+
653
+ for (const transform of route.action.options.transformations) {
654
+ switch (transform.type) {
655
+ case 'addHeader':
656
+ if (transform.header && transform.value) {
657
+ email.addHeader(transform.header, transform.value);
658
+ }
659
+ break;
660
+ }
661
+ }
662
+ }
663
+
664
+ // Apply DKIM signing if configured (after all transformations)
665
+ if (item.route?.action.options?.mtaOptions?.dkimSign || item.route?.action.process?.dkim) {
666
+ await this.applyDkimSigning(email, item.route.action.options?.mtaOptions || {});
667
+ }
668
+
669
+ logger.log('info', `Email successfully processed in store-and-forward mode, delivering via MTA`);
670
+
671
+ // After scanning + transformations, deliver via MTA
672
+ return await this.handleMtaDelivery(item);
673
+ } catch (error: any) {
674
+ logger.log('error', `Failed to process email: ${error.message}`);
675
+ throw error;
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Get file extension from filename
681
+ */
682
+ private getFileExtension(filename: string): string {
683
+ return filename.substring(filename.lastIndexOf('.')).toLowerCase();
684
+ }
685
+
686
+ /**
687
+ * Apply DKIM signing to an email
688
+ */
689
+ private async applyDkimSigning(email: Email, mtaOptions: any): Promise<void> {
690
+ if (!this.emailServer) {
691
+ logger.log('warn', 'Cannot apply DKIM signing without email server reference');
692
+ return;
693
+ }
694
+
695
+ const domainName = mtaOptions.dkimOptions?.domainName || email.from.split('@')[1];
696
+ const keySelector = mtaOptions.dkimOptions?.keySelector || 'default';
697
+
698
+ try {
699
+ // Ensure DKIM keys exist for the domain
700
+ await this.emailServer.dkimCreator.handleDKIMKeysForDomain(domainName);
701
+
702
+ // Get the private key
703
+ const dkimPrivateKey = (await this.emailServer.dkimCreator.readDKIMKeys(domainName)).privateKey;
704
+
705
+ // Convert Email to raw format for signing
706
+ const rawEmail = email.toRFC822String();
707
+
708
+ // Sign via Rust bridge
709
+ const bridge = RustSecurityBridge.getInstance();
710
+ const signResult = await bridge.signDkim({
711
+ rawMessage: rawEmail,
712
+ domain: domainName,
713
+ selector: keySelector,
714
+ privateKey: dkimPrivateKey,
715
+ });
716
+
717
+ if (signResult.header) {
718
+ email.addHeader('DKIM-Signature', signResult.header);
719
+ logger.log('info', `Successfully added DKIM signature for ${domainName}`);
720
+ }
721
+ } catch (error) {
722
+ logger.log('error', `Failed to apply DKIM signature: ${error.message}`);
723
+ // Don't throw - allow email to be sent without DKIM if signing fails
724
+ }
725
+ }
726
+
727
+ /**
728
+ * Update delivery time statistics
729
+ */
730
+ private updateDeliveryTimeStats(): void {
731
+ if (this.deliveryTimes.length === 0) return;
732
+
733
+ // Keep only the last 1000 delivery times
734
+ if (this.deliveryTimes.length > 1000) {
735
+ this.deliveryTimes = this.deliveryTimes.slice(-1000);
736
+ }
737
+
738
+ // Calculate average
739
+ const sum = this.deliveryTimes.reduce((acc, time) => acc + time, 0);
740
+ this.stats.avgDeliveryTime = sum / this.deliveryTimes.length;
741
+ }
742
+
743
+ /**
744
+ * Check if rate limit is exceeded
745
+ * @returns True if rate limited, false otherwise
746
+ */
747
+ private checkRateLimit(): boolean {
748
+ const now = Date.now();
749
+ const elapsed = now - this.rateLimitLastCheck;
750
+
751
+ // Reset counter if more than a minute has passed
752
+ if (elapsed >= 60000) {
753
+ this.rateLimitLastCheck = now;
754
+ this.rateLimitCounter = 0;
755
+ this.throttled = false;
756
+ this.stats.rateLimiting.currentRate = 0;
757
+ return false;
758
+ }
759
+
760
+ // Check if we're already throttled
761
+ if (this.throttled) {
762
+ return true;
763
+ }
764
+
765
+ // Increment counter
766
+ this.rateLimitCounter++;
767
+
768
+ // Calculate current rate (emails per minute)
769
+ const rate = (this.rateLimitCounter / elapsed) * 60000;
770
+ this.stats.rateLimiting.currentRate = rate;
771
+
772
+ // Check if rate limit is exceeded
773
+ if (rate > this.options.globalRateLimit) {
774
+ this.throttled = true;
775
+ this.stats.rateLimiting.throttled++;
776
+
777
+ // Schedule throttle reset
778
+ const resetDelay = 60000 - elapsed;
779
+ setTimeout(() => {
780
+ this.throttled = false;
781
+ this.rateLimitLastCheck = Date.now();
782
+ this.rateLimitCounter = 0;
783
+ this.stats.rateLimiting.currentRate = 0;
784
+ }, resetDelay);
785
+
786
+ return true;
787
+ }
788
+
789
+ return false;
790
+ }
791
+
792
+ /**
793
+ * Update delivery options
794
+ * @param options New options
795
+ */
796
+ public updateOptions(options: Partial<IMultiModeDeliveryOptions>): void {
797
+ this.options = {
798
+ ...this.options,
799
+ ...options
800
+ };
801
+
802
+ // Update rate limit statistics
803
+ if (options.globalRateLimit) {
804
+ this.stats.rateLimiting.globalLimit = options.globalRateLimit;
805
+ }
806
+
807
+ logger.log('info', 'MultiModeDeliverySystem options updated');
808
+ }
809
+
810
+ /**
811
+ * Get delivery statistics
812
+ */
813
+ public getStats(): IDeliveryStats {
814
+ return { ...this.stats };
815
+ }
816
+ }