@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,964 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as paths from '../paths.js';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// IPC command type map — mirrors the methods in mailer-bin's management mode
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
interface IDkimVerificationResult {
|
|
11
|
+
is_valid: boolean;
|
|
12
|
+
domain: string | null;
|
|
13
|
+
selector: string | null;
|
|
14
|
+
status: string;
|
|
15
|
+
details: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ISpfResult {
|
|
19
|
+
result: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
ip: string;
|
|
22
|
+
explanation: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface IDmarcResult {
|
|
26
|
+
passed: boolean;
|
|
27
|
+
policy: string;
|
|
28
|
+
domain: string;
|
|
29
|
+
dkim_result: string;
|
|
30
|
+
spf_result: string;
|
|
31
|
+
action: string;
|
|
32
|
+
details: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface IEmailSecurityResult {
|
|
36
|
+
dkim: IDkimVerificationResult[];
|
|
37
|
+
spf: ISpfResult | null;
|
|
38
|
+
dmarc: IDmarcResult | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface IValidationResult {
|
|
42
|
+
valid: boolean;
|
|
43
|
+
formatValid: boolean;
|
|
44
|
+
score: number;
|
|
45
|
+
error: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface IBounceDetection {
|
|
49
|
+
bounce_type: string;
|
|
50
|
+
category: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface IReputationResult {
|
|
54
|
+
ip: string;
|
|
55
|
+
score: number;
|
|
56
|
+
risk_level: string;
|
|
57
|
+
ip_type: string;
|
|
58
|
+
dnsbl_results: Array<{ server: string; listed: boolean; response: string | null }>;
|
|
59
|
+
listed_count: number;
|
|
60
|
+
total_checked: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface IContentScanResult {
|
|
64
|
+
threatScore: number;
|
|
65
|
+
threatType: string | null;
|
|
66
|
+
threatDetails: string | null;
|
|
67
|
+
scannedElements: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- SMTP Client types ---
|
|
71
|
+
|
|
72
|
+
interface IOutboundEmail {
|
|
73
|
+
from: string;
|
|
74
|
+
to: string[];
|
|
75
|
+
cc?: string[];
|
|
76
|
+
bcc?: string[];
|
|
77
|
+
subject?: string;
|
|
78
|
+
text?: string;
|
|
79
|
+
html?: string;
|
|
80
|
+
headers?: Record<string, string>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ISmtpSendResult {
|
|
84
|
+
accepted: string[];
|
|
85
|
+
rejected: string[];
|
|
86
|
+
messageId?: string;
|
|
87
|
+
response: string;
|
|
88
|
+
envelope: { from: string; to: string[] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ISmtpSendOptions {
|
|
92
|
+
host: string;
|
|
93
|
+
port: number;
|
|
94
|
+
secure?: boolean;
|
|
95
|
+
domain?: string;
|
|
96
|
+
auth?: { user: string; pass: string; method?: string };
|
|
97
|
+
email: IOutboundEmail;
|
|
98
|
+
dkim?: { domain: string; selector: string; privateKey: string; keyType?: string };
|
|
99
|
+
connectionTimeoutSecs?: number;
|
|
100
|
+
socketTimeoutSecs?: number;
|
|
101
|
+
poolKey?: string;
|
|
102
|
+
maxPoolConnections?: number;
|
|
103
|
+
tlsOpportunistic?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ISmtpSendRawOptions {
|
|
107
|
+
host: string;
|
|
108
|
+
port: number;
|
|
109
|
+
secure?: boolean;
|
|
110
|
+
domain?: string;
|
|
111
|
+
auth?: { user: string; pass: string; method?: string };
|
|
112
|
+
envelopeFrom: string;
|
|
113
|
+
envelopeTo: string[];
|
|
114
|
+
rawMessageBase64: string;
|
|
115
|
+
poolKey?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface ISmtpVerifyOptions {
|
|
119
|
+
host: string;
|
|
120
|
+
port: number;
|
|
121
|
+
secure?: boolean;
|
|
122
|
+
domain?: string;
|
|
123
|
+
auth?: { user: string; pass: string; method?: string };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface ISmtpVerifyResult {
|
|
127
|
+
reachable: boolean;
|
|
128
|
+
greeting?: string;
|
|
129
|
+
capabilities?: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface ISmtpPoolStatus {
|
|
133
|
+
pools: Record<string, { total: number; active: number; idle: number }>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface IVersionInfo {
|
|
137
|
+
bin: string;
|
|
138
|
+
core: string;
|
|
139
|
+
security: string;
|
|
140
|
+
smtp: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- SMTP Server types ---
|
|
144
|
+
|
|
145
|
+
interface ISmtpServerConfig {
|
|
146
|
+
hostname: string;
|
|
147
|
+
ports: number[];
|
|
148
|
+
securePort?: number;
|
|
149
|
+
tlsCertPem?: string;
|
|
150
|
+
tlsKeyPem?: string;
|
|
151
|
+
additionalTlsCerts?: Array<{ domains: string[]; certPem: string; keyPem: string }>;
|
|
152
|
+
maxMessageSize?: number;
|
|
153
|
+
maxConnections?: number;
|
|
154
|
+
maxRecipients?: number;
|
|
155
|
+
connectionTimeoutSecs?: number;
|
|
156
|
+
dataTimeoutSecs?: number;
|
|
157
|
+
authEnabled?: boolean;
|
|
158
|
+
maxAuthFailures?: number;
|
|
159
|
+
socketTimeoutSecs?: number;
|
|
160
|
+
processingTimeoutSecs?: number;
|
|
161
|
+
rateLimits?: IRateLimitConfig;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface IRateLimitConfig {
|
|
165
|
+
maxConnectionsPerIp?: number;
|
|
166
|
+
maxMessagesPerSender?: number;
|
|
167
|
+
maxAuthFailuresPerIp?: number;
|
|
168
|
+
windowSecs?: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface IEmailData {
|
|
172
|
+
type: 'inline' | 'file';
|
|
173
|
+
base64?: string;
|
|
174
|
+
path?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface IEmailReceivedEvent {
|
|
178
|
+
correlationId: string;
|
|
179
|
+
sessionId: string;
|
|
180
|
+
mailFrom: string;
|
|
181
|
+
rcptTo: string[];
|
|
182
|
+
data: IEmailData;
|
|
183
|
+
remoteAddr: string;
|
|
184
|
+
clientHostname: string | null;
|
|
185
|
+
secure: boolean;
|
|
186
|
+
authenticatedUser: string | null;
|
|
187
|
+
securityResults: any | null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface IAuthRequestEvent {
|
|
191
|
+
correlationId: string;
|
|
192
|
+
sessionId: string;
|
|
193
|
+
username: string;
|
|
194
|
+
password: string;
|
|
195
|
+
remoteAddr: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface IScramCredentialRequestEvent {
|
|
199
|
+
correlationId: string;
|
|
200
|
+
sessionId: string;
|
|
201
|
+
username: string;
|
|
202
|
+
remoteAddr: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Type-safe command map for the mailer-bin IPC bridge.
|
|
207
|
+
*/
|
|
208
|
+
type TMailerCommands = {
|
|
209
|
+
ping: {
|
|
210
|
+
params: Record<string, never>;
|
|
211
|
+
result: { pong: boolean };
|
|
212
|
+
};
|
|
213
|
+
version: {
|
|
214
|
+
params: Record<string, never>;
|
|
215
|
+
result: IVersionInfo;
|
|
216
|
+
};
|
|
217
|
+
validateEmail: {
|
|
218
|
+
params: { email: string };
|
|
219
|
+
result: IValidationResult;
|
|
220
|
+
};
|
|
221
|
+
detectBounce: {
|
|
222
|
+
params: { smtpResponse?: string; diagnosticCode?: string; statusCode?: string };
|
|
223
|
+
result: IBounceDetection;
|
|
224
|
+
};
|
|
225
|
+
checkIpReputation: {
|
|
226
|
+
params: { ip: string };
|
|
227
|
+
result: IReputationResult;
|
|
228
|
+
};
|
|
229
|
+
verifyDkim: {
|
|
230
|
+
params: { rawMessage: string };
|
|
231
|
+
result: IDkimVerificationResult[];
|
|
232
|
+
};
|
|
233
|
+
signDkim: {
|
|
234
|
+
params: { rawMessage: string; domain: string; selector?: string; privateKey: string; keyType?: string };
|
|
235
|
+
result: { header: string; signedMessage: string };
|
|
236
|
+
};
|
|
237
|
+
checkSpf: {
|
|
238
|
+
params: { ip: string; heloDomain: string; hostname?: string; mailFrom: string };
|
|
239
|
+
result: ISpfResult;
|
|
240
|
+
};
|
|
241
|
+
scanContent: {
|
|
242
|
+
params: {
|
|
243
|
+
subject?: string;
|
|
244
|
+
textBody?: string;
|
|
245
|
+
htmlBody?: string;
|
|
246
|
+
attachmentNames?: string[];
|
|
247
|
+
};
|
|
248
|
+
result: IContentScanResult;
|
|
249
|
+
};
|
|
250
|
+
verifyEmail: {
|
|
251
|
+
params: {
|
|
252
|
+
rawMessage: string;
|
|
253
|
+
ip: string;
|
|
254
|
+
heloDomain: string;
|
|
255
|
+
hostname?: string;
|
|
256
|
+
mailFrom: string;
|
|
257
|
+
};
|
|
258
|
+
result: IEmailSecurityResult;
|
|
259
|
+
};
|
|
260
|
+
startSmtpServer: {
|
|
261
|
+
params: ISmtpServerConfig;
|
|
262
|
+
result: { started: boolean };
|
|
263
|
+
};
|
|
264
|
+
stopSmtpServer: {
|
|
265
|
+
params: Record<string, never>;
|
|
266
|
+
result: { stopped: boolean; wasRunning?: boolean };
|
|
267
|
+
};
|
|
268
|
+
emailProcessingResult: {
|
|
269
|
+
params: {
|
|
270
|
+
correlationId: string;
|
|
271
|
+
accepted: boolean;
|
|
272
|
+
smtpCode?: number;
|
|
273
|
+
smtpMessage?: string;
|
|
274
|
+
};
|
|
275
|
+
result: { resolved: boolean };
|
|
276
|
+
};
|
|
277
|
+
authResult: {
|
|
278
|
+
params: {
|
|
279
|
+
correlationId: string;
|
|
280
|
+
success: boolean;
|
|
281
|
+
message?: string;
|
|
282
|
+
};
|
|
283
|
+
result: { resolved: boolean };
|
|
284
|
+
};
|
|
285
|
+
scramCredentialResult: {
|
|
286
|
+
params: {
|
|
287
|
+
correlationId: string;
|
|
288
|
+
found: boolean;
|
|
289
|
+
salt?: string;
|
|
290
|
+
iterations?: number;
|
|
291
|
+
storedKey?: string;
|
|
292
|
+
serverKey?: string;
|
|
293
|
+
};
|
|
294
|
+
result: { resolved: boolean };
|
|
295
|
+
};
|
|
296
|
+
configureRateLimits: {
|
|
297
|
+
params: IRateLimitConfig;
|
|
298
|
+
result: { configured: boolean };
|
|
299
|
+
};
|
|
300
|
+
sendEmail: {
|
|
301
|
+
params: ISmtpSendOptions;
|
|
302
|
+
result: ISmtpSendResult;
|
|
303
|
+
};
|
|
304
|
+
sendRawEmail: {
|
|
305
|
+
params: ISmtpSendRawOptions;
|
|
306
|
+
result: ISmtpSendResult;
|
|
307
|
+
};
|
|
308
|
+
verifySmtpConnection: {
|
|
309
|
+
params: ISmtpVerifyOptions;
|
|
310
|
+
result: ISmtpVerifyResult;
|
|
311
|
+
};
|
|
312
|
+
closeSmtpPool: {
|
|
313
|
+
params: { poolKey?: string };
|
|
314
|
+
result: { closed: boolean };
|
|
315
|
+
};
|
|
316
|
+
getSmtpPoolStatus: {
|
|
317
|
+
params: Record<string, never>;
|
|
318
|
+
result: ISmtpPoolStatus;
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Bridge state machine
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
export enum BridgeState {
|
|
327
|
+
Idle = 'idle',
|
|
328
|
+
Starting = 'starting',
|
|
329
|
+
Running = 'running',
|
|
330
|
+
Restarting = 'restarting',
|
|
331
|
+
Failed = 'failed',
|
|
332
|
+
Stopped = 'stopped',
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface IBridgeResilienceConfig {
|
|
336
|
+
maxRestartAttempts: number;
|
|
337
|
+
healthCheckIntervalMs: number;
|
|
338
|
+
restartBackoffBaseMs: number;
|
|
339
|
+
restartBackoffMaxMs: number;
|
|
340
|
+
healthCheckTimeoutMs: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const DEFAULT_RESILIENCE_CONFIG: IBridgeResilienceConfig = {
|
|
344
|
+
maxRestartAttempts: 5,
|
|
345
|
+
healthCheckIntervalMs: 30_000,
|
|
346
|
+
restartBackoffBaseMs: 1_000,
|
|
347
|
+
restartBackoffMaxMs: 30_000,
|
|
348
|
+
healthCheckTimeoutMs: 5_000,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// RustSecurityBridge — singleton wrapper around smartrust.RustBridge
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Bridge between TypeScript and the Rust `mailer-bin` binary.
|
|
357
|
+
*
|
|
358
|
+
* Uses `@push.rocks/smartrust` for JSON-over-stdin/stdout IPC.
|
|
359
|
+
* Singleton — access via `RustSecurityBridge.getInstance()`.
|
|
360
|
+
*
|
|
361
|
+
* Features resilience via auto-restart with exponential backoff,
|
|
362
|
+
* periodic health checks, and a state machine that tracks the
|
|
363
|
+
* bridge lifecycle.
|
|
364
|
+
*/
|
|
365
|
+
export class RustSecurityBridge extends EventEmitter {
|
|
366
|
+
private static instance: RustSecurityBridge | null = null;
|
|
367
|
+
private static _resilienceConfig: IBridgeResilienceConfig = { ...DEFAULT_RESILIENCE_CONFIG };
|
|
368
|
+
|
|
369
|
+
private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TMailerCommands>>;
|
|
370
|
+
private _running = false;
|
|
371
|
+
private _state: BridgeState = BridgeState.Idle;
|
|
372
|
+
private _restartAttempts = 0;
|
|
373
|
+
private _restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
374
|
+
private _healthCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
375
|
+
private _deliberateStop = false;
|
|
376
|
+
private _smtpServerConfig: ISmtpServerConfig | null = null;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Map Node.js process.platform / process.arch to the tsrust-style suffix
|
|
380
|
+
* used for cross-compiled binaries, e.g. mailer-bin_linux_amd64.
|
|
381
|
+
*/
|
|
382
|
+
private static getPlatformSuffix(): string | null {
|
|
383
|
+
const archMap: Record<string, string> = { x64: 'amd64', arm64: 'arm64' };
|
|
384
|
+
const os = process.platform; // 'linux', 'darwin', 'win32', …
|
|
385
|
+
const arch = archMap[process.arch];
|
|
386
|
+
if (!arch) return null;
|
|
387
|
+
return `${os}_${arch}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private constructor() {
|
|
391
|
+
super();
|
|
392
|
+
|
|
393
|
+
const suffix = RustSecurityBridge.getPlatformSuffix();
|
|
394
|
+
const localPaths: string[] = [];
|
|
395
|
+
|
|
396
|
+
// dist_rust/ candidates (tsrust cross-compiled output)
|
|
397
|
+
if (suffix) {
|
|
398
|
+
localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', `mailer-bin_${suffix}`));
|
|
399
|
+
}
|
|
400
|
+
localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', 'mailer-bin'));
|
|
401
|
+
// Local dev build paths
|
|
402
|
+
localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'release', 'mailer-bin'));
|
|
403
|
+
localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'debug', 'mailer-bin'));
|
|
404
|
+
|
|
405
|
+
this.bridge = new plugins.smartrust.RustBridge<TMailerCommands>({
|
|
406
|
+
binaryName: 'mailer-bin',
|
|
407
|
+
cliArgs: ['--management'],
|
|
408
|
+
requestTimeoutMs: 30_000,
|
|
409
|
+
readyTimeoutMs: 10_000,
|
|
410
|
+
localPaths,
|
|
411
|
+
searchSystemPath: false,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Forward lifecycle events
|
|
415
|
+
this.bridge.on('ready', () => {
|
|
416
|
+
this._running = true;
|
|
417
|
+
logger.log('info', 'Rust security bridge is ready');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
this.bridge.on('exit', (code: number | null, signal: string | null) => {
|
|
421
|
+
this._running = false;
|
|
422
|
+
logger.log('warn', `Rust security bridge exited (code=${code}, signal=${signal})`);
|
|
423
|
+
|
|
424
|
+
if (this._deliberateStop) {
|
|
425
|
+
this.setState(BridgeState.Stopped);
|
|
426
|
+
} else if (this._state === BridgeState.Running) {
|
|
427
|
+
// Unexpected exit — attempt restart
|
|
428
|
+
this.attemptRestart();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
this.bridge.on('stderr', (line: string) => {
|
|
433
|
+
logger.log('debug', `[rust-bridge] ${line}`);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// -----------------------------------------------------------------------
|
|
438
|
+
// Static configuration & singleton
|
|
439
|
+
// -----------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
/** Get or create the singleton instance. */
|
|
442
|
+
public static getInstance(): RustSecurityBridge {
|
|
443
|
+
if (!RustSecurityBridge.instance) {
|
|
444
|
+
RustSecurityBridge.instance = new RustSecurityBridge();
|
|
445
|
+
}
|
|
446
|
+
return RustSecurityBridge.instance;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Reset the singleton instance (for testing). */
|
|
450
|
+
public static resetInstance(): void {
|
|
451
|
+
if (RustSecurityBridge.instance) {
|
|
452
|
+
RustSecurityBridge.instance.stopHealthCheck();
|
|
453
|
+
if (RustSecurityBridge.instance._restartTimer) {
|
|
454
|
+
clearTimeout(RustSecurityBridge.instance._restartTimer);
|
|
455
|
+
RustSecurityBridge.instance._restartTimer = null;
|
|
456
|
+
}
|
|
457
|
+
RustSecurityBridge.instance.removeAllListeners();
|
|
458
|
+
}
|
|
459
|
+
RustSecurityBridge.instance = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Configure resilience parameters. Can be called before or after getInstance(). */
|
|
463
|
+
public static configure(config: Partial<IBridgeResilienceConfig>): void {
|
|
464
|
+
RustSecurityBridge._resilienceConfig = {
|
|
465
|
+
...RustSecurityBridge._resilienceConfig,
|
|
466
|
+
...config,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// -----------------------------------------------------------------------
|
|
471
|
+
// State management
|
|
472
|
+
// -----------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
/** Current bridge state. */
|
|
475
|
+
public get state(): BridgeState {
|
|
476
|
+
return this._state;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Whether the Rust process is currently running and accepting commands. */
|
|
480
|
+
public get running(): boolean {
|
|
481
|
+
return this._running;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private setState(newState: BridgeState): void {
|
|
485
|
+
const oldState = this._state;
|
|
486
|
+
if (oldState === newState) return;
|
|
487
|
+
this._state = newState;
|
|
488
|
+
logger.log('info', `Rust bridge state: ${oldState} -> ${newState}`);
|
|
489
|
+
this.emit('stateChange', { oldState, newState });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Throws a descriptive error if the bridge is not in Running state.
|
|
494
|
+
* Called at the top of every command method.
|
|
495
|
+
*/
|
|
496
|
+
private ensureRunning(): void {
|
|
497
|
+
if (this._state === BridgeState.Running && this._running) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
switch (this._state) {
|
|
501
|
+
case BridgeState.Idle:
|
|
502
|
+
throw new Error('Rust bridge has not been started yet. Call start() first.');
|
|
503
|
+
case BridgeState.Starting:
|
|
504
|
+
throw new Error('Rust bridge is still starting. Wait for start() to resolve.');
|
|
505
|
+
case BridgeState.Restarting:
|
|
506
|
+
throw new Error('Rust bridge is restarting after a crash. Commands will resume once it recovers.');
|
|
507
|
+
case BridgeState.Failed:
|
|
508
|
+
throw new Error('Rust bridge has failed after exhausting all restart attempts.');
|
|
509
|
+
case BridgeState.Stopped:
|
|
510
|
+
throw new Error('Rust bridge has been stopped. Call start() to restart it.');
|
|
511
|
+
default:
|
|
512
|
+
throw new Error(`Rust bridge is not running (state=${this._state}).`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// -----------------------------------------------------------------------
|
|
517
|
+
// Lifecycle
|
|
518
|
+
// -----------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Spawn the Rust binary and wait for the ready signal.
|
|
522
|
+
* @returns `true` if the binary started successfully, `false` otherwise.
|
|
523
|
+
*/
|
|
524
|
+
public async start(): Promise<boolean> {
|
|
525
|
+
if (this._running && this._state === BridgeState.Running) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
this._deliberateStop = false;
|
|
530
|
+
this._restartAttempts = 0;
|
|
531
|
+
this.setState(BridgeState.Starting);
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
const ok = await this.bridge.spawn();
|
|
535
|
+
this._running = ok;
|
|
536
|
+
if (ok) {
|
|
537
|
+
this.setState(BridgeState.Running);
|
|
538
|
+
this.startHealthCheck();
|
|
539
|
+
logger.log('info', 'Rust security bridge started');
|
|
540
|
+
} else {
|
|
541
|
+
this.setState(BridgeState.Failed);
|
|
542
|
+
logger.log('warn', 'Rust security bridge failed to start (binary not found or timeout)');
|
|
543
|
+
}
|
|
544
|
+
return ok;
|
|
545
|
+
} catch (err) {
|
|
546
|
+
this.setState(BridgeState.Failed);
|
|
547
|
+
logger.log('error', `Failed to start Rust security bridge: ${(err as Error).message}`);
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Kill the Rust process deliberately. */
|
|
553
|
+
public async stop(): Promise<void> {
|
|
554
|
+
this._deliberateStop = true;
|
|
555
|
+
|
|
556
|
+
// Cancel any pending restart
|
|
557
|
+
if (this._restartTimer) {
|
|
558
|
+
clearTimeout(this._restartTimer);
|
|
559
|
+
this._restartTimer = null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this.stopHealthCheck();
|
|
563
|
+
this._smtpServerConfig = null;
|
|
564
|
+
|
|
565
|
+
if (!this._running) {
|
|
566
|
+
this.setState(BridgeState.Stopped);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
this.bridge.kill();
|
|
571
|
+
this._running = false;
|
|
572
|
+
this.setState(BridgeState.Stopped);
|
|
573
|
+
logger.log('info', 'Rust security bridge stopped');
|
|
574
|
+
} catch (err) {
|
|
575
|
+
logger.log('error', `Error stopping Rust security bridge: ${(err as Error).message}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// -----------------------------------------------------------------------
|
|
580
|
+
// Auto-restart with exponential backoff
|
|
581
|
+
// -----------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
private attemptRestart(): void {
|
|
584
|
+
const config = RustSecurityBridge._resilienceConfig;
|
|
585
|
+
this._restartAttempts++;
|
|
586
|
+
|
|
587
|
+
if (this._restartAttempts > config.maxRestartAttempts) {
|
|
588
|
+
logger.log('error', `Rust bridge exceeded max restart attempts (${config.maxRestartAttempts}). Giving up.`);
|
|
589
|
+
this.setState(BridgeState.Failed);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this.setState(BridgeState.Restarting);
|
|
594
|
+
this.stopHealthCheck();
|
|
595
|
+
|
|
596
|
+
const delay = Math.min(
|
|
597
|
+
config.restartBackoffBaseMs * Math.pow(2, this._restartAttempts - 1),
|
|
598
|
+
config.restartBackoffMaxMs,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
logger.log('info', `Rust bridge restart attempt ${this._restartAttempts}/${config.maxRestartAttempts} in ${delay}ms`);
|
|
602
|
+
|
|
603
|
+
this._restartTimer = setTimeout(async () => {
|
|
604
|
+
this._restartTimer = null;
|
|
605
|
+
|
|
606
|
+
// Guard: if stop() was called while we were waiting, don't restart
|
|
607
|
+
if (this._deliberateStop) {
|
|
608
|
+
this.setState(BridgeState.Stopped);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const ok = await this.bridge.spawn();
|
|
614
|
+
this._running = ok;
|
|
615
|
+
|
|
616
|
+
if (ok) {
|
|
617
|
+
logger.log('info', 'Rust bridge restarted successfully');
|
|
618
|
+
this._restartAttempts = 0;
|
|
619
|
+
this.setState(BridgeState.Running);
|
|
620
|
+
this.startHealthCheck();
|
|
621
|
+
await this.restoreAfterRestart();
|
|
622
|
+
} else {
|
|
623
|
+
logger.log('warn', 'Rust bridge restart failed (spawn returned false)');
|
|
624
|
+
this.attemptRestart();
|
|
625
|
+
}
|
|
626
|
+
} catch (err) {
|
|
627
|
+
logger.log('error', `Rust bridge restart failed: ${(err as Error).message}`);
|
|
628
|
+
this.attemptRestart();
|
|
629
|
+
}
|
|
630
|
+
}, delay);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Restore state after a successful restart:
|
|
635
|
+
* - Re-send startSmtpServer command if the SMTP server was running
|
|
636
|
+
*/
|
|
637
|
+
private async restoreAfterRestart(): Promise<void> {
|
|
638
|
+
if (this._smtpServerConfig) {
|
|
639
|
+
try {
|
|
640
|
+
logger.log('info', 'Restoring SMTP server after bridge restart');
|
|
641
|
+
const result = await this.bridge.sendCommand('startSmtpServer', this._smtpServerConfig);
|
|
642
|
+
if (result?.started) {
|
|
643
|
+
logger.log('info', 'SMTP server restored after bridge restart');
|
|
644
|
+
} else {
|
|
645
|
+
logger.log('warn', 'SMTP server failed to restore after bridge restart');
|
|
646
|
+
}
|
|
647
|
+
} catch (err) {
|
|
648
|
+
logger.log('error', `Failed to restore SMTP server after restart: ${(err as Error).message}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// -----------------------------------------------------------------------
|
|
654
|
+
// Health check
|
|
655
|
+
// -----------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
private startHealthCheck(): void {
|
|
658
|
+
this.stopHealthCheck();
|
|
659
|
+
const config = RustSecurityBridge._resilienceConfig;
|
|
660
|
+
|
|
661
|
+
this._healthCheckTimer = setInterval(async () => {
|
|
662
|
+
if (this._state !== BridgeState.Running || !this._running) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
const pongPromise = this.bridge.sendCommand('ping', {} as any);
|
|
668
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
669
|
+
setTimeout(() => reject(new Error('Health check timeout')), config.healthCheckTimeoutMs),
|
|
670
|
+
);
|
|
671
|
+
const res = await Promise.race([pongPromise, timeoutPromise]);
|
|
672
|
+
if (!(res as any)?.pong) {
|
|
673
|
+
throw new Error('Health check: unexpected ping response');
|
|
674
|
+
}
|
|
675
|
+
} catch (err) {
|
|
676
|
+
logger.log('warn', `Rust bridge health check failed: ${(err as Error).message}. Killing process to trigger restart.`);
|
|
677
|
+
try {
|
|
678
|
+
this.bridge.kill();
|
|
679
|
+
} catch {
|
|
680
|
+
// Already dead
|
|
681
|
+
}
|
|
682
|
+
// The exit handler will trigger attemptRestart()
|
|
683
|
+
}
|
|
684
|
+
}, config.healthCheckIntervalMs);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private stopHealthCheck(): void {
|
|
688
|
+
if (this._healthCheckTimer) {
|
|
689
|
+
clearInterval(this._healthCheckTimer);
|
|
690
|
+
this._healthCheckTimer = null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// -----------------------------------------------------------------------
|
|
695
|
+
// Commands — thin typed wrappers over sendCommand
|
|
696
|
+
// -----------------------------------------------------------------------
|
|
697
|
+
|
|
698
|
+
/** Ping the Rust process. */
|
|
699
|
+
public async ping(): Promise<boolean> {
|
|
700
|
+
this.ensureRunning();
|
|
701
|
+
const res = await this.bridge.sendCommand('ping', {} as any);
|
|
702
|
+
return res?.pong === true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Get version information for all Rust crates. */
|
|
706
|
+
public async getVersion(): Promise<IVersionInfo> {
|
|
707
|
+
this.ensureRunning();
|
|
708
|
+
return this.bridge.sendCommand('version', {} as any);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Validate an email address. */
|
|
712
|
+
public async validateEmail(email: string): Promise<IValidationResult> {
|
|
713
|
+
this.ensureRunning();
|
|
714
|
+
return this.bridge.sendCommand('validateEmail', { email });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Detect bounce type from SMTP response / diagnostic code. */
|
|
718
|
+
public async detectBounce(opts: {
|
|
719
|
+
smtpResponse?: string;
|
|
720
|
+
diagnosticCode?: string;
|
|
721
|
+
statusCode?: string;
|
|
722
|
+
}): Promise<IBounceDetection> {
|
|
723
|
+
this.ensureRunning();
|
|
724
|
+
return this.bridge.sendCommand('detectBounce', opts);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Scan email content for threats (phishing, spam, malware, etc.). */
|
|
728
|
+
public async scanContent(opts: {
|
|
729
|
+
subject?: string;
|
|
730
|
+
textBody?: string;
|
|
731
|
+
htmlBody?: string;
|
|
732
|
+
attachmentNames?: string[];
|
|
733
|
+
}): Promise<IContentScanResult> {
|
|
734
|
+
this.ensureRunning();
|
|
735
|
+
return this.bridge.sendCommand('scanContent', opts);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Check IP reputation via DNSBL. */
|
|
739
|
+
public async checkIpReputation(ip: string): Promise<IReputationResult> {
|
|
740
|
+
this.ensureRunning();
|
|
741
|
+
return this.bridge.sendCommand('checkIpReputation', { ip });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Verify DKIM signatures on a raw email message. */
|
|
745
|
+
public async verifyDkim(rawMessage: string): Promise<IDkimVerificationResult[]> {
|
|
746
|
+
this.ensureRunning();
|
|
747
|
+
return this.bridge.sendCommand('verifyDkim', { rawMessage });
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Sign an email with DKIM (RSA or Ed25519). */
|
|
751
|
+
public async signDkim(opts: {
|
|
752
|
+
rawMessage: string;
|
|
753
|
+
domain: string;
|
|
754
|
+
selector?: string;
|
|
755
|
+
privateKey: string;
|
|
756
|
+
keyType?: string;
|
|
757
|
+
}): Promise<{ header: string; signedMessage: string }> {
|
|
758
|
+
this.ensureRunning();
|
|
759
|
+
return this.bridge.sendCommand('signDkim', opts);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** Check SPF for a sender. */
|
|
763
|
+
public async checkSpf(opts: {
|
|
764
|
+
ip: string;
|
|
765
|
+
heloDomain: string;
|
|
766
|
+
hostname?: string;
|
|
767
|
+
mailFrom: string;
|
|
768
|
+
}): Promise<ISpfResult> {
|
|
769
|
+
this.ensureRunning();
|
|
770
|
+
return this.bridge.sendCommand('checkSpf', opts);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Compound email security verification: DKIM + SPF + DMARC in one IPC call.
|
|
775
|
+
*
|
|
776
|
+
* This is the preferred method for inbound email verification — it avoids
|
|
777
|
+
* 3 sequential round-trips and correctly passes raw mail-auth types internally.
|
|
778
|
+
*/
|
|
779
|
+
public async verifyEmail(opts: {
|
|
780
|
+
rawMessage: string;
|
|
781
|
+
ip: string;
|
|
782
|
+
heloDomain: string;
|
|
783
|
+
hostname?: string;
|
|
784
|
+
mailFrom: string;
|
|
785
|
+
}): Promise<IEmailSecurityResult> {
|
|
786
|
+
this.ensureRunning();
|
|
787
|
+
return this.bridge.sendCommand('verifyEmail', opts);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// -----------------------------------------------------------------------
|
|
791
|
+
// SMTP Client — outbound email delivery via Rust
|
|
792
|
+
// -----------------------------------------------------------------------
|
|
793
|
+
|
|
794
|
+
/** Send a structured email via the Rust SMTP client. */
|
|
795
|
+
public async sendOutboundEmail(opts: ISmtpSendOptions): Promise<ISmtpSendResult> {
|
|
796
|
+
this.ensureRunning();
|
|
797
|
+
return this.bridge.sendCommand('sendEmail', opts);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** Send a pre-formatted raw email via the Rust SMTP client. */
|
|
801
|
+
public async sendRawEmail(opts: ISmtpSendRawOptions): Promise<ISmtpSendResult> {
|
|
802
|
+
this.ensureRunning();
|
|
803
|
+
return this.bridge.sendCommand('sendRawEmail', opts);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Verify connectivity to an SMTP server. */
|
|
807
|
+
public async verifySmtpConnection(opts: ISmtpVerifyOptions): Promise<ISmtpVerifyResult> {
|
|
808
|
+
this.ensureRunning();
|
|
809
|
+
return this.bridge.sendCommand('verifySmtpConnection', opts);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Close a specific connection pool (or all pools if no key is given). */
|
|
813
|
+
public async closeSmtpPool(poolKey?: string): Promise<void> {
|
|
814
|
+
this.ensureRunning();
|
|
815
|
+
await this.bridge.sendCommand('closeSmtpPool', poolKey ? { poolKey } : ({} as any));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Get status of all SMTP client connection pools. */
|
|
819
|
+
public async getSmtpPoolStatus(): Promise<ISmtpPoolStatus> {
|
|
820
|
+
this.ensureRunning();
|
|
821
|
+
return this.bridge.sendCommand('getSmtpPoolStatus', {} as any);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// -----------------------------------------------------------------------
|
|
825
|
+
// SMTP Server lifecycle
|
|
826
|
+
// -----------------------------------------------------------------------
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Start the Rust SMTP server.
|
|
830
|
+
* The server will listen on the configured ports and emit events for
|
|
831
|
+
* emailReceived and authRequest that must be handled by the caller.
|
|
832
|
+
*/
|
|
833
|
+
public async startSmtpServer(config: ISmtpServerConfig): Promise<boolean> {
|
|
834
|
+
this.ensureRunning();
|
|
835
|
+
this._smtpServerConfig = config;
|
|
836
|
+
const result = await this.bridge.sendCommand('startSmtpServer', config);
|
|
837
|
+
return result?.started === true;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Stop the Rust SMTP server. */
|
|
841
|
+
public async stopSmtpServer(): Promise<void> {
|
|
842
|
+
this.ensureRunning();
|
|
843
|
+
this._smtpServerConfig = null;
|
|
844
|
+
await this.bridge.sendCommand('stopSmtpServer', {} as any);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Send the result of email processing back to the Rust SMTP server.
|
|
849
|
+
* This resolves a pending correlation-ID callback, allowing the Rust
|
|
850
|
+
* server to send the SMTP response to the client.
|
|
851
|
+
*/
|
|
852
|
+
public async sendEmailProcessingResult(opts: {
|
|
853
|
+
correlationId: string;
|
|
854
|
+
accepted: boolean;
|
|
855
|
+
smtpCode?: number;
|
|
856
|
+
smtpMessage?: string;
|
|
857
|
+
}): Promise<void> {
|
|
858
|
+
this.ensureRunning();
|
|
859
|
+
await this.bridge.sendCommand('emailProcessingResult', opts);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Send the result of authentication validation back to the Rust SMTP server.
|
|
864
|
+
*/
|
|
865
|
+
public async sendAuthResult(opts: {
|
|
866
|
+
correlationId: string;
|
|
867
|
+
success: boolean;
|
|
868
|
+
message?: string;
|
|
869
|
+
}): Promise<void> {
|
|
870
|
+
this.ensureRunning();
|
|
871
|
+
await this.bridge.sendCommand('authResult', opts);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Send SCRAM credentials back to the Rust SMTP server.
|
|
876
|
+
* Values (salt, storedKey, serverKey) must be base64-encoded.
|
|
877
|
+
*/
|
|
878
|
+
public async sendScramCredentialResult(opts: {
|
|
879
|
+
correlationId: string;
|
|
880
|
+
found: boolean;
|
|
881
|
+
salt?: string;
|
|
882
|
+
iterations?: number;
|
|
883
|
+
storedKey?: string;
|
|
884
|
+
serverKey?: string;
|
|
885
|
+
}): Promise<void> {
|
|
886
|
+
this.ensureRunning();
|
|
887
|
+
await this.bridge.sendCommand('scramCredentialResult', opts);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/** Update rate limit configuration at runtime. */
|
|
891
|
+
public async configureRateLimits(config: IRateLimitConfig): Promise<void> {
|
|
892
|
+
this.ensureRunning();
|
|
893
|
+
await this.bridge.sendCommand('configureRateLimits', config);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// -----------------------------------------------------------------------
|
|
897
|
+
// Event registration — delegates to the underlying bridge EventEmitter
|
|
898
|
+
// -----------------------------------------------------------------------
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Register a handler for emailReceived events from the Rust SMTP server.
|
|
902
|
+
* These events fire when a complete email has been received and needs processing.
|
|
903
|
+
*/
|
|
904
|
+
public onEmailReceived(handler: (data: IEmailReceivedEvent) => void): void {
|
|
905
|
+
this.bridge.on('management:emailReceived', handler);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Register a handler for authRequest events from the Rust SMTP server.
|
|
910
|
+
* The handler must call sendAuthResult() with the correlationId.
|
|
911
|
+
*/
|
|
912
|
+
public onAuthRequest(handler: (data: IAuthRequestEvent) => void): void {
|
|
913
|
+
this.bridge.on('management:authRequest', handler);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Register a handler for scramCredentialRequest events from the Rust SMTP server.
|
|
918
|
+
* The handler must call sendScramCredentialResult() with the correlationId.
|
|
919
|
+
*/
|
|
920
|
+
public onScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void {
|
|
921
|
+
this.bridge.on('management:scramCredentialRequest', handler);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Remove an emailReceived event handler. */
|
|
925
|
+
public offEmailReceived(handler: (data: IEmailReceivedEvent) => void): void {
|
|
926
|
+
this.bridge.off('management:emailReceived', handler);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** Remove an authRequest event handler. */
|
|
930
|
+
public offAuthRequest(handler: (data: IAuthRequestEvent) => void): void {
|
|
931
|
+
this.bridge.off('management:authRequest', handler);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** Remove a scramCredentialRequest event handler. */
|
|
935
|
+
public offScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void {
|
|
936
|
+
this.bridge.off('management:scramCredentialRequest', handler);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Re-export interfaces for consumers
|
|
941
|
+
export type {
|
|
942
|
+
IDkimVerificationResult,
|
|
943
|
+
ISpfResult,
|
|
944
|
+
IDmarcResult,
|
|
945
|
+
IEmailSecurityResult,
|
|
946
|
+
IValidationResult,
|
|
947
|
+
IBounceDetection,
|
|
948
|
+
IContentScanResult,
|
|
949
|
+
IReputationResult as IRustReputationResult,
|
|
950
|
+
IVersionInfo,
|
|
951
|
+
ISmtpServerConfig,
|
|
952
|
+
IRateLimitConfig,
|
|
953
|
+
IEmailData,
|
|
954
|
+
IEmailReceivedEvent,
|
|
955
|
+
IAuthRequestEvent,
|
|
956
|
+
IScramCredentialRequestEvent,
|
|
957
|
+
IOutboundEmail,
|
|
958
|
+
ISmtpSendResult,
|
|
959
|
+
ISmtpSendOptions,
|
|
960
|
+
ISmtpSendRawOptions,
|
|
961
|
+
ISmtpVerifyOptions,
|
|
962
|
+
ISmtpVerifyResult,
|
|
963
|
+
ISmtpPoolStatus,
|
|
964
|
+
};
|