@push.rocks/smartmta 5.3.0 → 5.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +23 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +7 -1
- package/dist_ts/mail/core/classes.bouncemanager.js +25 -5
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +4 -2
- package/dist_ts/mail/delivery/classes.delivery.queue.js +30 -14
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +13 -1
- package/dist_ts/mail/index.d.ts +1 -0
- package/dist_ts/mail/index.js +2 -1
- package/dist_ts/mail/interfaces.storage.d.ts +7 -0
- package/dist_ts/mail/interfaces.storage.js +4 -0
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +2 -1
- package/dist_ts/mail/routing/classes.dkim.manager.js +17 -8
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +1 -5
- package/dist_ts/mail/routing/classes.dns.manager.js +2 -2
- package/dist_ts/mail/routing/classes.email.router.d.ts +2 -1
- package/dist_ts/mail/routing/classes.email.router.js +6 -5
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +17 -3
- package/dist_ts/mail/routing/classes.unified.email.server.js +32 -8
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +10 -5
- package/dist_ts/mail/security/classes.dkimcreator.js +91 -70
- package/dist_ts/security/classes.ipreputationchecker.d.ts +4 -3
- package/dist_ts/security/classes.ipreputationchecker.js +4 -3
- package/package.json +11 -11
- package/readme.hints.md +1 -1
- package/readme.md +17 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/core/classes.bouncemanager.ts +32 -9
- package/ts/mail/delivery/classes.delivery.queue.ts +40 -21
- package/ts/mail/delivery/classes.unified.rate.limiter.ts +15 -1
- package/ts/mail/index.ts +2 -1
- package/ts/mail/interfaces.storage.ts +13 -0
- package/ts/mail/routing/classes.dkim.manager.ts +24 -11
- package/ts/mail/routing/classes.dns.manager.ts +3 -8
- package/ts/mail/routing/classes.email.router.ts +7 -6
- package/ts/mail/routing/classes.unified.email.server.ts +52 -12
- package/ts/mail/security/classes.dkimcreator.ts +115 -91
- package/ts/security/classes.ipreputationchecker.ts +7 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import * as paths from '../../paths.js';
|
|
3
3
|
import { logger } from '../../logger.js';
|
|
4
|
+
import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
|
|
4
5
|
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
|
|
5
6
|
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
6
7
|
import { LRUCache } from 'lru-cache';
|
|
@@ -88,7 +89,10 @@ export class BounceManager {
|
|
|
88
89
|
|
|
89
90
|
// Store of bounced emails
|
|
90
91
|
private bounceStore: BounceRecord[] = [];
|
|
91
|
-
|
|
92
|
+
|
|
93
|
+
// Periodic cleanup timer for old bounce records
|
|
94
|
+
private cleanupInterval?: NodeJS.Timeout;
|
|
95
|
+
|
|
92
96
|
// Cache of recently bounced email addresses to avoid sending to known bad addresses
|
|
93
97
|
private bounceCache: LRUCache<string, {
|
|
94
98
|
lastBounce: number;
|
|
@@ -104,13 +108,13 @@ export class BounceManager {
|
|
|
104
108
|
expiresAt?: number; // undefined means permanent
|
|
105
109
|
}> = new Map();
|
|
106
110
|
|
|
107
|
-
private storageManager?:
|
|
111
|
+
private storageManager?: IStorageManagerLike;
|
|
108
112
|
|
|
109
113
|
constructor(options?: {
|
|
110
114
|
retryStrategy?: Partial<RetryStrategy>;
|
|
111
115
|
maxCacheSize?: number;
|
|
112
116
|
cacheTTL?: number;
|
|
113
|
-
|
|
117
|
+
storageManager?: IStorageManagerLike;
|
|
114
118
|
}) {
|
|
115
119
|
// Set retry strategy with defaults
|
|
116
120
|
if (options?.retryStrategy) {
|
|
@@ -135,6 +139,15 @@ export class BounceManager {
|
|
|
135
139
|
this.loadSuppressionList().catch(error => {
|
|
136
140
|
logger.log('error', `Failed to load suppression list on startup: ${error.message}`);
|
|
137
141
|
});
|
|
142
|
+
|
|
143
|
+
// Start periodic cleanup of old bounce records (every 1 hour, removes records older than 7 days)
|
|
144
|
+
this.cleanupInterval = setInterval(() => {
|
|
145
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
146
|
+
const removed = this.clearOldBounceRecords(sevenDaysAgo);
|
|
147
|
+
if (removed > 0) {
|
|
148
|
+
logger.log('info', `Auto-cleanup removed ${removed} old bounce records`);
|
|
149
|
+
}
|
|
150
|
+
}, 60 * 60 * 1000);
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
/**
|
|
@@ -540,7 +553,7 @@ export class BounceManager {
|
|
|
540
553
|
try {
|
|
541
554
|
const suppressionData = JSON.stringify(Array.from(this.suppressionList.entries()));
|
|
542
555
|
|
|
543
|
-
if (this.storageManager) {
|
|
556
|
+
if (hasStorageManagerMethods(this.storageManager, ['set'])) {
|
|
544
557
|
// Use storage manager
|
|
545
558
|
await this.storageManager.set('/email/bounces/suppression-list.json', suppressionData);
|
|
546
559
|
} else {
|
|
@@ -562,7 +575,7 @@ export class BounceManager {
|
|
|
562
575
|
let entries = null;
|
|
563
576
|
let needsMigration = false;
|
|
564
577
|
|
|
565
|
-
if (this.storageManager) {
|
|
578
|
+
if (hasStorageManagerMethods(this.storageManager, ['get'])) {
|
|
566
579
|
// Try to load from storage manager first
|
|
567
580
|
const suppressionData = await this.storageManager.get('/email/bounces/suppression-list.json');
|
|
568
581
|
|
|
@@ -624,7 +637,7 @@ export class BounceManager {
|
|
|
624
637
|
try {
|
|
625
638
|
const bounceData = JSON.stringify(bounce, null, 2);
|
|
626
639
|
|
|
627
|
-
if (this.storageManager) {
|
|
640
|
+
if (hasStorageManagerMethods(this.storageManager, ['set'])) {
|
|
628
641
|
// Use storage manager
|
|
629
642
|
await this.storageManager.set(`/email/bounces/records/${bounce.id}.json`, bounceData);
|
|
630
643
|
} else {
|
|
@@ -717,7 +730,7 @@ export class BounceManager {
|
|
|
717
730
|
*/
|
|
718
731
|
public clearOldBounceRecords(olderThan: number): number {
|
|
719
732
|
let removed = 0;
|
|
720
|
-
|
|
733
|
+
|
|
721
734
|
this.bounceStore = this.bounceStore.filter(bounce => {
|
|
722
735
|
if (bounce.timestamp < olderThan) {
|
|
723
736
|
removed++;
|
|
@@ -725,7 +738,17 @@ export class BounceManager {
|
|
|
725
738
|
}
|
|
726
739
|
return true;
|
|
727
740
|
});
|
|
728
|
-
|
|
741
|
+
|
|
729
742
|
return removed;
|
|
730
743
|
}
|
|
731
|
-
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Stop the bounce manager and clear cleanup timers
|
|
747
|
+
*/
|
|
748
|
+
public stop(): void {
|
|
749
|
+
if (this.cleanupInterval) {
|
|
750
|
+
clearInterval(this.cleanupInterval);
|
|
751
|
+
this.cleanupInterval = undefined;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
@@ -18,7 +18,7 @@ export interface IQueueItem {
|
|
|
18
18
|
id: string;
|
|
19
19
|
processingMode: EmailProcessingMode;
|
|
20
20
|
processingResult: any;
|
|
21
|
-
route
|
|
21
|
+
route?: IEmailRoute;
|
|
22
22
|
status: QueueItemStatus;
|
|
23
23
|
attempts: number;
|
|
24
24
|
nextAttempt: Date;
|
|
@@ -78,6 +78,7 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
78
78
|
private options: Required<IQueueOptions>;
|
|
79
79
|
private queue: Map<string, IQueueItem> = new Map();
|
|
80
80
|
private checkTimer?: NodeJS.Timeout;
|
|
81
|
+
private cleanupTimer?: NodeJS.Timeout;
|
|
81
82
|
private stats: IQueueStats;
|
|
82
83
|
private processing: boolean = false;
|
|
83
84
|
private totalProcessed: number = 0;
|
|
@@ -158,8 +159,19 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
158
159
|
if (this.checkTimer) {
|
|
159
160
|
clearInterval(this.checkTimer);
|
|
160
161
|
}
|
|
161
|
-
|
|
162
|
+
|
|
162
163
|
this.checkTimer = setInterval(() => this.processQueue(), this.options.checkInterval);
|
|
164
|
+
|
|
165
|
+
// Start periodic cleanup of delivered/failed items (every 30 minutes)
|
|
166
|
+
if (this.cleanupTimer) {
|
|
167
|
+
clearInterval(this.cleanupTimer);
|
|
168
|
+
}
|
|
169
|
+
this.cleanupTimer = setInterval(() => {
|
|
170
|
+
this.cleanupOldItems(24 * 60 * 60 * 1000).catch((err) => {
|
|
171
|
+
logger.log('error', `Auto-cleanup failed: ${err.message}`);
|
|
172
|
+
});
|
|
173
|
+
}, 30 * 60 * 1000);
|
|
174
|
+
|
|
163
175
|
this.processing = true;
|
|
164
176
|
this.stats.processingActive = true;
|
|
165
177
|
this.emit('processingStarted');
|
|
@@ -174,7 +186,11 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
174
186
|
clearInterval(this.checkTimer);
|
|
175
187
|
this.checkTimer = undefined;
|
|
176
188
|
}
|
|
177
|
-
|
|
189
|
+
if (this.cleanupTimer) {
|
|
190
|
+
clearInterval(this.cleanupTimer);
|
|
191
|
+
this.cleanupTimer = undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
this.processing = false;
|
|
179
195
|
this.stats.processingActive = false;
|
|
180
196
|
this.emit('processingStopped');
|
|
@@ -221,7 +237,7 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
221
237
|
* @param mode Processing mode
|
|
222
238
|
* @param route Email route
|
|
223
239
|
*/
|
|
224
|
-
public async enqueue(processingResult: any, mode: EmailProcessingMode, route
|
|
240
|
+
public async enqueue(processingResult: any, mode: EmailProcessingMode, route?: IEmailRoute): Promise<string> {
|
|
225
241
|
// Check if queue is full
|
|
226
242
|
if (this.queue.size >= this.options.maxQueueSize) {
|
|
227
243
|
throw new Error('Queue is full');
|
|
@@ -268,6 +284,10 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
268
284
|
public getItem(id: string): IQueueItem | undefined {
|
|
269
285
|
return this.queue.get(id);
|
|
270
286
|
}
|
|
287
|
+
|
|
288
|
+
public listItems(): IQueueItem[] {
|
|
289
|
+
return Array.from(this.queue.values()).map((item) => ({ ...item }));
|
|
290
|
+
}
|
|
271
291
|
|
|
272
292
|
/**
|
|
273
293
|
* Mark an item as being processed
|
|
@@ -590,19 +610,24 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
590
610
|
*/
|
|
591
611
|
public async cleanupOldItems(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> {
|
|
592
612
|
const cutoff = new Date(Date.now() - maxAge);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
613
|
+
|
|
614
|
+
// Collect IDs first to avoid modifying the Map during iteration
|
|
615
|
+
const idsToRemove: string[] = [];
|
|
596
616
|
for (const item of this.queue.values()) {
|
|
597
617
|
if (['delivered', 'failed'].includes(item.status) && item.updatedAt < cutoff) {
|
|
598
|
-
|
|
599
|
-
await this.removeItem(item.id);
|
|
600
|
-
removedCount++;
|
|
618
|
+
idsToRemove.push(item.id);
|
|
601
619
|
}
|
|
602
620
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
621
|
+
|
|
622
|
+
// Remove collected items
|
|
623
|
+
for (const id of idsToRemove) {
|
|
624
|
+
await this.removeItem(id);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (idsToRemove.length > 0) {
|
|
628
|
+
logger.log('info', `Cleaned up ${idsToRemove.length} old items from delivery queue`);
|
|
629
|
+
}
|
|
630
|
+
return idsToRemove.length;
|
|
606
631
|
}
|
|
607
632
|
|
|
608
633
|
/**
|
|
@@ -611,15 +636,9 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
611
636
|
public async shutdown(): Promise<void> {
|
|
612
637
|
logger.log('info', 'Shutting down UnifiedDeliveryQueue');
|
|
613
638
|
|
|
614
|
-
// Stop processing
|
|
639
|
+
// Stop processing (clears both check and cleanup timers)
|
|
615
640
|
this.stopProcessing();
|
|
616
641
|
|
|
617
|
-
// Clear the check timer to prevent memory leaks
|
|
618
|
-
if (this.checkTimer) {
|
|
619
|
-
clearInterval(this.checkTimer);
|
|
620
|
-
this.checkTimer = undefined;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
642
|
// If using disk storage, make sure all items are persisted
|
|
624
643
|
if (this.options.storageType === 'disk') {
|
|
625
644
|
const pendingWrites: Promise<void>[] = [];
|
|
@@ -642,4 +661,4 @@ export class UnifiedDeliveryQueue extends EventEmitter {
|
|
|
642
661
|
this.emit('shutdown');
|
|
643
662
|
logger.log('info', 'UnifiedDeliveryQueue shut down successfully');
|
|
644
663
|
}
|
|
645
|
-
}
|
|
664
|
+
}
|
|
@@ -231,7 +231,21 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
231
231
|
this.domainCounters.delete(key);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
|
|
234
|
+
|
|
235
|
+
// Clean stale stats.byIp entries for IPs that no longer have active counters or blocks
|
|
236
|
+
for (const ip of Object.keys(this.stats.byIp)) {
|
|
237
|
+
if (!this.ipCounters.has(ip) && !(this.config.blocks && ip in this.config.blocks)) {
|
|
238
|
+
delete this.stats.byIp[ip];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Clean stale stats.byPattern entries for patterns that no longer have active counters
|
|
243
|
+
for (const pattern of Object.keys(this.stats.byPattern)) {
|
|
244
|
+
if (!this.patternCounters.has(pattern)) {
|
|
245
|
+
delete this.stats.byPattern[pattern];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
235
249
|
// Update statistics
|
|
236
250
|
this.updateStats();
|
|
237
251
|
}
|
package/ts/mail/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Export all mail modules for simplified imports
|
|
2
|
+
export * from './interfaces.storage.js';
|
|
2
3
|
export * from './routing/index.js';
|
|
3
4
|
export * from './security/index.js';
|
|
4
5
|
|
|
@@ -14,4 +15,4 @@ import { Email } from './core/classes.email.js';
|
|
|
14
15
|
// Re-export commonly used classes
|
|
15
16
|
export {
|
|
16
17
|
Email,
|
|
17
|
-
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface IStorageManagerLike {
|
|
2
|
+
get?(key: string): Promise<string | null>;
|
|
3
|
+
set?(key: string, value: string): Promise<void>;
|
|
4
|
+
list?(prefix: string): Promise<string[]>;
|
|
5
|
+
delete?(key: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function hasStorageManagerMethods<T extends keyof IStorageManagerLike>(
|
|
9
|
+
storageManager: IStorageManagerLike | undefined,
|
|
10
|
+
methods: T[],
|
|
11
|
+
): storageManager is IStorageManagerLike & Required<Pick<IStorageManagerLike, T>> {
|
|
12
|
+
return !!storageManager && methods.every((method) => typeof storageManager[method] === 'function');
|
|
13
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { logger } from '../../logger.js';
|
|
2
2
|
import { DKIMCreator } from '../security/classes.dkimcreator.js';
|
|
3
|
+
import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
|
|
3
4
|
import { DomainRegistry } from './classes.domain.registry.js';
|
|
4
5
|
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
5
6
|
import { Email } from '../core/classes.email.js';
|
|
6
7
|
|
|
7
8
|
/** External DcRouter interface shape used by DkimManager */
|
|
8
9
|
interface DcRouter {
|
|
9
|
-
storageManager
|
|
10
|
+
storageManager?: IStorageManagerLike;
|
|
10
11
|
dnsServer?: any;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -39,11 +40,19 @@ export class DkimManager {
|
|
|
39
40
|
let keyPair: { privateKey: string; publicKey: string };
|
|
40
41
|
|
|
41
42
|
try {
|
|
42
|
-
keyPair =
|
|
43
|
+
keyPair = selector === 'default'
|
|
44
|
+
? await this.dkimCreator.readDKIMKeys(domain)
|
|
45
|
+
: await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
|
|
43
46
|
logger.log('info', `Using existing DKIM keys for domain: ${domain}`);
|
|
44
|
-
} catch
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
} catch {
|
|
48
|
+
await this.dkimCreator.handleDKIMKeysForSelector(
|
|
49
|
+
domain,
|
|
50
|
+
selector,
|
|
51
|
+
domainConfig.dkim?.keySize || 2048,
|
|
52
|
+
);
|
|
53
|
+
keyPair = selector === 'default'
|
|
54
|
+
? await this.dkimCreator.readDKIMKeys(domain)
|
|
55
|
+
: await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
|
|
47
56
|
logger.log('info', `Generated new DKIM keys for domain: ${domain}`);
|
|
48
57
|
}
|
|
49
58
|
|
|
@@ -106,10 +115,12 @@ export class DkimManager {
|
|
|
106
115
|
|
|
107
116
|
logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
|
|
108
117
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
if (hasStorageManagerMethods(this.dcRouter.storageManager, ['set'])) {
|
|
119
|
+
await this.dcRouter.storageManager.set(
|
|
120
|
+
`/email/dkim/${domain}/public.key`,
|
|
121
|
+
keyPair.publicKey
|
|
122
|
+
);
|
|
123
|
+
}
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
this.dkimCreator.cleanupOldKeys(domain, 30).catch(error => {
|
|
@@ -127,8 +138,10 @@ export class DkimManager {
|
|
|
127
138
|
|
|
128
139
|
async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> {
|
|
129
140
|
try {
|
|
130
|
-
await this.dkimCreator.
|
|
131
|
-
const { privateKey } =
|
|
141
|
+
await this.dkimCreator.handleDKIMKeysForSelector(domain, selector);
|
|
142
|
+
const { privateKey } = selector === 'default'
|
|
143
|
+
? await this.dkimCreator.readDKIMKeys(domain)
|
|
144
|
+
: await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
|
|
132
145
|
const rawEmail = email.toRFC822String();
|
|
133
146
|
|
|
134
147
|
// Detect key type from PEM header
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { IEmailDomainConfig } from './interfaces.js';
|
|
3
|
+
import type { IStorageManagerLike } from '../interfaces.storage.js';
|
|
3
4
|
import { logger } from '../../logger.js';
|
|
4
5
|
/** External DcRouter interface shape used by DnsManager */
|
|
5
6
|
interface IDcRouterLike {
|
|
@@ -8,12 +9,6 @@ interface IDcRouterLike {
|
|
|
8
9
|
options?: { dnsNsDomains?: string[]; dnsScopes?: string[] };
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
/** External StorageManager interface shape used by DnsManager */
|
|
12
|
-
interface IStorageManagerLike {
|
|
13
|
-
get(key: string): Promise<string | null>;
|
|
14
|
-
set(key: string, value: string): Promise<void>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
12
|
/**
|
|
18
13
|
* DNS validation result
|
|
19
14
|
*/
|
|
@@ -528,7 +523,7 @@ export class DnsManager {
|
|
|
528
523
|
|
|
529
524
|
try {
|
|
530
525
|
// Get DKIM DNS record from DKIMCreator
|
|
531
|
-
const dnsRecord = await dkimCreator.getDNSRecordForDomain(domain);
|
|
526
|
+
const dnsRecord = await dkimCreator.getDNSRecordForDomain(domain, selector);
|
|
532
527
|
|
|
533
528
|
// For internal-dns domains, register the DNS handler
|
|
534
529
|
if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
|
|
@@ -570,4 +565,4 @@ export class DnsManager {
|
|
|
570
565
|
}
|
|
571
566
|
}
|
|
572
567
|
}
|
|
573
|
-
}
|
|
568
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
|
|
3
4
|
import type { IEmailRoute, IEmailMatch, IEmailAction, IEmailContext } from './interfaces.js';
|
|
4
5
|
import type { Email } from '../core/classes.email.js';
|
|
5
6
|
|
|
@@ -9,7 +10,7 @@ import type { Email } from '../core/classes.email.js';
|
|
|
9
10
|
export class EmailRouter extends EventEmitter {
|
|
10
11
|
private routes: IEmailRoute[];
|
|
11
12
|
private patternCache: Map<string, boolean> = new Map();
|
|
12
|
-
private storageManager?:
|
|
13
|
+
private storageManager?: IStorageManagerLike;
|
|
13
14
|
private persistChanges: boolean;
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -18,7 +19,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
18
19
|
* @param options Router options
|
|
19
20
|
*/
|
|
20
21
|
constructor(routes: IEmailRoute[], options?: {
|
|
21
|
-
storageManager?:
|
|
22
|
+
storageManager?: IStorageManagerLike;
|
|
22
23
|
persistChanges?: boolean;
|
|
23
24
|
}) {
|
|
24
25
|
super();
|
|
@@ -27,7 +28,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
27
28
|
this.persistChanges = options?.persistChanges ?? !!this.storageManager;
|
|
28
29
|
|
|
29
30
|
// If storage manager is provided, try to load persisted routes
|
|
30
|
-
if (this.storageManager) {
|
|
31
|
+
if (hasStorageManagerMethods(this.storageManager, ['get'])) {
|
|
31
32
|
this.loadRoutes({ merge: true }).catch(error => {
|
|
32
33
|
console.error(`Failed to load persisted routes: ${error.message}`);
|
|
33
34
|
});
|
|
@@ -394,7 +395,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
394
395
|
* Save current routes to storage
|
|
395
396
|
*/
|
|
396
397
|
public async saveRoutes(): Promise<void> {
|
|
397
|
-
if (!this.storageManager) {
|
|
398
|
+
if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
|
|
398
399
|
this.emit('persistenceWarning', 'Cannot save routes: StorageManager not configured');
|
|
399
400
|
return;
|
|
400
401
|
}
|
|
@@ -425,7 +426,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
425
426
|
merge?: boolean; // Merge with existing routes
|
|
426
427
|
replace?: boolean; // Replace existing routes
|
|
427
428
|
}): Promise<IEmailRoute[]> {
|
|
428
|
-
if (!this.storageManager) {
|
|
429
|
+
if (!hasStorageManagerMethods(this.storageManager, ['get'])) {
|
|
429
430
|
this.emit('persistenceWarning', 'Cannot load routes: StorageManager not configured');
|
|
430
431
|
return [];
|
|
431
432
|
}
|
|
@@ -572,4 +573,4 @@ export class EmailRouter extends EventEmitter {
|
|
|
572
573
|
public getRoute(name: string): IEmailRoute | undefined {
|
|
573
574
|
return this.routes.find(r => r.name === name);
|
|
574
575
|
}
|
|
575
|
-
}
|
|
576
|
+
}
|
|
@@ -8,17 +8,18 @@ import {
|
|
|
8
8
|
SecurityEventType
|
|
9
9
|
} from '../../security/index.js';
|
|
10
10
|
import { DKIMCreator } from '../security/classes.dkimcreator.js';
|
|
11
|
+
import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
|
|
11
12
|
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
12
|
-
import type { IEmailReceivedEvent, IAuthRequestEvent
|
|
13
|
+
import type { IEmailReceivedEvent, IAuthRequestEvent } from '../../security/classes.rustsecuritybridge.js';
|
|
13
14
|
import { EmailRouter } from './classes.email.router.js';
|
|
14
|
-
import type { IEmailRoute,
|
|
15
|
+
import type { IEmailRoute, IEmailContext, IEmailDomainConfig } from './interfaces.js';
|
|
15
16
|
import { Email } from '../core/classes.email.js';
|
|
16
17
|
import { DomainRegistry } from './classes.domain.registry.js';
|
|
17
18
|
import { DnsManager } from './classes.dns.manager.js';
|
|
18
19
|
import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
|
|
19
20
|
import type { ISmtpSendResult, IOutboundEmail } from '../../security/classes.rustsecuritybridge.js';
|
|
20
|
-
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
|
|
21
|
-
import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
|
|
21
|
+
import { MultiModeDeliverySystem, type IDeliveryStats, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
|
|
22
|
+
import { UnifiedDeliveryQueue, type IQueueItem, type IQueueOptions, type IQueueStats } from '../delivery/classes.delivery.queue.js';
|
|
22
23
|
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
|
|
23
24
|
import { SmtpState } from '../delivery/interfaces.js';
|
|
24
25
|
import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
|
|
@@ -28,7 +29,7 @@ import { DkimManager } from './classes.dkim.manager.js';
|
|
|
28
29
|
|
|
29
30
|
/** External DcRouter interface shape used by UnifiedEmailServer */
|
|
30
31
|
interface DcRouter {
|
|
31
|
-
storageManager:
|
|
32
|
+
storageManager: IStorageManagerLike;
|
|
32
33
|
dnsServer?: any;
|
|
33
34
|
options?: any;
|
|
34
35
|
}
|
|
@@ -49,11 +50,14 @@ export interface IExtendedSmtpSession extends ISmtpSession {
|
|
|
49
50
|
export interface IUnifiedEmailServerOptions {
|
|
50
51
|
// Base server options
|
|
51
52
|
ports: number[];
|
|
53
|
+
/** Public SMTP hostname used for greeting/banner and as the default outbound identity. */
|
|
52
54
|
hostname: string;
|
|
53
55
|
domains: IEmailDomainConfig[]; // Domain configurations
|
|
54
56
|
banner?: string;
|
|
55
57
|
debug?: boolean;
|
|
56
58
|
useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
|
|
59
|
+
/** Persist router changes back into storage when a storage manager is available. */
|
|
60
|
+
persistRoutes?: boolean;
|
|
57
61
|
|
|
58
62
|
// Authentication options
|
|
59
63
|
auth?: {
|
|
@@ -92,6 +96,8 @@ export interface IUnifiedEmailServerOptions {
|
|
|
92
96
|
|
|
93
97
|
// Outbound settings
|
|
94
98
|
outbound?: {
|
|
99
|
+
/** Override the SMTP identity used for outbound delivery. Defaults to `hostname`. */
|
|
100
|
+
hostname?: string;
|
|
95
101
|
maxConnections?: number;
|
|
96
102
|
connectionTimeout?: number;
|
|
97
103
|
socketTimeout?: number;
|
|
@@ -99,6 +105,9 @@ export interface IUnifiedEmailServerOptions {
|
|
|
99
105
|
defaultFrom?: string;
|
|
100
106
|
};
|
|
101
107
|
|
|
108
|
+
// Delivery queue
|
|
109
|
+
queue?: IQueueOptions;
|
|
110
|
+
|
|
102
111
|
// Rate limiting (global limits, can be overridden per domain)
|
|
103
112
|
rateLimits?: IHierarchicalRateLimits;
|
|
104
113
|
}
|
|
@@ -206,7 +215,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
206
215
|
// Initialize email router with routes and storage manager
|
|
207
216
|
this.emailRouter = new EmailRouter(options.routes || [], {
|
|
208
217
|
storageManager: dcRouter.storageManager,
|
|
209
|
-
persistChanges:
|
|
218
|
+
persistChanges: options.persistRoutes ?? hasStorageManagerMethods(dcRouter.storageManager, ['get', 'set'])
|
|
210
219
|
});
|
|
211
220
|
|
|
212
221
|
// Initialize rate limiter
|
|
@@ -226,7 +235,8 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
226
235
|
storageType: 'memory', // Default to memory storage
|
|
227
236
|
maxRetries: 3,
|
|
228
237
|
baseRetryDelay: 300000, // 5 minutes
|
|
229
|
-
maxRetryDelay: 3600000 // 1 hour
|
|
238
|
+
maxRetryDelay: 3600000, // 1 hour
|
|
239
|
+
...options.queue,
|
|
230
240
|
};
|
|
231
241
|
|
|
232
242
|
this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
|
|
@@ -277,6 +287,14 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
277
287
|
// We'll create the SMTP servers during the start() method
|
|
278
288
|
}
|
|
279
289
|
|
|
290
|
+
private getAdvertisedHostname(): string {
|
|
291
|
+
return this.options.hostname;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private getOutboundHostname(): string {
|
|
295
|
+
return this.options.outbound?.hostname || this.options.hostname;
|
|
296
|
+
}
|
|
297
|
+
|
|
280
298
|
/**
|
|
281
299
|
* Send an outbound email via the Rust SMTP client.
|
|
282
300
|
* Uses connection pooling in the Rust binary for efficiency.
|
|
@@ -314,7 +332,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
314
332
|
host,
|
|
315
333
|
port,
|
|
316
334
|
secure: port === 465,
|
|
317
|
-
domain: this.
|
|
335
|
+
domain: this.getOutboundHostname(),
|
|
318
336
|
auth: options?.auth,
|
|
319
337
|
email: outboundEmail,
|
|
320
338
|
dkim,
|
|
@@ -455,7 +473,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
455
473
|
const securePort = (this.options.ports as number[]).find(p => p === 465);
|
|
456
474
|
|
|
457
475
|
const started = await this.rustBridge.startSmtpServer({
|
|
458
|
-
hostname: this.
|
|
476
|
+
hostname: this.getAdvertisedHostname(),
|
|
459
477
|
ports: smtpPorts,
|
|
460
478
|
securePort: securePort,
|
|
461
479
|
tlsCertPem,
|
|
@@ -518,6 +536,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
518
536
|
logger.log('info', 'Email delivery queue shut down');
|
|
519
537
|
}
|
|
520
538
|
|
|
539
|
+
this.bounceManager.stop();
|
|
540
|
+
logger.log('info', 'Bounce manager stopped');
|
|
541
|
+
|
|
521
542
|
// Close all Rust SMTP client connection pools
|
|
522
543
|
try {
|
|
523
544
|
await this.rustBridge.closeSmtpPool();
|
|
@@ -973,6 +994,10 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
973
994
|
this.emailRouter.updateRoutes(routes);
|
|
974
995
|
}
|
|
975
996
|
|
|
997
|
+
public getEmailRoutes(): IEmailRoute[] {
|
|
998
|
+
return this.emailRouter.getRoutes();
|
|
999
|
+
}
|
|
1000
|
+
|
|
976
1001
|
/**
|
|
977
1002
|
* Get server statistics
|
|
978
1003
|
*/
|
|
@@ -980,6 +1005,22 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
980
1005
|
return { ...this.stats };
|
|
981
1006
|
}
|
|
982
1007
|
|
|
1008
|
+
public getQueueStats(): IQueueStats {
|
|
1009
|
+
return this.deliveryQueue.getStats();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
public getQueueItems(): IQueueItem[] {
|
|
1013
|
+
return this.deliveryQueue.listItems();
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
public getQueueItem(id: string): IQueueItem | undefined {
|
|
1017
|
+
return this.deliveryQueue.getItem(id);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
public getDeliveryStats(): IDeliveryStats {
|
|
1021
|
+
return this.deliverySystem.getStats();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
983
1024
|
/**
|
|
984
1025
|
* Get domain registry
|
|
985
1026
|
*/
|
|
@@ -1039,11 +1080,10 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1039
1080
|
// Sign with DKIM if configured
|
|
1040
1081
|
if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
|
|
1041
1082
|
const domain = email.from.split('@')[1];
|
|
1042
|
-
await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || '
|
|
1083
|
+
await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'default');
|
|
1043
1084
|
}
|
|
1044
1085
|
|
|
1045
|
-
const id =
|
|
1046
|
-
await this.deliveryQueue.enqueue(email, mode, route);
|
|
1086
|
+
const id = await this.deliveryQueue.enqueue(email, mode, route);
|
|
1047
1087
|
|
|
1048
1088
|
logger.log('info', `Email queued with ID: ${id}`);
|
|
1049
1089
|
return id;
|