@push.rocks/smartmta 6.5.2 → 8.0.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 +40 -0
- package/dist_rust/mailer-bin_linux_amd64 +0 -0
- package/dist_rust/mailer-bin_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/functions.errors.d.ts +3 -0
- package/dist_ts/functions.errors.js +8 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +16 -12
- package/dist_ts/mail/core/classes.bouncemanager.js +146 -129
- package/dist_ts/mail/core/classes.email.js +15 -13
- package/dist_ts/mail/core/classes.emailvalidator.d.ts +3 -3
- package/dist_ts/mail/core/classes.emailvalidator.js +7 -5
- package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
- package/dist_ts/mail/core/classes.templatemanager.js +35 -51
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +92 -22
- package/dist_ts/mail/delivery/classes.delivery.queue.js +738 -151
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
- package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
- package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
- package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
- package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
- package/dist_ts/mail/delivery/interfaces.js +1 -1
- package/dist_ts/mail/interfaces.storage.d.ts +37 -6
- package/dist_ts/mail/interfaces.storage.js +33 -3
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +10 -6
- package/dist_ts/mail/routing/classes.dkim.manager.js +47 -31
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +7 -5
- package/dist_ts/mail/routing/classes.dns.manager.js +22 -11
- package/dist_ts/mail/routing/classes.email.action.executor.d.ts +2 -1
- package/dist_ts/mail/routing/classes.email.action.executor.js +45 -16
- package/dist_ts/mail/routing/classes.email.router.d.ts +5 -2
- package/dist_ts/mail/routing/classes.email.router.js +17 -12
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +12 -6
- package/dist_ts/mail/routing/classes.unified.email.server.js +69 -78
- package/dist_ts/mail/routing/interfaces.d.ts +0 -2
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +22 -45
- package/dist_ts/mail/security/classes.dkimcreator.js +93 -296
- package/dist_ts/mail/security/classes.spfverifier.js +5 -3
- package/dist_ts/paths.d.ts +0 -12
- package/dist_ts/paths.js +3 -36
- package/dist_ts/plugins.d.ts +2 -5
- package/dist_ts/plugins.js +3 -6
- package/dist_ts/security/classes.contentscanner.js +14 -12
- package/dist_ts/security/classes.ipreputationchecker.d.ts +9 -6
- package/dist_ts/security/classes.ipreputationchecker.js +42 -93
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +52 -4
- package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
- package/dist_ts/security/classes.securitylogger.js +7 -5
- package/dist_ts/security/index.d.ts +1 -1
- package/dist_ts/security/index.js +2 -2
- package/package.json +8 -9
- package/readme.hints.md +4 -3
- package/readme.md +50 -18
- package/readme.plan.md +6 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/functions.errors.ts +8 -0
- package/ts/index.ts +3 -0
- package/ts/mail/core/classes.bouncemanager.ts +188 -155
- package/ts/mail/core/classes.email.ts +20 -14
- package/ts/mail/core/classes.emailvalidator.ts +9 -7
- package/ts/mail/core/classes.templatemanager.ts +42 -57
- package/ts/mail/delivery/classes.delivery.queue.ts +996 -185
- package/ts/mail/delivery/classes.delivery.system.ts +583 -170
- package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
- package/ts/mail/delivery/functions.safe-observers.ts +45 -0
- package/ts/mail/delivery/interfaces.ts +27 -1
- package/ts/mail/interfaces.storage.ts +64 -10
- package/ts/mail/routing/classes.dkim.manager.ts +65 -40
- package/ts/mail/routing/classes.dns.manager.ts +39 -16
- package/ts/mail/routing/classes.email.action.executor.ts +64 -17
- package/ts/mail/routing/classes.email.router.ts +20 -13
- package/ts/mail/routing/classes.unified.email.server.ts +107 -86
- package/ts/mail/routing/interfaces.ts +0 -2
- package/ts/mail/security/classes.dkimcreator.ts +150 -355
- package/ts/mail/security/classes.spfverifier.ts +4 -2
- package/ts/paths.ts +2 -41
- package/ts/plugins.ts +1 -6
- package/ts/security/classes.contentscanner.ts +14 -12
- package/ts/security/classes.ipreputationchecker.ts +46 -99
- package/ts/security/classes.rustsecuritybridge.ts +272 -6
- package/ts/security/classes.securitylogger.ts +6 -4
- package/ts/security/index.ts +5 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from '../../logger.js';
|
|
2
|
+
import { getErrorMessage } from '../../functions.errors.js';
|
|
2
3
|
import {
|
|
3
4
|
SecurityLogger,
|
|
4
5
|
SecurityLogLevel,
|
|
@@ -8,7 +9,12 @@ import type { IEmailAction, IEmailContext } from './interfaces.js';
|
|
|
8
9
|
import { Email } from '../core/classes.email.js';
|
|
9
10
|
import { BounceManager } from '../core/classes.bouncemanager.js';
|
|
10
11
|
import { UnifiedDeliveryQueue } from '../delivery/classes.delivery.queue.js';
|
|
11
|
-
import
|
|
12
|
+
import { callObserverSafely } from '../delivery/functions.safe-observers.js';
|
|
13
|
+
import {
|
|
14
|
+
SmtpDeliveryError,
|
|
15
|
+
type ISmtpRecipientResult,
|
|
16
|
+
type ISmtpSendResult,
|
|
17
|
+
} from '../../security/classes.rustsecuritybridge.js';
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Dependencies injected from UnifiedEmailServer to avoid circular imports
|
|
@@ -30,6 +36,31 @@ export interface IActionExecutorDeps {
|
|
|
30
36
|
export class EmailActionExecutor {
|
|
31
37
|
constructor(private deps: IActionExecutorDeps) {}
|
|
32
38
|
|
|
39
|
+
private async suppressPermanentRcptFailures(
|
|
40
|
+
email: Email,
|
|
41
|
+
results: ISmtpRecipientResult[],
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
for (const result of results) {
|
|
44
|
+
if (result.accepted || result.responseCode < 500) continue;
|
|
45
|
+
try {
|
|
46
|
+
await this.deps.bounceManager.processSmtpFailure(
|
|
47
|
+
result.recipient,
|
|
48
|
+
`SMTP recipient rejected with ${result.responseCode}`,
|
|
49
|
+
{
|
|
50
|
+
sender: email.from,
|
|
51
|
+
originalEmailId: email.headers['Message-ID'] as string,
|
|
52
|
+
statusCode: String(result.responseCode),
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.log(
|
|
57
|
+
'error',
|
|
58
|
+
`Failed to persist permanent RCPT failure for ${result.recipient}: ${(error as Error).message}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
async executeAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
|
|
34
65
|
switch (action.type) {
|
|
35
66
|
case 'forward':
|
|
@@ -72,13 +103,14 @@ export class EmailActionExecutor {
|
|
|
72
103
|
|
|
73
104
|
try {
|
|
74
105
|
// Send email via Rust SMTP client
|
|
75
|
-
await this.deps.sendOutboundEmail(host, port, email, {
|
|
106
|
+
const result = await this.deps.sendOutboundEmail(host, port, email, {
|
|
76
107
|
auth: auth as { user: string; pass: string } | undefined,
|
|
77
108
|
});
|
|
109
|
+
await this.suppressPermanentRcptFailures(email, result.recipientResults || []);
|
|
78
110
|
|
|
79
111
|
logger.log('info', `Successfully forwarded email to ${host}:${port}`);
|
|
80
112
|
|
|
81
|
-
SecurityLogger.getInstance().logEvent({
|
|
113
|
+
await callObserverSafely('forward success security log', () => SecurityLogger.getInstance().logEvent({
|
|
82
114
|
level: SecurityLogLevel.INFO,
|
|
83
115
|
type: SecurityEventType.EMAIL_FORWARDING,
|
|
84
116
|
message: 'Email forwarded successfully',
|
|
@@ -91,11 +123,12 @@ export class EmailActionExecutor {
|
|
|
91
123
|
recipients: email.to
|
|
92
124
|
},
|
|
93
125
|
success: true
|
|
94
|
-
});
|
|
126
|
+
}));
|
|
95
127
|
} catch (error) {
|
|
96
|
-
|
|
128
|
+
const errorMessage = getErrorMessage(error);
|
|
129
|
+
logger.log('error', `Failed to forward email: ${errorMessage}`);
|
|
97
130
|
|
|
98
|
-
SecurityLogger.getInstance().logEvent({
|
|
131
|
+
await callObserverSafely('forward failure security log', () => SecurityLogger.getInstance().logEvent({
|
|
99
132
|
level: SecurityLogLevel.ERROR,
|
|
100
133
|
type: SecurityEventType.EMAIL_FORWARDING,
|
|
101
134
|
message: 'Email forwarding failed',
|
|
@@ -105,17 +138,31 @@ export class EmailActionExecutor {
|
|
|
105
138
|
routeName: context.session.matchedRoute?.name,
|
|
106
139
|
targetHost: host,
|
|
107
140
|
targetPort: port,
|
|
108
|
-
error:
|
|
141
|
+
error: errorMessage
|
|
109
142
|
},
|
|
110
143
|
success: false
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const smtpError = SmtpDeliveryError.from(error);
|
|
147
|
+
if (
|
|
148
|
+
!smtpError.retryable
|
|
149
|
+
&& smtpError.errorType === 'protocol'
|
|
150
|
+
&& smtpError.phase === 'rcpt_to'
|
|
151
|
+
&& (
|
|
152
|
+
(typeof smtpError.smtpCode === 'number' && smtpError.smtpCode >= 500)
|
|
153
|
+
|| smtpError.recipientResults.some(
|
|
154
|
+
(result) => !result.accepted && result.responseCode >= 500,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
) {
|
|
158
|
+
const permanentResults = smtpError.recipientResults.length > 0
|
|
159
|
+
? smtpError.recipientResults
|
|
160
|
+
: smtpError.recipients.map((recipient) => ({
|
|
161
|
+
recipient,
|
|
162
|
+
accepted: false,
|
|
163
|
+
responseCode: smtpError.smtpCode!,
|
|
164
|
+
}));
|
|
165
|
+
await this.suppressPermanentRcptFailures(email, permanentResults);
|
|
119
166
|
}
|
|
120
167
|
throw error;
|
|
121
168
|
}
|
|
@@ -151,7 +198,7 @@ export class EmailActionExecutor {
|
|
|
151
198
|
|
|
152
199
|
logger.log('info', `Rejecting email with code ${code}: ${message}`);
|
|
153
200
|
|
|
154
|
-
SecurityLogger.getInstance().logEvent({
|
|
201
|
+
await callObserverSafely('routing rejection security log', () => SecurityLogger.getInstance().logEvent({
|
|
155
202
|
level: SecurityLogLevel.WARN,
|
|
156
203
|
type: SecurityEventType.EMAIL_PROCESSING,
|
|
157
204
|
message: 'Email rejected by routing rule',
|
|
@@ -165,7 +212,7 @@ export class EmailActionExecutor {
|
|
|
165
212
|
to: _email.to
|
|
166
213
|
},
|
|
167
214
|
success: false
|
|
168
|
-
});
|
|
215
|
+
}));
|
|
169
216
|
|
|
170
217
|
// Throw error with SMTP code and message
|
|
171
218
|
const error = new Error(message);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import {
|
|
3
|
+
import type { IStorageManager } from '../interfaces.storage.js';
|
|
4
4
|
import type { IEmailRoute, IEmailMatch, IEmailAction, IEmailContext } from './interfaces.js';
|
|
5
5
|
import type { Email } from '../core/classes.email.js';
|
|
6
|
+
import { getErrorMessage } from '../../functions.errors.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Email router that evaluates routes and determines actions
|
|
@@ -10,8 +11,9 @@ import type { Email } from '../core/classes.email.js';
|
|
|
10
11
|
export class EmailRouter extends EventEmitter {
|
|
11
12
|
private routes: IEmailRoute[];
|
|
12
13
|
private patternCache: Map<string, boolean> = new Map();
|
|
13
|
-
private storageManager?:
|
|
14
|
+
private storageManager?: IStorageManager;
|
|
14
15
|
private persistChanges: boolean;
|
|
16
|
+
private initializationPromise: Promise<void>;
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Create a new email router
|
|
@@ -19,7 +21,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
19
21
|
* @param options Router options
|
|
20
22
|
*/
|
|
21
23
|
constructor(routes: IEmailRoute[], options?: {
|
|
22
|
-
storageManager?:
|
|
24
|
+
storageManager?: IStorageManager;
|
|
23
25
|
persistChanges?: boolean;
|
|
24
26
|
}) {
|
|
25
27
|
super();
|
|
@@ -27,12 +29,17 @@ export class EmailRouter extends EventEmitter {
|
|
|
27
29
|
this.storageManager = options?.storageManager;
|
|
28
30
|
this.persistChanges = options?.persistChanges ?? !!this.storageManager;
|
|
29
31
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
this.loadRoutes({ merge: true }).
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
// Capture hydration so callers can establish a real readiness barrier before routing.
|
|
33
|
+
const hydrationPromise = this.storageManager
|
|
34
|
+
? this.loadRoutes({ merge: true }).then(() => undefined)
|
|
35
|
+
: Promise.resolve();
|
|
36
|
+
hydrationPromise.catch(() => undefined);
|
|
37
|
+
this.initializationPromise = hydrationPromise;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Wait until managed routes have been hydrated. */
|
|
41
|
+
public async initialize(): Promise<void> {
|
|
42
|
+
await this.initializationPromise;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/**
|
|
@@ -395,7 +402,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
395
402
|
* Save current routes to storage
|
|
396
403
|
*/
|
|
397
404
|
public async saveRoutes(): Promise<void> {
|
|
398
|
-
if (!
|
|
405
|
+
if (!this.storageManager) {
|
|
399
406
|
this.emit('persistenceWarning', 'Cannot save routes: StorageManager not configured');
|
|
400
407
|
return;
|
|
401
408
|
}
|
|
@@ -413,7 +420,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
413
420
|
|
|
414
421
|
this.emit('routesPersisted', this.routes.length);
|
|
415
422
|
} catch (error) {
|
|
416
|
-
console.error(`Failed to save routes: ${error
|
|
423
|
+
console.error(`Failed to save routes: ${getErrorMessage(error)}`);
|
|
417
424
|
throw error;
|
|
418
425
|
}
|
|
419
426
|
}
|
|
@@ -426,7 +433,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
426
433
|
merge?: boolean; // Merge with existing routes
|
|
427
434
|
replace?: boolean; // Replace existing routes
|
|
428
435
|
}): Promise<IEmailRoute[]> {
|
|
429
|
-
if (!
|
|
436
|
+
if (!this.storageManager) {
|
|
430
437
|
this.emit('persistenceWarning', 'Cannot load routes: StorageManager not configured');
|
|
431
438
|
return [];
|
|
432
439
|
}
|
|
@@ -473,7 +480,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
473
480
|
|
|
474
481
|
return loadedRoutes;
|
|
475
482
|
} catch (error) {
|
|
476
|
-
console.error(`Failed to load routes: ${error
|
|
483
|
+
console.error(`Failed to load routes: ${getErrorMessage(error)}`);
|
|
477
484
|
throw error;
|
|
478
485
|
}
|
|
479
486
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
|
-
import * as paths from '../../paths.js';
|
|
3
2
|
import { EventEmitter } from 'events';
|
|
4
3
|
import { logger } from '../../logger.js';
|
|
4
|
+
import { getErrorMessage } from '../../functions.errors.js';
|
|
5
5
|
import {
|
|
6
6
|
SecurityLogger,
|
|
7
7
|
SecurityLogLevel,
|
|
8
8
|
SecurityEventType
|
|
9
9
|
} from '../../security/index.js';
|
|
10
10
|
import { DKIMCreator } from '../security/classes.dkimcreator.js';
|
|
11
|
-
import {
|
|
11
|
+
import type { IStorageManager } from '../interfaces.storage.js';
|
|
12
12
|
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
13
13
|
import type { IEmailReceivedEvent, IRcptToRequestEvent, IAuthRequestEvent, IScramCredentialRequestEvent } from '../../security/classes.rustsecuritybridge.js';
|
|
14
14
|
import { EmailRouter } from './classes.email.router.js';
|
|
@@ -25,11 +25,12 @@ import { SmtpState } from '../delivery/interfaces.js';
|
|
|
25
25
|
import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
|
|
26
26
|
import { EmailActionExecutor } from './classes.email.action.executor.js';
|
|
27
27
|
import { DkimManager } from './classes.dkim.manager.js';
|
|
28
|
+
import { callObserverSafely, emitSafely } from '../delivery/functions.safe-observers.js';
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
/** External DcRouter interface shape used by UnifiedEmailServer */
|
|
31
32
|
interface DcRouter {
|
|
32
|
-
storageManager:
|
|
33
|
+
storageManager: IStorageManager;
|
|
33
34
|
dnsServer?: any;
|
|
34
35
|
options?: any;
|
|
35
36
|
}
|
|
@@ -105,6 +106,8 @@ export interface IUnifiedEmailServerOptions {
|
|
|
105
106
|
domains: IEmailDomainConfig[]; // Domain configurations
|
|
106
107
|
banner?: string;
|
|
107
108
|
debug?: boolean;
|
|
109
|
+
/** Automatic for standalone use; caller-managed keeps DNS/key ownership with the embedding app. */
|
|
110
|
+
dkimKeyProvisioning?: 'automatic' | 'caller-managed';
|
|
108
111
|
useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
|
|
109
112
|
/** Persist router changes back into storage when a storage manager is available. */
|
|
110
113
|
persistRoutes?: boolean;
|
|
@@ -118,9 +121,9 @@ export interface IUnifiedEmailServerOptions {
|
|
|
118
121
|
|
|
119
122
|
// TLS options
|
|
120
123
|
tls?: {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
certPem?: string;
|
|
125
|
+
keyPem?: string;
|
|
126
|
+
caPem?: string;
|
|
124
127
|
minVersion?: string;
|
|
125
128
|
ciphers?: string;
|
|
126
129
|
};
|
|
@@ -146,6 +149,8 @@ export interface IUnifiedEmailServerOptions {
|
|
|
146
149
|
recipientValidationTimeoutMs?: number;
|
|
147
150
|
/** Timeout for message acceptance callbacks. Defaults to 30000 ms. */
|
|
148
151
|
messageAcceptanceTimeoutMs?: number;
|
|
152
|
+
/** Maximum complete messages concurrently held across Rust and TypeScript. */
|
|
153
|
+
maxConcurrentMessages?: number;
|
|
149
154
|
/** Require trusted PROXY v1 from the backend proxy before SMTP greeting. */
|
|
150
155
|
proxyProtocol?: {
|
|
151
156
|
required?: boolean;
|
|
@@ -179,8 +184,8 @@ export interface IUnifiedEmailServerOptions {
|
|
|
179
184
|
connectionProxyProvider?: (context: IOutboundConnectionProxyContext) => Promise<IOutboundConnectionProxy | null | undefined>;
|
|
180
185
|
};
|
|
181
186
|
|
|
182
|
-
//
|
|
183
|
-
queue
|
|
187
|
+
// Queue persistence must be selected explicitly.
|
|
188
|
+
queue: IQueueOptions;
|
|
184
189
|
|
|
185
190
|
// Rate limiting (global limits, can be overridden per domain)
|
|
186
191
|
rateLimits?: IHierarchicalRateLimits;
|
|
@@ -283,7 +288,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
283
288
|
this.rustBridge = RustSecurityBridge.getInstance();
|
|
284
289
|
|
|
285
290
|
// Initialize DKIM creator with storage manager
|
|
286
|
-
this.dkimCreator = new DKIMCreator(
|
|
291
|
+
this.dkimCreator = new DKIMCreator(dcRouter.storageManager);
|
|
287
292
|
|
|
288
293
|
// Initialize bounce manager with storage manager
|
|
289
294
|
this.bounceManager = new BounceManager({
|
|
@@ -298,7 +303,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
298
303
|
// Initialize email router with routes and storage manager
|
|
299
304
|
this.emailRouter = new EmailRouter(options.routes || [], {
|
|
300
305
|
storageManager: dcRouter.storageManager,
|
|
301
|
-
persistChanges: options.persistRoutes ??
|
|
306
|
+
persistChanges: options.persistRoutes ?? true
|
|
302
307
|
});
|
|
303
308
|
|
|
304
309
|
// Initialize rate limiter
|
|
@@ -315,7 +320,6 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
315
320
|
|
|
316
321
|
// Initialize delivery components
|
|
317
322
|
const queueOptions: IQueueOptions = {
|
|
318
|
-
storageType: 'memory', // Default to memory storage
|
|
319
323
|
maxRetries: 3,
|
|
320
324
|
baseRetryDelay: 300000, // 5 minutes
|
|
321
325
|
maxRetryDelay: 3600000, // 1 hour
|
|
@@ -329,7 +333,16 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
329
333
|
concurrentDeliveries: 10,
|
|
330
334
|
processBounces: true,
|
|
331
335
|
bounceHandler: {
|
|
332
|
-
processSmtpFailure:
|
|
336
|
+
processSmtpFailure: async (recipient, smtpResponse, failureOptions) => {
|
|
337
|
+
const processed = await this.processSmtpFailure(
|
|
338
|
+
recipient,
|
|
339
|
+
smtpResponse,
|
|
340
|
+
failureOptions,
|
|
341
|
+
);
|
|
342
|
+
if (!processed) {
|
|
343
|
+
throw new Error(`SMTP bounce operation failed for ${recipient}`);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
333
346
|
},
|
|
334
347
|
onDeliverySuccess: async (_item, _result) => {
|
|
335
348
|
// Delivery success recorded via delivery system
|
|
@@ -346,8 +359,14 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
346
359
|
});
|
|
347
360
|
|
|
348
361
|
// Initialize DKIM manager
|
|
349
|
-
this.dkimManager = new DkimManager(
|
|
350
|
-
|
|
362
|
+
this.dkimManager = new DkimManager(
|
|
363
|
+
this.dkimCreator,
|
|
364
|
+
this.domainRegistry,
|
|
365
|
+
dcRouter,
|
|
366
|
+
this.rustBridge,
|
|
367
|
+
options.dkimKeyProvisioning || 'automatic',
|
|
368
|
+
);
|
|
369
|
+
this.dnsManager = new DnsManager(this.dcRouter, undefined, options.dkimKeyProvisioning || 'automatic');
|
|
351
370
|
|
|
352
371
|
// Initialize statistics
|
|
353
372
|
this.stats = {
|
|
@@ -406,18 +425,24 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
406
425
|
tlsOpportunistic?: boolean;
|
|
407
426
|
}): Promise<ISmtpSendResult> {
|
|
408
427
|
// Build DKIM config if domain has keys
|
|
409
|
-
let dkim: {
|
|
428
|
+
let dkim: {
|
|
429
|
+
domain: string;
|
|
430
|
+
selector: string;
|
|
431
|
+
privateKey: string;
|
|
432
|
+
keyType: 'rsa' | 'ed25519';
|
|
433
|
+
} | undefined;
|
|
410
434
|
if (options?.dkimDomain) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
435
|
+
const selector = options.dkimSelector || 'default';
|
|
436
|
+
const material = await this.dkimCreator.readValidatedDKIMKeysForSelector(
|
|
437
|
+
options.dkimDomain,
|
|
438
|
+
selector,
|
|
439
|
+
);
|
|
440
|
+
dkim = {
|
|
441
|
+
domain: options.dkimDomain,
|
|
442
|
+
selector,
|
|
443
|
+
privateKey: material.privateKey,
|
|
444
|
+
keyType: material.keyType,
|
|
445
|
+
};
|
|
421
446
|
}
|
|
422
447
|
|
|
423
448
|
// Serialize the Email to the outbound format
|
|
@@ -476,13 +501,14 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
476
501
|
await this.startDeliveryPipeline();
|
|
477
502
|
await this.startRustBridge();
|
|
478
503
|
await this.initializeDkimAndDns();
|
|
504
|
+
await this.startDeliveryProcessing();
|
|
479
505
|
this.registerBridgeEventHandlers();
|
|
480
506
|
await this.startSmtpServer();
|
|
481
507
|
this.startDnsValidation();
|
|
482
508
|
logger.log('info', 'UnifiedEmailServer started successfully');
|
|
483
|
-
this
|
|
509
|
+
await emitSafely(this, 'started');
|
|
484
510
|
} catch (error) {
|
|
485
|
-
logger.log('error', `Failed to start UnifiedEmailServer: ${error
|
|
511
|
+
logger.log('error', `Failed to start UnifiedEmailServer: ${getErrorMessage(error)}`);
|
|
486
512
|
try {
|
|
487
513
|
await this.stop();
|
|
488
514
|
} catch (cleanupError) {
|
|
@@ -493,10 +519,19 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
493
519
|
}
|
|
494
520
|
|
|
495
521
|
private async startDeliveryPipeline(): Promise<void> {
|
|
496
|
-
await
|
|
522
|
+
await Promise.all([
|
|
523
|
+
this.bounceManager.initialize(),
|
|
524
|
+
this.emailRouter.initialize(),
|
|
525
|
+
]);
|
|
526
|
+
logger.log('info', 'Managed email routing and suppression state initialized');
|
|
527
|
+
|
|
528
|
+
await this.deliveryQueue.initialize({ startProcessing: false });
|
|
497
529
|
logger.log('info', 'Email delivery queue initialized');
|
|
530
|
+
}
|
|
498
531
|
|
|
532
|
+
private async startDeliveryProcessing(): Promise<void> {
|
|
499
533
|
await this.deliverySystem.start();
|
|
534
|
+
this.deliveryQueue.resume();
|
|
500
535
|
logger.log('info', 'Email delivery system started');
|
|
501
536
|
}
|
|
502
537
|
|
|
@@ -509,9 +544,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
509
544
|
|
|
510
545
|
this.unregisterBridgeStateHandler();
|
|
511
546
|
this.bridgeStateChangeHandler = ({ oldState, newState }: { oldState: string; newState: string }) => {
|
|
512
|
-
if (newState === 'failed') this
|
|
513
|
-
else if (newState === 'restarting') this
|
|
514
|
-
else if (newState === 'running' && oldState === 'restarting') this
|
|
547
|
+
if (newState === 'failed') void emitSafely(this, 'bridgeFailed');
|
|
548
|
+
else if (newState === 'restarting') void emitSafely(this, 'bridgeRestarting');
|
|
549
|
+
else if (newState === 'running' && oldState === 'restarting') void emitSafely(this, 'bridgeRecovered');
|
|
515
550
|
};
|
|
516
551
|
this.rustBridge.on('stateChange', this.bridgeStateChangeHandler);
|
|
517
552
|
}
|
|
@@ -520,6 +555,15 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
520
555
|
await this.dkimManager.setupDkimForDomains();
|
|
521
556
|
logger.log('info', 'DKIM configuration completed for all domains');
|
|
522
557
|
|
|
558
|
+
if (this.options.dkimKeyProvisioning === 'caller-managed') {
|
|
559
|
+
// Caller-managed mode validates only exact-selector key material. SmartMTA
|
|
560
|
+
// must not register DNS handlers, generate DNS records, rotate keys, or
|
|
561
|
+
// mutate any /email/dns/* or /email/dkim/* managed state.
|
|
562
|
+
this.applyDomainRateLimits();
|
|
563
|
+
logger.log('info', 'Caller-managed DKIM readiness validated without provisioning');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
523
567
|
if (!this.localDnsProvisioned) {
|
|
524
568
|
await this.dnsManager.provisionLocalDnsRecords(
|
|
525
569
|
this.domainRegistry.getAllConfigs(),
|
|
@@ -668,20 +712,11 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
668
712
|
}
|
|
669
713
|
|
|
670
714
|
private async startSmtpServer(): Promise<void> {
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (hasTlsConfig) {
|
|
676
|
-
try {
|
|
677
|
-
tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
|
|
678
|
-
tlsCertPem = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
|
|
679
|
-
logger.log('info', 'TLS certificates loaded successfully');
|
|
680
|
-
} catch (error) {
|
|
681
|
-
logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
|
|
682
|
-
}
|
|
715
|
+
const tlsCertPem = this.options.tls?.certPem;
|
|
716
|
+
const tlsKeyPem = this.options.tls?.keyPem;
|
|
717
|
+
if ((tlsCertPem && !tlsKeyPem) || (!tlsCertPem && tlsKeyPem)) {
|
|
718
|
+
throw new Error('SMTP TLS requires both certPem and keyPem');
|
|
683
719
|
}
|
|
684
|
-
|
|
685
720
|
const configuredSecurePort = this.options.smtp?.securePort;
|
|
686
721
|
if (configuredSecurePort !== undefined && !(this.options.ports as number[]).includes(configuredSecurePort)) {
|
|
687
722
|
throw new Error(`Configured SMTP securePort ${configuredSecurePort} is not included in ports`);
|
|
@@ -708,6 +743,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
708
743
|
processingTimeoutSecs: this.options.hooks?.onMessageData
|
|
709
744
|
? Math.ceil(this.getMessageAcceptanceTimeoutMs() / 1000) + 1
|
|
710
745
|
: 30,
|
|
746
|
+
maxConcurrentMessages: this.options.smtp?.maxConcurrentMessages ?? 4,
|
|
711
747
|
recipientValidationEnabled: this.isRecipientValidationEnabled(),
|
|
712
748
|
recipientValidationTimeoutSecs: this.options.smtp?.recipientValidationTimeoutMs
|
|
713
749
|
? Math.ceil(this.options.smtp.recipientValidationTimeoutMs / 1000)
|
|
@@ -775,9 +811,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
775
811
|
}
|
|
776
812
|
|
|
777
813
|
logger.log('info', 'UnifiedEmailServer stopped successfully');
|
|
778
|
-
this
|
|
814
|
+
await emitSafely(this, 'stopped');
|
|
779
815
|
} catch (error) {
|
|
780
|
-
logger.log('error', `Error stopping UnifiedEmailServer: ${error
|
|
816
|
+
logger.log('error', `Error stopping UnifiedEmailServer: ${getErrorMessage(error)}`);
|
|
781
817
|
throw error;
|
|
782
818
|
}
|
|
783
819
|
}
|
|
@@ -959,24 +995,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
959
995
|
logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`);
|
|
960
996
|
|
|
961
997
|
try {
|
|
962
|
-
|
|
963
|
-
let rawMessageBuffer: Buffer;
|
|
964
|
-
if (data.data.type === 'inline' && data.data.base64) {
|
|
965
|
-
rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
|
|
966
|
-
} else if (data.data.type === 'file' && data.data.path) {
|
|
967
|
-
try {
|
|
968
|
-
rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
|
|
969
|
-
} finally {
|
|
970
|
-
try {
|
|
971
|
-
plugins.fs.unlinkSync(data.data.path);
|
|
972
|
-
} catch {
|
|
973
|
-
// Ignore cleanup errors
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
} else {
|
|
977
|
-
throw new Error('Invalid email data transport');
|
|
978
|
-
}
|
|
979
|
-
|
|
998
|
+
const rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
|
|
980
999
|
// Build a session-like object for processEmailByMode
|
|
981
1000
|
const session: IExtendedSmtpSession = {
|
|
982
1001
|
id: data.sessionId || 'rust-' + Math.random().toString(36).substring(2),
|
|
@@ -1592,18 +1611,12 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1592
1611
|
}
|
|
1593
1612
|
}
|
|
1594
1613
|
|
|
1595
|
-
// Sign with DKIM if configured
|
|
1596
|
-
if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
|
|
1597
|
-
const domain = email.from.split('@')[1];
|
|
1598
|
-
await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'default');
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
1614
|
const id = await this.deliveryQueue.enqueue(email, mode, route);
|
|
1602
1615
|
|
|
1603
1616
|
logger.log('info', `Email queued with ID: ${id}`);
|
|
1604
1617
|
return id;
|
|
1605
1618
|
} catch (error) {
|
|
1606
|
-
logger.log('error', `Failed to send email: ${error
|
|
1619
|
+
logger.log('error', `Failed to send email: ${getErrorMessage(error)}`);
|
|
1607
1620
|
throw error;
|
|
1608
1621
|
}
|
|
1609
1622
|
}
|
|
@@ -1624,9 +1637,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1624
1637
|
bounceCategory: bounceRecord.bounceCategory
|
|
1625
1638
|
});
|
|
1626
1639
|
|
|
1627
|
-
this
|
|
1640
|
+
await emitSafely(this, 'bounceProcessed', bounceRecord);
|
|
1628
1641
|
|
|
1629
|
-
SecurityLogger.getInstance().logEvent({
|
|
1642
|
+
await callObserverSafely('bounce security log', () => SecurityLogger.getInstance().logEvent({
|
|
1630
1643
|
level: SecurityLogLevel.INFO,
|
|
1631
1644
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1632
1645
|
message: `Bounce notification processed for recipient`,
|
|
@@ -1637,7 +1650,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1637
1650
|
bounceCategory: bounceRecord.bounceCategory
|
|
1638
1651
|
},
|
|
1639
1652
|
success: true
|
|
1640
|
-
});
|
|
1653
|
+
}));
|
|
1641
1654
|
|
|
1642
1655
|
return true;
|
|
1643
1656
|
} else {
|
|
@@ -1645,15 +1658,16 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1645
1658
|
return false;
|
|
1646
1659
|
}
|
|
1647
1660
|
} catch (error) {
|
|
1648
|
-
|
|
1661
|
+
const errorMessage = getErrorMessage(error);
|
|
1662
|
+
logger.log('error', `Error processing bounce notification: ${errorMessage}`);
|
|
1649
1663
|
|
|
1650
|
-
SecurityLogger.getInstance().logEvent({
|
|
1664
|
+
await callObserverSafely('bounce failure security log', () => SecurityLogger.getInstance().logEvent({
|
|
1651
1665
|
level: SecurityLogLevel.ERROR,
|
|
1652
1666
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1653
1667
|
message: 'Failed to process bounce notification',
|
|
1654
|
-
details: { error:
|
|
1668
|
+
details: { error: errorMessage, subject: bounceEmail.subject },
|
|
1655
1669
|
success: false
|
|
1656
|
-
});
|
|
1670
|
+
}));
|
|
1657
1671
|
|
|
1658
1672
|
return false;
|
|
1659
1673
|
}
|
|
@@ -1662,7 +1676,13 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1662
1676
|
public async processSmtpFailure(
|
|
1663
1677
|
recipient: string,
|
|
1664
1678
|
smtpResponse: string,
|
|
1665
|
-
options: {
|
|
1679
|
+
options: {
|
|
1680
|
+
operationId?: string;
|
|
1681
|
+
sender?: string;
|
|
1682
|
+
originalEmailId?: string;
|
|
1683
|
+
statusCode?: string;
|
|
1684
|
+
headers?: Record<string, string>;
|
|
1685
|
+
} = {}
|
|
1666
1686
|
): Promise<boolean> {
|
|
1667
1687
|
logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`);
|
|
1668
1688
|
|
|
@@ -1673,9 +1693,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1673
1693
|
bounceType: bounceRecord.bounceType
|
|
1674
1694
|
});
|
|
1675
1695
|
|
|
1676
|
-
this
|
|
1696
|
+
await emitSafely(this, 'bounceProcessed', bounceRecord);
|
|
1677
1697
|
|
|
1678
|
-
SecurityLogger.getInstance().logEvent({
|
|
1698
|
+
await callObserverSafely('SMTP failure security log', () => SecurityLogger.getInstance().logEvent({
|
|
1679
1699
|
level: SecurityLogLevel.INFO,
|
|
1680
1700
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1681
1701
|
message: `SMTP failure processed for recipient`,
|
|
@@ -1687,19 +1707,20 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1687
1707
|
smtpResponse
|
|
1688
1708
|
},
|
|
1689
1709
|
success: true
|
|
1690
|
-
});
|
|
1710
|
+
}));
|
|
1691
1711
|
|
|
1692
1712
|
return true;
|
|
1693
1713
|
} catch (error) {
|
|
1694
|
-
|
|
1714
|
+
const errorMessage = getErrorMessage(error);
|
|
1715
|
+
logger.log('error', `Error processing SMTP failure: ${errorMessage}`);
|
|
1695
1716
|
|
|
1696
|
-
SecurityLogger.getInstance().logEvent({
|
|
1717
|
+
await callObserverSafely('SMTP failure security log', () => SecurityLogger.getInstance().logEvent({
|
|
1697
1718
|
level: SecurityLogLevel.ERROR,
|
|
1698
1719
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1699
1720
|
message: 'Failed to process SMTP failure',
|
|
1700
|
-
details: { recipient, smtpResponse, error:
|
|
1721
|
+
details: { recipient, smtpResponse, error: errorMessage },
|
|
1701
1722
|
success: false
|
|
1702
|
-
});
|
|
1723
|
+
}));
|
|
1703
1724
|
|
|
1704
1725
|
return false;
|
|
1705
1726
|
}
|
|
@@ -89,7 +89,6 @@ export interface IEmailAction {
|
|
|
89
89
|
mtaOptions?: {
|
|
90
90
|
domain?: string;
|
|
91
91
|
allowLocalDelivery?: boolean;
|
|
92
|
-
localDeliveryPath?: string;
|
|
93
92
|
dkimSign?: boolean;
|
|
94
93
|
dkimOptions?: {
|
|
95
94
|
domainName: string;
|
|
@@ -99,7 +98,6 @@ export interface IEmailAction {
|
|
|
99
98
|
smtpBanner?: string;
|
|
100
99
|
maxConnections?: number;
|
|
101
100
|
connTimeout?: number;
|
|
102
|
-
spoolDir?: string;
|
|
103
101
|
};
|
|
104
102
|
/** Content scanning configuration */
|
|
105
103
|
contentScanning?: boolean;
|