@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.
- package/changelog.md +7 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/logger.d.ts +17 -0
- package/dist_ts/logger.js +76 -0
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +185 -0
- package/dist_ts/mail/core/classes.bouncemanager.js +569 -0
- package/dist_ts/mail/core/classes.email.d.ts +291 -0
- package/dist_ts/mail/core/classes.email.js +802 -0
- package/dist_ts/mail/core/classes.emailvalidator.d.ts +61 -0
- package/dist_ts/mail/core/classes.emailvalidator.js +184 -0
- package/dist_ts/mail/core/classes.templatemanager.d.ts +95 -0
- package/dist_ts/mail/core/classes.templatemanager.js +240 -0
- package/dist_ts/mail/core/index.d.ts +4 -0
- package/dist_ts/mail/core/index.js +6 -0
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +163 -0
- package/dist_ts/mail/delivery/classes.delivery.queue.js +488 -0
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +160 -0
- package/dist_ts/mail/delivery/classes.delivery.system.js +630 -0
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.d.ts +200 -0
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +820 -0
- package/dist_ts/mail/delivery/index.d.ts +4 -0
- package/dist_ts/mail/delivery/index.js +6 -0
- package/dist_ts/mail/delivery/interfaces.d.ts +140 -0
- package/dist_ts/mail/delivery/interfaces.js +17 -0
- package/dist_ts/mail/index.d.ts +7 -0
- package/dist_ts/mail/index.js +12 -0
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +25 -0
- package/dist_ts/mail/routing/classes.dkim.manager.js +127 -0
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +79 -0
- package/dist_ts/mail/routing/classes.dns.manager.js +415 -0
- package/dist_ts/mail/routing/classes.domain.registry.d.ts +54 -0
- package/dist_ts/mail/routing/classes.domain.registry.js +119 -0
- package/dist_ts/mail/routing/classes.email.action.executor.d.ts +33 -0
- package/dist_ts/mail/routing/classes.email.action.executor.js +137 -0
- package/dist_ts/mail/routing/classes.email.router.d.ts +171 -0
- package/dist_ts/mail/routing/classes.email.router.js +494 -0
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +241 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +935 -0
- package/dist_ts/mail/routing/index.d.ts +7 -0
- package/dist_ts/mail/routing/index.js +9 -0
- package/dist_ts/mail/routing/interfaces.d.ts +187 -0
- package/dist_ts/mail/routing/interfaces.js +2 -0
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +72 -0
- package/dist_ts/mail/security/classes.dkimcreator.js +360 -0
- package/dist_ts/mail/security/classes.spfverifier.d.ts +62 -0
- package/dist_ts/mail/security/classes.spfverifier.js +87 -0
- package/dist_ts/mail/security/index.d.ts +2 -0
- package/dist_ts/mail/security/index.js +4 -0
- package/dist_ts/paths.d.ts +14 -0
- package/dist_ts/paths.js +39 -0
- package/dist_ts/plugins.d.ts +24 -0
- package/dist_ts/plugins.js +28 -0
- package/dist_ts/security/classes.contentscanner.d.ts +130 -0
- package/dist_ts/security/classes.contentscanner.js +338 -0
- package/dist_ts/security/classes.ipreputationchecker.d.ts +73 -0
- package/dist_ts/security/classes.ipreputationchecker.js +263 -0
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +398 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +484 -0
- package/dist_ts/security/classes.securitylogger.d.ts +140 -0
- package/dist_ts/security/classes.securitylogger.js +235 -0
- package/dist_ts/security/index.d.ts +4 -0
- package/dist_ts/security/index.js +5 -0
- package/package.json +6 -1
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/index.ts +3 -0
- package/ts/logger.ts +91 -0
- package/ts/mail/core/classes.bouncemanager.ts +731 -0
- package/ts/mail/core/classes.email.ts +942 -0
- package/ts/mail/core/classes.emailvalidator.ts +239 -0
- package/ts/mail/core/classes.templatemanager.ts +320 -0
- package/ts/mail/core/index.ts +5 -0
- package/ts/mail/delivery/classes.delivery.queue.ts +645 -0
- package/ts/mail/delivery/classes.delivery.system.ts +816 -0
- package/ts/mail/delivery/classes.unified.rate.limiter.ts +1053 -0
- package/ts/mail/delivery/index.ts +5 -0
- package/ts/mail/delivery/interfaces.ts +167 -0
- package/ts/mail/index.ts +17 -0
- package/ts/mail/routing/classes.dkim.manager.ts +157 -0
- package/ts/mail/routing/classes.dns.manager.ts +573 -0
- package/ts/mail/routing/classes.domain.registry.ts +139 -0
- package/ts/mail/routing/classes.email.action.executor.ts +175 -0
- package/ts/mail/routing/classes.email.router.ts +575 -0
- package/ts/mail/routing/classes.unified.email.server.ts +1207 -0
- package/ts/mail/routing/index.ts +9 -0
- package/ts/mail/routing/interfaces.ts +202 -0
- package/ts/mail/security/classes.dkimcreator.ts +447 -0
- package/ts/mail/security/classes.spfverifier.ts +126 -0
- package/ts/mail/security/index.ts +3 -0
- package/ts/paths.ts +48 -0
- package/ts/plugins.ts +53 -0
- package/ts/security/classes.contentscanner.ts +400 -0
- package/ts/security/classes.ipreputationchecker.ts +315 -0
- package/ts/security/classes.rustsecuritybridge.ts +943 -0
- package/ts/security/classes.securitylogger.ts +299 -0
- 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
|
+
}
|