@push.rocks/smartmta 5.1.3 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +15 -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 +403 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +502 -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 +964 -0
- package/ts/security/classes.securitylogger.ts +299 -0
- package/ts/security/index.ts +40 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { logger } from '../../logger.js';
|
|
6
|
+
import { type EmailProcessingMode } from './interfaces.js';
|
|
7
|
+
import type { IEmailRoute } from '../routing/interfaces.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Queue item status
|
|
11
|
+
*/
|
|
12
|
+
export type QueueItemStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Queue item interface
|
|
16
|
+
*/
|
|
17
|
+
export interface IQueueItem {
|
|
18
|
+
id: string;
|
|
19
|
+
processingMode: EmailProcessingMode;
|
|
20
|
+
processingResult: any;
|
|
21
|
+
route: IEmailRoute;
|
|
22
|
+
status: QueueItemStatus;
|
|
23
|
+
attempts: number;
|
|
24
|
+
nextAttempt: Date;
|
|
25
|
+
lastError?: string;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
deliveredAt?: Date;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Queue options interface
|
|
33
|
+
*/
|
|
34
|
+
export interface IQueueOptions {
|
|
35
|
+
// Storage options
|
|
36
|
+
storageType?: 'memory' | 'disk';
|
|
37
|
+
persistentPath?: string;
|
|
38
|
+
|
|
39
|
+
// Queue behavior
|
|
40
|
+
checkInterval?: number;
|
|
41
|
+
maxQueueSize?: number;
|
|
42
|
+
maxPerDestination?: number;
|
|
43
|
+
|
|
44
|
+
// Delivery attempts
|
|
45
|
+
maxRetries?: number;
|
|
46
|
+
baseRetryDelay?: number;
|
|
47
|
+
maxRetryDelay?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Queue statistics interface
|
|
52
|
+
*/
|
|
53
|
+
export interface IQueueStats {
|
|
54
|
+
queueSize: number;
|
|
55
|
+
status: {
|
|
56
|
+
pending: number;
|
|
57
|
+
processing: number;
|
|
58
|
+
delivered: number;
|
|
59
|
+
failed: number;
|
|
60
|
+
deferred: number;
|
|
61
|
+
};
|
|
62
|
+
modes: {
|
|
63
|
+
forward: number;
|
|
64
|
+
mta: number;
|
|
65
|
+
process: number;
|
|
66
|
+
};
|
|
67
|
+
oldestItem?: Date;
|
|
68
|
+
newestItem?: Date;
|
|
69
|
+
averageAttempts: number;
|
|
70
|
+
totalProcessed: number;
|
|
71
|
+
processingActive: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A unified queue for all email modes
|
|
76
|
+
*/
|
|
77
|
+
export class UnifiedDeliveryQueue extends EventEmitter {
|
|
78
|
+
private options: Required<IQueueOptions>;
|
|
79
|
+
private queue: Map<string, IQueueItem> = new Map();
|
|
80
|
+
private checkTimer?: NodeJS.Timeout;
|
|
81
|
+
private stats: IQueueStats;
|
|
82
|
+
private processing: boolean = false;
|
|
83
|
+
private totalProcessed: number = 0;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a new unified delivery queue
|
|
87
|
+
* @param options Queue options
|
|
88
|
+
*/
|
|
89
|
+
constructor(options: IQueueOptions) {
|
|
90
|
+
super();
|
|
91
|
+
|
|
92
|
+
// Set default options
|
|
93
|
+
this.options = {
|
|
94
|
+
storageType: options.storageType || 'memory',
|
|
95
|
+
persistentPath: options.persistentPath || path.join(process.cwd(), 'email-queue'),
|
|
96
|
+
checkInterval: options.checkInterval || 30000, // 30 seconds
|
|
97
|
+
maxQueueSize: options.maxQueueSize || 10000,
|
|
98
|
+
maxPerDestination: options.maxPerDestination || 100,
|
|
99
|
+
maxRetries: options.maxRetries || 5,
|
|
100
|
+
baseRetryDelay: options.baseRetryDelay || 60000, // 1 minute
|
|
101
|
+
maxRetryDelay: options.maxRetryDelay || 3600000 // 1 hour
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Initialize statistics
|
|
105
|
+
this.stats = {
|
|
106
|
+
queueSize: 0,
|
|
107
|
+
status: {
|
|
108
|
+
pending: 0,
|
|
109
|
+
processing: 0,
|
|
110
|
+
delivered: 0,
|
|
111
|
+
failed: 0,
|
|
112
|
+
deferred: 0
|
|
113
|
+
},
|
|
114
|
+
modes: {
|
|
115
|
+
forward: 0,
|
|
116
|
+
mta: 0,
|
|
117
|
+
process: 0
|
|
118
|
+
},
|
|
119
|
+
averageAttempts: 0,
|
|
120
|
+
totalProcessed: 0,
|
|
121
|
+
processingActive: false
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Initialize the queue
|
|
127
|
+
*/
|
|
128
|
+
public async initialize(): Promise<void> {
|
|
129
|
+
logger.log('info', 'Initializing UnifiedDeliveryQueue');
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Create persistent storage directory if using disk storage
|
|
133
|
+
if (this.options.storageType === 'disk') {
|
|
134
|
+
if (!fs.existsSync(this.options.persistentPath)) {
|
|
135
|
+
fs.mkdirSync(this.options.persistentPath, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Load existing items from disk
|
|
139
|
+
await this.loadFromDisk();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Start the queue processing timer
|
|
143
|
+
this.startProcessing();
|
|
144
|
+
|
|
145
|
+
// Emit initialized event
|
|
146
|
+
this.emit('initialized');
|
|
147
|
+
logger.log('info', 'UnifiedDeliveryQueue initialized successfully');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.log('error', `Failed to initialize queue: ${error.message}`);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Start queue processing
|
|
156
|
+
*/
|
|
157
|
+
private startProcessing(): void {
|
|
158
|
+
if (this.checkTimer) {
|
|
159
|
+
clearInterval(this.checkTimer);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.checkTimer = setInterval(() => this.processQueue(), this.options.checkInterval);
|
|
163
|
+
this.processing = true;
|
|
164
|
+
this.stats.processingActive = true;
|
|
165
|
+
this.emit('processingStarted');
|
|
166
|
+
logger.log('info', 'Queue processing started');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Stop queue processing
|
|
171
|
+
*/
|
|
172
|
+
private stopProcessing(): void {
|
|
173
|
+
if (this.checkTimer) {
|
|
174
|
+
clearInterval(this.checkTimer);
|
|
175
|
+
this.checkTimer = undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.processing = false;
|
|
179
|
+
this.stats.processingActive = false;
|
|
180
|
+
this.emit('processingStopped');
|
|
181
|
+
logger.log('info', 'Queue processing stopped');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check for items that need to be processed
|
|
186
|
+
*/
|
|
187
|
+
private async processQueue(): Promise<void> {
|
|
188
|
+
try {
|
|
189
|
+
const now = new Date();
|
|
190
|
+
let readyItems: IQueueItem[] = [];
|
|
191
|
+
|
|
192
|
+
// Find items ready for processing
|
|
193
|
+
for (const item of this.queue.values()) {
|
|
194
|
+
if (item.status === 'pending' || (item.status === 'deferred' && item.nextAttempt <= now)) {
|
|
195
|
+
readyItems.push(item);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (readyItems.length === 0) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Sort by oldest first
|
|
204
|
+
readyItems.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
205
|
+
|
|
206
|
+
// Emit event for ready items
|
|
207
|
+
this.emit('itemsReady', readyItems);
|
|
208
|
+
logger.log('info', `Found ${readyItems.length} items ready for processing`);
|
|
209
|
+
|
|
210
|
+
// Update statistics
|
|
211
|
+
this.updateStats();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.log('error', `Error processing queue: ${error.message}`);
|
|
214
|
+
this.emit('error', error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Add an item to the queue
|
|
220
|
+
* @param processingResult Processing result to queue
|
|
221
|
+
* @param mode Processing mode
|
|
222
|
+
* @param route Email route
|
|
223
|
+
*/
|
|
224
|
+
public async enqueue(processingResult: any, mode: EmailProcessingMode, route: IEmailRoute): Promise<string> {
|
|
225
|
+
// Check if queue is full
|
|
226
|
+
if (this.queue.size >= this.options.maxQueueSize) {
|
|
227
|
+
throw new Error('Queue is full');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Generate a unique ID
|
|
231
|
+
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
232
|
+
|
|
233
|
+
// Create queue item
|
|
234
|
+
const item: IQueueItem = {
|
|
235
|
+
id,
|
|
236
|
+
processingMode: mode,
|
|
237
|
+
processingResult,
|
|
238
|
+
route,
|
|
239
|
+
status: 'pending',
|
|
240
|
+
attempts: 0,
|
|
241
|
+
nextAttempt: new Date(),
|
|
242
|
+
createdAt: new Date(),
|
|
243
|
+
updatedAt: new Date()
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Add to queue
|
|
247
|
+
this.queue.set(id, item);
|
|
248
|
+
|
|
249
|
+
// Persist to disk if using disk storage
|
|
250
|
+
if (this.options.storageType === 'disk') {
|
|
251
|
+
await this.persistItem(item);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Update statistics
|
|
255
|
+
this.updateStats();
|
|
256
|
+
|
|
257
|
+
// Emit event
|
|
258
|
+
this.emit('itemEnqueued', item);
|
|
259
|
+
logger.log('info', `Item enqueued with ID ${id}, mode: ${mode}`);
|
|
260
|
+
|
|
261
|
+
return id;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get an item from the queue
|
|
266
|
+
* @param id Item ID
|
|
267
|
+
*/
|
|
268
|
+
public getItem(id: string): IQueueItem | undefined {
|
|
269
|
+
return this.queue.get(id);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Mark an item as being processed
|
|
274
|
+
* @param id Item ID
|
|
275
|
+
*/
|
|
276
|
+
public async markProcessing(id: string): Promise<boolean> {
|
|
277
|
+
const item = this.queue.get(id);
|
|
278
|
+
|
|
279
|
+
if (!item) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Update status
|
|
284
|
+
item.status = 'processing';
|
|
285
|
+
item.attempts++;
|
|
286
|
+
item.updatedAt = new Date();
|
|
287
|
+
|
|
288
|
+
// Persist changes if using disk storage
|
|
289
|
+
if (this.options.storageType === 'disk') {
|
|
290
|
+
await this.persistItem(item);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Update statistics
|
|
294
|
+
this.updateStats();
|
|
295
|
+
|
|
296
|
+
// Emit event
|
|
297
|
+
this.emit('itemProcessing', item);
|
|
298
|
+
logger.log('info', `Item ${id} marked as processing, attempt ${item.attempts}`);
|
|
299
|
+
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Mark an item as delivered
|
|
305
|
+
* @param id Item ID
|
|
306
|
+
*/
|
|
307
|
+
public async markDelivered(id: string): Promise<boolean> {
|
|
308
|
+
const item = this.queue.get(id);
|
|
309
|
+
|
|
310
|
+
if (!item) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Update status
|
|
315
|
+
item.status = 'delivered';
|
|
316
|
+
item.updatedAt = new Date();
|
|
317
|
+
item.deliveredAt = new Date();
|
|
318
|
+
|
|
319
|
+
// Persist changes if using disk storage
|
|
320
|
+
if (this.options.storageType === 'disk') {
|
|
321
|
+
await this.persistItem(item);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Update statistics
|
|
325
|
+
this.totalProcessed++;
|
|
326
|
+
this.updateStats();
|
|
327
|
+
|
|
328
|
+
// Emit event
|
|
329
|
+
this.emit('itemDelivered', item);
|
|
330
|
+
logger.log('info', `Item ${id} marked as delivered after ${item.attempts} attempts`);
|
|
331
|
+
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Mark an item as failed
|
|
337
|
+
* @param id Item ID
|
|
338
|
+
* @param error Error message
|
|
339
|
+
*/
|
|
340
|
+
public async markFailed(id: string, error: string): Promise<boolean> {
|
|
341
|
+
const item = this.queue.get(id);
|
|
342
|
+
|
|
343
|
+
if (!item) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Determine if we should retry
|
|
348
|
+
if (item.attempts < this.options.maxRetries) {
|
|
349
|
+
// Calculate next retry time with exponential backoff
|
|
350
|
+
const delay = Math.min(
|
|
351
|
+
this.options.baseRetryDelay * Math.pow(2, item.attempts - 1),
|
|
352
|
+
this.options.maxRetryDelay
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Update status
|
|
356
|
+
item.status = 'deferred';
|
|
357
|
+
item.lastError = error;
|
|
358
|
+
item.nextAttempt = new Date(Date.now() + delay);
|
|
359
|
+
item.updatedAt = new Date();
|
|
360
|
+
|
|
361
|
+
// Persist changes if using disk storage
|
|
362
|
+
if (this.options.storageType === 'disk') {
|
|
363
|
+
await this.persistItem(item);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Emit event
|
|
367
|
+
this.emit('itemDeferred', item);
|
|
368
|
+
logger.log('info', `Item ${id} deferred for ${delay}ms, attempt ${item.attempts}, error: ${error}`);
|
|
369
|
+
} else {
|
|
370
|
+
// Mark as permanently failed
|
|
371
|
+
item.status = 'failed';
|
|
372
|
+
item.lastError = error;
|
|
373
|
+
item.updatedAt = new Date();
|
|
374
|
+
|
|
375
|
+
// Persist changes if using disk storage
|
|
376
|
+
if (this.options.storageType === 'disk') {
|
|
377
|
+
await this.persistItem(item);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Update statistics
|
|
381
|
+
this.totalProcessed++;
|
|
382
|
+
|
|
383
|
+
// Emit event
|
|
384
|
+
this.emit('itemFailed', item);
|
|
385
|
+
logger.log('warn', `Item ${id} permanently failed after ${item.attempts} attempts, error: ${error}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Update statistics
|
|
389
|
+
this.updateStats();
|
|
390
|
+
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Remove an item from the queue
|
|
396
|
+
* @param id Item ID
|
|
397
|
+
*/
|
|
398
|
+
public async removeItem(id: string): Promise<boolean> {
|
|
399
|
+
const item = this.queue.get(id);
|
|
400
|
+
|
|
401
|
+
if (!item) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Remove from queue
|
|
406
|
+
this.queue.delete(id);
|
|
407
|
+
|
|
408
|
+
// Remove from disk if using disk storage
|
|
409
|
+
if (this.options.storageType === 'disk') {
|
|
410
|
+
await this.removeItemFromDisk(id);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Update statistics
|
|
414
|
+
this.updateStats();
|
|
415
|
+
|
|
416
|
+
// Emit event
|
|
417
|
+
this.emit('itemRemoved', item);
|
|
418
|
+
logger.log('info', `Item ${id} removed from queue`);
|
|
419
|
+
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Persist an item to disk
|
|
425
|
+
* @param item Item to persist
|
|
426
|
+
*/
|
|
427
|
+
private async persistItem(item: IQueueItem): Promise<void> {
|
|
428
|
+
try {
|
|
429
|
+
const filePath = path.join(this.options.persistentPath, `${item.id}.json`);
|
|
430
|
+
await fs.promises.writeFile(filePath, JSON.stringify(item, null, 2), 'utf8');
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger.log('error', `Failed to persist item ${item.id}: ${error.message}`);
|
|
433
|
+
this.emit('error', error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Remove an item from disk
|
|
439
|
+
* @param id Item ID
|
|
440
|
+
*/
|
|
441
|
+
private async removeItemFromDisk(id: string): Promise<void> {
|
|
442
|
+
try {
|
|
443
|
+
const filePath = path.join(this.options.persistentPath, `${id}.json`);
|
|
444
|
+
|
|
445
|
+
if (fs.existsSync(filePath)) {
|
|
446
|
+
await fs.promises.unlink(filePath);
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
logger.log('error', `Failed to remove item ${id} from disk: ${error.message}`);
|
|
450
|
+
this.emit('error', error);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Load queue items from disk
|
|
456
|
+
*/
|
|
457
|
+
private async loadFromDisk(): Promise<void> {
|
|
458
|
+
try {
|
|
459
|
+
// Check if directory exists
|
|
460
|
+
if (!fs.existsSync(this.options.persistentPath)) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Get all JSON files
|
|
465
|
+
const files = fs.readdirSync(this.options.persistentPath).filter(file => file.endsWith('.json'));
|
|
466
|
+
|
|
467
|
+
// Load each file
|
|
468
|
+
for (const file of files) {
|
|
469
|
+
try {
|
|
470
|
+
const filePath = path.join(this.options.persistentPath, file);
|
|
471
|
+
const data = await fs.promises.readFile(filePath, 'utf8');
|
|
472
|
+
const item = JSON.parse(data) as IQueueItem;
|
|
473
|
+
|
|
474
|
+
// Convert date strings to Date objects
|
|
475
|
+
item.createdAt = new Date(item.createdAt);
|
|
476
|
+
item.updatedAt = new Date(item.updatedAt);
|
|
477
|
+
item.nextAttempt = new Date(item.nextAttempt);
|
|
478
|
+
if (item.deliveredAt) {
|
|
479
|
+
item.deliveredAt = new Date(item.deliveredAt);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Add to queue
|
|
483
|
+
this.queue.set(item.id, item);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
logger.log('error', `Failed to load item from ${file}: ${error.message}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Update statistics
|
|
490
|
+
this.updateStats();
|
|
491
|
+
|
|
492
|
+
logger.log('info', `Loaded ${this.queue.size} items from disk`);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
logger.log('error', `Failed to load items from disk: ${error.message}`);
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Update queue statistics
|
|
501
|
+
*/
|
|
502
|
+
private updateStats(): void {
|
|
503
|
+
// Reset counters
|
|
504
|
+
this.stats.queueSize = this.queue.size;
|
|
505
|
+
this.stats.status = {
|
|
506
|
+
pending: 0,
|
|
507
|
+
processing: 0,
|
|
508
|
+
delivered: 0,
|
|
509
|
+
failed: 0,
|
|
510
|
+
deferred: 0
|
|
511
|
+
};
|
|
512
|
+
this.stats.modes = {
|
|
513
|
+
forward: 0,
|
|
514
|
+
mta: 0,
|
|
515
|
+
process: 0
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
let totalAttempts = 0;
|
|
519
|
+
let oldestTime = Date.now();
|
|
520
|
+
let newestTime = 0;
|
|
521
|
+
|
|
522
|
+
// Count by status and mode
|
|
523
|
+
for (const item of this.queue.values()) {
|
|
524
|
+
// Count by status
|
|
525
|
+
this.stats.status[item.status]++;
|
|
526
|
+
|
|
527
|
+
// Count by mode
|
|
528
|
+
this.stats.modes[item.processingMode]++;
|
|
529
|
+
|
|
530
|
+
// Track total attempts
|
|
531
|
+
totalAttempts += item.attempts;
|
|
532
|
+
|
|
533
|
+
// Track oldest and newest
|
|
534
|
+
const itemTime = item.createdAt.getTime();
|
|
535
|
+
if (itemTime < oldestTime) {
|
|
536
|
+
oldestTime = itemTime;
|
|
537
|
+
}
|
|
538
|
+
if (itemTime > newestTime) {
|
|
539
|
+
newestTime = itemTime;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Calculate average attempts
|
|
544
|
+
this.stats.averageAttempts = this.queue.size > 0 ? totalAttempts / this.queue.size : 0;
|
|
545
|
+
|
|
546
|
+
// Set oldest and newest
|
|
547
|
+
this.stats.oldestItem = this.queue.size > 0 ? new Date(oldestTime) : undefined;
|
|
548
|
+
this.stats.newestItem = this.queue.size > 0 ? new Date(newestTime) : undefined;
|
|
549
|
+
|
|
550
|
+
// Set total processed
|
|
551
|
+
this.stats.totalProcessed = this.totalProcessed;
|
|
552
|
+
|
|
553
|
+
// Set processing active
|
|
554
|
+
this.stats.processingActive = this.processing;
|
|
555
|
+
|
|
556
|
+
// Emit statistics event
|
|
557
|
+
this.emit('statsUpdated', this.stats);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Get queue statistics
|
|
562
|
+
*/
|
|
563
|
+
public getStats(): IQueueStats {
|
|
564
|
+
return { ...this.stats };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Pause queue processing
|
|
569
|
+
*/
|
|
570
|
+
public pause(): void {
|
|
571
|
+
if (this.processing) {
|
|
572
|
+
this.stopProcessing();
|
|
573
|
+
logger.log('info', 'Queue processing paused');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Resume queue processing
|
|
579
|
+
*/
|
|
580
|
+
public resume(): void {
|
|
581
|
+
if (!this.processing) {
|
|
582
|
+
this.startProcessing();
|
|
583
|
+
logger.log('info', 'Queue processing resumed');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Clean up old delivered and failed items
|
|
589
|
+
* @param maxAge Maximum age in milliseconds (default: 7 days)
|
|
590
|
+
*/
|
|
591
|
+
public async cleanupOldItems(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> {
|
|
592
|
+
const cutoff = new Date(Date.now() - maxAge);
|
|
593
|
+
let removedCount = 0;
|
|
594
|
+
|
|
595
|
+
// Find old items
|
|
596
|
+
for (const item of this.queue.values()) {
|
|
597
|
+
if (['delivered', 'failed'].includes(item.status) && item.updatedAt < cutoff) {
|
|
598
|
+
// Remove item
|
|
599
|
+
await this.removeItem(item.id);
|
|
600
|
+
removedCount++;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
logger.log('info', `Cleaned up ${removedCount} old items`);
|
|
605
|
+
return removedCount;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Shutdown the queue
|
|
610
|
+
*/
|
|
611
|
+
public async shutdown(): Promise<void> {
|
|
612
|
+
logger.log('info', 'Shutting down UnifiedDeliveryQueue');
|
|
613
|
+
|
|
614
|
+
// Stop processing
|
|
615
|
+
this.stopProcessing();
|
|
616
|
+
|
|
617
|
+
// Clear the check timer to prevent memory leaks
|
|
618
|
+
if (this.checkTimer) {
|
|
619
|
+
clearInterval(this.checkTimer);
|
|
620
|
+
this.checkTimer = undefined;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// If using disk storage, make sure all items are persisted
|
|
624
|
+
if (this.options.storageType === 'disk') {
|
|
625
|
+
const pendingWrites: Promise<void>[] = [];
|
|
626
|
+
|
|
627
|
+
for (const item of this.queue.values()) {
|
|
628
|
+
pendingWrites.push(this.persistItem(item));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Wait for all writes to complete
|
|
632
|
+
await Promise.all(pendingWrites);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Clear the queue (memory only)
|
|
636
|
+
this.queue.clear();
|
|
637
|
+
|
|
638
|
+
// Update statistics
|
|
639
|
+
this.updateStats();
|
|
640
|
+
|
|
641
|
+
// Emit shutdown event
|
|
642
|
+
this.emit('shutdown');
|
|
643
|
+
logger.log('info', 'UnifiedDeliveryQueue shut down successfully');
|
|
644
|
+
}
|
|
645
|
+
}
|