@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,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
+ }