@push.rocks/smartmta 13.0.1 → 14.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.
Files changed (26) hide show
  1. package/changelog.md +37 -0
  2. package/dist_rust/mailer-bin_linux_amd64_musl +0 -0
  3. package/dist_rust/mailer-bin_linux_amd64_musl.third-party-notices.json +0 -51
  4. package/dist_rust/mailer-bin_linux_amd64_musl.third-party-notices.txt +1 -291
  5. package/dist_rust/mailer-bin_linux_amd64_musl.tsrust-build.json +5 -5
  6. package/dist_rust/mailer-bin_linux_arm64_musl +0 -0
  7. package/dist_rust/mailer-bin_linux_arm64_musl.third-party-notices.json +0 -51
  8. package/dist_rust/mailer-bin_linux_arm64_musl.third-party-notices.txt +1 -291
  9. package/dist_rust/mailer-bin_linux_arm64_musl.tsrust-build.json +5 -5
  10. package/dist_ts/00_commitinfo_data.js +1 -1
  11. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +17 -14
  12. package/dist_ts/mail/routing/classes.unified.email.server.js +80 -71
  13. package/dist_ts/security/classes.rustsecuritybridge.d.ts +108 -1
  14. package/dist_ts/security/classes.rustsecuritybridge.js +187 -50
  15. package/native-notices/manifest.json +1 -64
  16. package/native-notices/readme.md +3 -3
  17. package/package.json +10 -11
  18. package/readme.md +22 -14
  19. package/ts/00_commitinfo_data.ts +1 -1
  20. package/ts/mail/routing/classes.unified.email.server.ts +82 -75
  21. package/ts/security/classes.rustsecuritybridge.ts +264 -49
  22. package/native-notices/crates/charset-0.1.5/copyright +0 -12
  23. package/native-notices/crates/charset-0.1.5/license-apache +0 -202
  24. package/native-notices/crates/charset-0.1.5/license-mit +0 -25
  25. package/native-notices/crates/mailparse-0.16.1/license +0 -13
  26. package/native-notices/crates/quoted_printable-0.5.2/license +0 -13
@@ -546,6 +546,66 @@ interface IEmailData {
546
546
  base64: string;
547
547
  }
548
548
 
549
+ /** A mailbox of the parsed-message bridge JSON. */
550
+ export interface IBridgeMailAddress {
551
+ email: string;
552
+ name?: string;
553
+ }
554
+
555
+ /** The header fields of an inline nested message (never produced with smartmta's setting). */
556
+ export interface IBridgeMessageHeader {
557
+ from: IBridgeMailAddress[];
558
+ to: IBridgeMailAddress[];
559
+ cc: IBridgeMailAddress[];
560
+ bcc: IBridgeMailAddress[];
561
+ subject?: string;
562
+ date?: string;
563
+ }
564
+
565
+ /** One body group: the text and HTML sections of one multipart/alternative or one body part. */
566
+ export interface IBridgeMailBodyGroup {
567
+ text?: string;
568
+ html?: string;
569
+ messageHeader?: IBridgeMessageHeader;
570
+ }
571
+
572
+ export interface IBridgeParsedAttachment {
573
+ filename?: string;
574
+ contentType: string;
575
+ disposition?: 'attachment' | 'inline';
576
+ contentId?: string;
577
+ /** Standard base64 of the transfer-decoded bytes. */
578
+ content: string;
579
+ size: number;
580
+ }
581
+
582
+ /**
583
+ * A message as the Rust mailer-mime crate parsed it (docs/cleanroom/mailer-mime.md section 2.3).
584
+ * Each body travels once, in `bodyGroups`; the flattened From list is the concatenation of
585
+ * `fromFields`.
586
+ */
587
+ export interface IBridgeParsedMessage {
588
+ headers: Array<{ key: string; name: string; value: string }>;
589
+ headerLines: Array<{ key: string; line: string }>;
590
+ messageId?: string;
591
+ inReplyTo?: string;
592
+ references: string[];
593
+ /** ISO 8601 UTC with milliseconds. */
594
+ date?: string;
595
+ subject?: string;
596
+ sender?: IBridgeMailAddress;
597
+ replyTo: IBridgeMailAddress[];
598
+ to: IBridgeMailAddress[];
599
+ cc: IBridgeMailAddress[];
600
+ bcc: IBridgeMailAddress[];
601
+ bodyGroups: IBridgeMailBodyGroup[];
602
+ attachments: IBridgeParsedAttachment[];
603
+ /** One flattened mailbox list per From field, in field order. */
604
+ fromFields: IBridgeMailAddress[][];
605
+ partFilenames: string[];
606
+ abruptHeaderEnd: boolean;
607
+ }
608
+
549
609
  interface IEmailReceivedEvent {
550
610
  correlationId: string;
551
611
  sessionId: string;
@@ -557,6 +617,8 @@ interface IEmailReceivedEvent {
557
617
  secure: boolean;
558
618
  authenticatedUser: string | null;
559
619
  securityResults: IInboundEmailSecurityResult | null;
620
+ /** The one parse Rust made at DATA. */
621
+ parsed: IBridgeParsedMessage;
560
622
  }
561
623
 
562
624
  interface IRcptToRequestEvent {
@@ -641,6 +703,10 @@ type TMailerCommands = {
641
703
  };
642
704
  result: IEmailSecurityResult;
643
705
  };
706
+ parseMessage: {
707
+ params: { data: string };
708
+ result: { parsed: IBridgeParsedMessage };
709
+ };
644
710
  startSmtpServer: {
645
711
  params: ISmtpServerConfig;
646
712
  result: { started: boolean };
@@ -726,6 +792,8 @@ type TMailerCommands = {
726
792
  };
727
793
  };
728
794
 
795
+ type TMailerBridge = InstanceType<typeof plugins.smartrust.RustBridge<TMailerCommands>>;
796
+
729
797
  // ---------------------------------------------------------------------------
730
798
  // Bridge state machine
731
799
  // ---------------------------------------------------------------------------
@@ -773,7 +841,18 @@ export class RustSecurityBridge extends EventEmitter {
773
841
  private static instance: RustSecurityBridge | null = null;
774
842
  private static _resilienceConfig: IBridgeResilienceConfig = { ...DEFAULT_RESILIENCE_CONFIG };
775
843
 
776
- private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TMailerCommands>>;
844
+ /** The server default maximum message size, used until `setMaxMessageSize` is called. */
845
+ private static readonly defaultMaxMessageSize = 10 * 1024 * 1024;
846
+
847
+ /** The smartrust bridge of the running process; a new one is created at every spawn. */
848
+ private bridge: TMailerBridge | null = null;
849
+ private readonly localPaths: string[];
850
+ /** The IPC line cap the next spawn uses: 4 × the maximum message size. */
851
+ private _lineCap = 4 * RustSecurityBridge.defaultMaxMessageSize;
852
+ /** The line cap of the running process, which both transport ends were created with. */
853
+ private _runningLineCap: number | null = null;
854
+ /** Management event handlers, attached to every bridge this singleton creates. */
855
+ private readonly managementHandlers = new Map<string, Set<(data: any) => void>>();
777
856
  private _running = false;
778
857
  private _state: BridgeState = BridgeState.Idle;
779
858
  private _restartAttempts = 0;
@@ -785,7 +864,7 @@ export class RustSecurityBridge extends EventEmitter {
785
864
  private _smtpServerConfig: ISmtpServerConfig | null = null;
786
865
 
787
866
  /**
788
- * Map Node.js process.platform / process.arch to the tsrust target suffix
867
+ * Map Node.js process.platform / process.arch to the rsrepo target suffix
789
868
  * of the shipped binaries. Linux binaries are static musl builds, e.g.
790
869
  * mailer-bin_linux_amd64_musl.
791
870
  */
@@ -804,7 +883,7 @@ export class RustSecurityBridge extends EventEmitter {
804
883
  const suffix = RustSecurityBridge.getPlatformSuffix();
805
884
  const localPaths: string[] = [];
806
885
 
807
- // dist_rust/ candidates (tsrust cross-compiled output)
886
+ // dist_rust/ candidates (rsrepo cross-compiled output)
808
887
  if (suffix) {
809
888
  localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', `mailer-bin_${suffix}`));
810
889
  }
@@ -813,24 +892,37 @@ export class RustSecurityBridge extends EventEmitter {
813
892
  localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'release', 'mailer-bin'));
814
893
  localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'debug', 'mailer-bin'));
815
894
 
816
- this.bridge = new plugins.smartrust.RustBridge<TMailerCommands>({
895
+ this.localPaths = localPaths;
896
+ }
897
+
898
+ /**
899
+ * Creates the smartrust bridge for one spawn. The line cap is fixed per process at both ends
900
+ * of the transport: smartrust's `maxPayloadSize` and `mailer-bin --max-line-bytes` get the
901
+ * one computed value. Lifecycle listeners and every registered management handler are
902
+ * attached, so a handler registered before a spawn or restart keeps receiving events.
903
+ */
904
+ private createBridge(): TMailerBridge {
905
+ const cap = this._lineCap;
906
+ const bridge: TMailerBridge = new plugins.smartrust.RustBridge<TMailerCommands>({
817
907
  binaryName: 'mailer-bin',
818
- cliArgs: ['--management'],
908
+ cliArgs: ['--management', '--max-line-bytes', String(cap)],
819
909
  // A full SMTP transaction can legitimately span several socket waits.
820
910
  // Health checks retain their own short timeout and run concurrently.
821
911
  requestTimeoutMs: 300_000,
822
912
  readyTimeoutMs: 10_000,
823
- localPaths,
913
+ maxPayloadSize: cap,
914
+ localPaths: this.localPaths,
824
915
  searchSystemPath: false,
825
916
  });
826
917
 
827
- // Forward lifecycle events
828
- this.bridge.on('ready', () => {
918
+ bridge.on('ready', () => {
919
+ if (bridge !== this.bridge) return;
829
920
  this._running = true;
830
921
  logger.log('info', 'Rust security bridge is ready');
831
922
  });
832
923
 
833
- this.bridge.on('exit', (code: number | null, signal: string | null) => {
924
+ bridge.on('exit', (code: number | null, signal: string | null) => {
925
+ if (bridge !== this.bridge) return;
834
926
  this._running = false;
835
927
  logger.log('error', `Rust security bridge exited (code=${code}, signal=${signal})`);
836
928
 
@@ -845,9 +937,41 @@ export class RustSecurityBridge extends EventEmitter {
845
937
  }
846
938
  });
847
939
 
848
- this.bridge.on('stderr', (line: string) => {
940
+ bridge.on('stderr', (line: string) => {
849
941
  logger.log('debug', `[rust-bridge] ${line}`);
850
942
  });
943
+
944
+ for (const [eventName, handlers] of this.managementHandlers) {
945
+ for (const handler of handlers) {
946
+ bridge.on(eventName, handler);
947
+ }
948
+ }
949
+ return bridge;
950
+ }
951
+
952
+ /** Replaces the bridge with a new one for the current cap and spawns it. */
953
+ private async spawnBridge(): Promise<boolean> {
954
+ const previous = this.bridge;
955
+ if (previous) {
956
+ previous.removeAllListeners();
957
+ previous.kill();
958
+ }
959
+ const bridge = this.createBridge();
960
+ this.bridge = bridge;
961
+ this._runningLineCap = null;
962
+ const ok = await bridge.spawn();
963
+ if (ok && bridge === this.bridge) {
964
+ this._runningLineCap = this._lineCap;
965
+ }
966
+ return ok;
967
+ }
968
+
969
+ /** The bridge of the running process; commands are guarded by ensureRunning() first. */
970
+ private get activeBridge(): TMailerBridge {
971
+ if (!this.bridge) {
972
+ throw new Error('Rust bridge has not been started yet. Call start() first.');
973
+ }
974
+ return this.bridge;
851
975
  }
852
976
 
853
977
  // -----------------------------------------------------------------------
@@ -952,7 +1076,7 @@ export class RustSecurityBridge extends EventEmitter {
952
1076
  this.setState(BridgeState.Starting);
953
1077
 
954
1078
  try {
955
- const ok = await this.bridge.spawn();
1079
+ const ok = await this.spawnBridge();
956
1080
  this._running = ok;
957
1081
  if (ok) {
958
1082
  this.setState(BridgeState.Running);
@@ -988,7 +1112,7 @@ export class RustSecurityBridge extends EventEmitter {
988
1112
  return;
989
1113
  }
990
1114
  try {
991
- this.bridge.kill();
1115
+ this.bridge?.kill();
992
1116
  this._running = false;
993
1117
  this.setState(BridgeState.Stopped);
994
1118
  logger.log('info', 'Rust security bridge stopped');
@@ -997,6 +1121,71 @@ export class RustSecurityBridge extends EventEmitter {
997
1121
  }
998
1122
  }
999
1123
 
1124
+ // -----------------------------------------------------------------------
1125
+ // IPC line cap
1126
+ // -----------------------------------------------------------------------
1127
+
1128
+ /** The IPC line cap the next spawn uses (4 × the maximum message size). */
1129
+ public get lineCap(): number {
1130
+ return this._lineCap;
1131
+ }
1132
+
1133
+ /**
1134
+ * Sets the SMTP maximum message size the IPC line cap derives from: `cap = 4 × n`, the one
1135
+ * value used as smartrust's `maxPayloadSize` and as `mailer-bin --max-line-bytes`. Outbound
1136
+ * requests (`sendEmail`, `sendRawEmail`, `signDkim`, `verifyDkim`) are limited by the same
1137
+ * cap. When the process runs with a different cap it is restarted deliberately (no crash
1138
+ * count, no backoff) and a stored listener configuration is restored; otherwise the value is
1139
+ * recorded for the next spawn. Throws, changing nothing, for an `n` that is not a positive
1140
+ * safe integer or whose cap is not a safe integer.
1141
+ */
1142
+ public async setMaxMessageSize(maxMessageSize: number): Promise<void> {
1143
+ const cap = 4 * maxMessageSize;
1144
+ if (!Number.isSafeInteger(maxMessageSize) || maxMessageSize <= 0 || !Number.isSafeInteger(cap)) {
1145
+ throw new Error(
1146
+ `maxMessageSize must be a positive safe integer whose 4x line cap is a safe integer, got ${maxMessageSize}`,
1147
+ );
1148
+ }
1149
+ this._lineCap = cap;
1150
+ if (this._running && this._state === BridgeState.Running && this._runningLineCap !== cap) {
1151
+ await this.restartForLineCap();
1152
+ }
1153
+ }
1154
+
1155
+ /** A deliberate restart for a changed line cap; it is not a crash. */
1156
+ private async restartForLineCap(): Promise<void> {
1157
+ logger.log('info', `Restarting Rust bridge for IPC line cap ${this._lineCap}`);
1158
+ this.stopHealthCheck();
1159
+ this.setState(BridgeState.Restarting);
1160
+ this._spawnInFlight = true;
1161
+ try {
1162
+ const previous = this.bridge;
1163
+ if (previous) {
1164
+ previous.removeAllListeners();
1165
+ await previous.terminate();
1166
+ }
1167
+ this._running = false;
1168
+ const ok = await this.spawnBridge();
1169
+ this._running = ok;
1170
+ if (!ok) {
1171
+ throw new Error('spawn returned false');
1172
+ }
1173
+ if (!(await this.restoreAfterRestart())) {
1174
+ throw new Error('SMTP server restore failed');
1175
+ }
1176
+ this.setState(BridgeState.Running);
1177
+ this.startHealthCheck();
1178
+ logger.log('info', `Rust bridge running with IPC line cap ${this._lineCap}`);
1179
+ } catch (error) {
1180
+ logger.log('error', `Rust bridge restart for a new line cap failed: ${(error as Error).message}`);
1181
+ // The process did not come back: from here on it is a failure, recovered like a crash.
1182
+ this.attemptRestart();
1183
+ throw error;
1184
+ } finally {
1185
+ this._spawnInFlight = false;
1186
+ }
1187
+ }
1188
+
1000
1189
  // -----------------------------------------------------------------------
1001
1190
  // Auto-restart with exponential backoff
1002
1191
  // -----------------------------------------------------------------------
@@ -1042,7 +1231,7 @@ export class RustSecurityBridge extends EventEmitter {
1042
1231
 
1043
1232
  this._spawnInFlight = true;
1044
1233
  try {
1045
- const ok = await this.bridge.spawn();
1234
+ const ok = await this.spawnBridge();
1046
1235
  this._running = ok;
1047
1236
  if (!ok) {
1048
1237
  throw new Error('spawn returned false');
@@ -1054,7 +1243,7 @@ export class RustSecurityBridge extends EventEmitter {
1054
1243
  // and go through another restart cycle instead of sitting half-alive.
1055
1244
  logger.log('error', 'Rust bridge restarted but the SMTP server could not be restored — killing process to retry');
1056
1245
  try {
1057
- this.bridge.kill();
1246
+ this.bridge?.kill();
1058
1247
  } catch {
1059
1248
  // Already dead
1060
1249
  }
@@ -1086,7 +1275,7 @@ export class RustSecurityBridge extends EventEmitter {
1086
1275
  }
1087
1276
  try {
1088
1277
  logger.log('info', 'Restoring SMTP server after bridge restart');
1089
- const result = await this.bridge.sendCommand('startSmtpServer', this._smtpServerConfig);
1278
+ const result = await this.activeBridge.sendCommand('startSmtpServer', this._smtpServerConfig);
1090
1279
  if (result?.started) {
1091
1280
  logger.log('info', 'SMTP server restored after bridge restart');
1092
1281
  return true;
@@ -1114,7 +1303,7 @@ export class RustSecurityBridge extends EventEmitter {
1114
1303
  }
1115
1304
 
1116
1305
  try {
1117
- const pongPromise = this.bridge.sendCommand('ping', {} as any);
1306
+ const pongPromise = this.activeBridge.sendCommand('ping', {} as any);
1118
1307
  const timeoutPromise = new Promise<never>((_, reject) =>
1119
1308
  setTimeout(() => reject(new Error('Health check timeout')), config.healthCheckTimeoutMs),
1120
1309
  );
@@ -1134,7 +1323,7 @@ export class RustSecurityBridge extends EventEmitter {
1134
1323
  logger.log('error', `Rust bridge health check failed twice in a row: ${(err as Error).message}. Killing process to trigger restart.`);
1135
1324
  this._healthCheckStrikes = 0;
1136
1325
  try {
1137
- this.bridge.kill();
1326
+ this.bridge?.kill();
1138
1327
  } catch {
1139
1328
  // Already dead
1140
1329
  }
@@ -1157,20 +1346,20 @@ export class RustSecurityBridge extends EventEmitter {
1157
1346
  /** Ping the Rust process. */
1158
1347
  public async ping(): Promise<boolean> {
1159
1348
  this.ensureRunning();
1160
- const res = await this.bridge.sendCommand('ping', {} as any);
1349
+ const res = await this.activeBridge.sendCommand('ping', {} as any);
1161
1350
  return res?.pong === true;
1162
1351
  }
1163
1352
 
1164
1353
  /** Get version information for all Rust crates. */
1165
1354
  public async getVersion(): Promise<IVersionInfo> {
1166
1355
  this.ensureRunning();
1167
- return this.bridge.sendCommand('version', {} as any);
1356
+ return this.activeBridge.sendCommand('version', {} as any);
1168
1357
  }
1169
1358
 
1170
1359
  /** Validate an email address. */
1171
1360
  public async validateEmail(email: string): Promise<IValidationResult> {
1172
1361
  this.ensureRunning();
1173
- return this.bridge.sendCommand('validateEmail', { email });
1362
+ return this.activeBridge.sendCommand('validateEmail', { email });
1174
1363
  }
1175
1364
 
1176
1365
  /** Detect bounce type from SMTP response / diagnostic code. */
@@ -1180,7 +1369,7 @@ export class RustSecurityBridge extends EventEmitter {
1180
1369
  statusCode?: string;
1181
1370
  }): Promise<IBounceDetection> {
1182
1371
  this.ensureRunning();
1183
- return this.bridge.sendCommand('detectBounce', opts);
1372
+ return this.activeBridge.sendCommand('detectBounce', opts);
1184
1373
  }
1185
1374
 
1186
1375
  /** Scan email content for threats (phishing, spam, malware, etc.). */
@@ -1191,19 +1380,19 @@ export class RustSecurityBridge extends EventEmitter {
1191
1380
  attachmentNames?: string[];
1192
1381
  }): Promise<IContentScanResult> {
1193
1382
  this.ensureRunning();
1194
- return this.bridge.sendCommand('scanContent', opts);
1383
+ return this.activeBridge.sendCommand('scanContent', opts);
1195
1384
  }
1196
1385
 
1197
1386
  /** Check IP reputation via DNSBL. */
1198
1387
  public async checkIpReputation(ip: string): Promise<IReputationResult> {
1199
1388
  this.ensureRunning();
1200
- return this.bridge.sendCommand('checkIpReputation', { ip });
1389
+ return this.activeBridge.sendCommand('checkIpReputation', { ip });
1201
1390
  }
1202
1391
 
1203
1392
  /** Verify DKIM signatures on a raw email message. */
1204
1393
  public async verifyDkim(rawMessage: string): Promise<IDkimVerificationResult[]> {
1205
1394
  this.ensureRunning();
1206
- return this.bridge.sendCommand('verifyDkim', { rawMessage });
1395
+ return this.activeBridge.sendCommand('verifyDkim', { rawMessage });
1207
1396
  }
1208
1397
 
1209
1398
  /** Sign an email with DKIM (RSA or Ed25519). */
@@ -1215,7 +1404,7 @@ export class RustSecurityBridge extends EventEmitter {
1215
1404
  keyType?: string;
1216
1405
  }): Promise<{ header: string; signedMessage: string }> {
1217
1406
  this.ensureRunning();
1218
- return this.bridge.sendCommand('signDkim', opts);
1407
+ return this.activeBridge.sendCommand('signDkim', opts);
1219
1408
  }
1220
1409
 
1221
1410
  /** Check SPF for a sender. */
@@ -1226,7 +1415,7 @@ export class RustSecurityBridge extends EventEmitter {
1226
1415
  mailFrom: string;
1227
1416
  }): Promise<ISpfResult> {
1228
1417
  this.ensureRunning();
1229
- return this.bridge.sendCommand('checkSpf', opts);
1418
+ return this.activeBridge.sendCommand('checkSpf', opts);
1230
1419
  }
1231
1420
 
1232
1421
  /**
@@ -1243,7 +1432,18 @@ export class RustSecurityBridge extends EventEmitter {
1243
1432
  mailFrom: string;
1244
1433
  }): Promise<IEmailSecurityResult> {
1245
1434
  this.ensureRunning();
1246
- return this.bridge.sendCommand('verifyEmail', opts);
1435
+ return this.activeBridge.sendCommand('verifyEmail', opts);
1436
+ }
1437
+
1438
+ /**
1439
+ * Parses stored message bytes with the parser and setting of the DATA path (`parseMessage`,
1440
+ * ruling Q1), so a replay builds the same Email the live path built. Rejects with the
1441
+ * parser's error text when a parser limit is exceeded or the answer would exceed the line cap.
1442
+ */
1443
+ public async parseMessage(rawMessage: Buffer): Promise<IBridgeParsedMessage> {
1444
+ this.ensureRunning();
1445
+ const result = await this.activeBridge.sendCommand('parseMessage', { data: rawMessage.toString('base64') });
1446
+ return result.parsed;
1247
1447
  }
1248
1448
 
1249
1449
  // -----------------------------------------------------------------------
@@ -1254,7 +1454,7 @@ export class RustSecurityBridge extends EventEmitter {
1254
1454
  public async sendOutboundEmail(opts: ISmtpSendOptions): Promise<ISmtpSendResult> {
1255
1455
  this.ensureRunning();
1256
1456
  try {
1257
- const result = await this.bridge.sendCommand('sendEmail', opts);
1457
+ const result = await this.activeBridge.sendCommand('sendEmail', opts);
1258
1458
  return {
1259
1459
  ...result,
1260
1460
  transactionLog: normalizeSmtpTranscript(result.transactionLog),
@@ -1271,7 +1471,7 @@ export class RustSecurityBridge extends EventEmitter {
1271
1471
  public async sendRawEmail(opts: TSmtpSendRawOptions): Promise<ISmtpSendResult> {
1272
1472
  this.ensureRunning();
1273
1473
  try {
1274
- const result = await this.bridge.sendCommand('sendRawEmail', opts);
1474
+ const result = await this.activeBridge.sendCommand('sendRawEmail', opts);
1275
1475
 
1276
1476
  return {
1277
1477
  ...result,
@@ -1290,30 +1490,30 @@ export class RustSecurityBridge extends EventEmitter {
1290
1490
  mailFrom: string;
1291
1491
  }): Promise<IVerifyCapturedOutboundEmailResult> {
1292
1492
  this.ensureRunning();
1293
- return this.bridge.sendCommand('verifyCapturedOutboundEmail', opts);
1493
+ return this.activeBridge.sendCommand('verifyCapturedOutboundEmail', opts);
1294
1494
  }
1295
1495
 
1296
1496
  public async discardCapturedOutboundEmail(captureId: string): Promise<void> {
1297
1497
  this.ensureRunning();
1298
- await this.bridge.sendCommand('discardCapturedOutboundEmail', { captureId });
1498
+ await this.activeBridge.sendCommand('discardCapturedOutboundEmail', { captureId });
1299
1499
  }
1300
1500
 
1301
1501
  /** Verify connectivity to an SMTP server. */
1302
1502
  public async verifySmtpConnection(opts: ISmtpVerifyOptions): Promise<ISmtpVerifyResult> {
1303
1503
  this.ensureRunning();
1304
- return this.bridge.sendCommand('verifySmtpConnection', opts);
1504
+ return this.activeBridge.sendCommand('verifySmtpConnection', opts);
1305
1505
  }
1306
1506
 
1307
1507
  /** Close a specific connection pool (or all pools if no key is given). */
1308
1508
  public async closeSmtpPool(poolKey?: string): Promise<void> {
1309
1509
  this.ensureRunning();
1310
- await this.bridge.sendCommand('closeSmtpPool', poolKey ? { poolKey } : ({} as any));
1510
+ await this.activeBridge.sendCommand('closeSmtpPool', poolKey ? { poolKey } : ({} as any));
1311
1511
  }
1312
1512
 
1313
1513
  /** Get status of all SMTP client connection pools. */
1314
1514
  public async getSmtpPoolStatus(): Promise<ISmtpPoolStatus> {
1315
1515
  this.ensureRunning();
1316
- return this.bridge.sendCommand('getSmtpPoolStatus', {} as any);
1516
+ return this.activeBridge.sendCommand('getSmtpPoolStatus', {} as any);
1317
1517
  }
1318
1518
 
1319
1519
  // -----------------------------------------------------------------------
@@ -1328,7 +1528,7 @@ export class RustSecurityBridge extends EventEmitter {
1328
1528
  public async startSmtpServer(config: ISmtpServerConfig): Promise<boolean> {
1329
1529
  this.ensureRunning();
1330
1530
  this._smtpServerConfig = config;
1331
- const result = await this.bridge.sendCommand('startSmtpServer', config);
1531
+ const result = await this.activeBridge.sendCommand('startSmtpServer', config);
1332
1532
  return result?.started === true;
1333
1533
  }
1334
1534
 
@@ -1336,7 +1536,7 @@ export class RustSecurityBridge extends EventEmitter {
1336
1536
  public async stopSmtpServer(): Promise<void> {
1337
1537
  this.ensureRunning();
1338
1538
  this._smtpServerConfig = null;
1339
- await this.bridge.sendCommand('stopSmtpServer', {} as any);
1539
+ await this.activeBridge.sendCommand('stopSmtpServer', {} as any);
1340
1540
  }
1341
1541
 
1342
1542
  /**
@@ -1351,7 +1551,7 @@ export class RustSecurityBridge extends EventEmitter {
1351
1551
  smtpMessage?: string;
1352
1552
  }): Promise<void> {
1353
1553
  this.ensureRunning();
1354
- await this.bridge.sendCommand('emailProcessingResult', opts);
1554
+ await this.activeBridge.sendCommand('emailProcessingResult', opts);
1355
1555
  }
1356
1556
 
1357
1557
  /**
@@ -1364,7 +1564,7 @@ export class RustSecurityBridge extends EventEmitter {
1364
1564
  smtpMessage?: string;
1365
1565
  }): Promise<void> {
1366
1566
  this.ensureRunning();
1367
- await this.bridge.sendCommand('rcptToResult', opts);
1567
+ await this.activeBridge.sendCommand('rcptToResult', opts);
1368
1568
  }
1369
1569
 
1370
1570
  /**
@@ -1376,7 +1576,7 @@ export class RustSecurityBridge extends EventEmitter {
1376
1576
  message?: string;
1377
1577
  }): Promise<void> {
1378
1578
  this.ensureRunning();
1379
- await this.bridge.sendCommand('authResult', opts);
1579
+ await this.activeBridge.sendCommand('authResult', opts);
1380
1580
  }
1381
1581
 
1382
1582
  /**
@@ -1392,32 +1592,47 @@ export class RustSecurityBridge extends EventEmitter {
1392
1592
  serverKey?: string;
1393
1593
  }): Promise<void> {
1394
1594
  this.ensureRunning();
1395
- await this.bridge.sendCommand('scramCredentialResult', opts);
1595
+ await this.activeBridge.sendCommand('scramCredentialResult', opts);
1396
1596
  }
1397
1597
 
1398
1598
  /** Update rate limit configuration at runtime. */
1399
1599
  public async configureRateLimits(config: IRateLimitConfig): Promise<void> {
1400
1600
  this.ensureRunning();
1401
- await this.bridge.sendCommand('configureRateLimits', config);
1601
+ await this.activeBridge.sendCommand('configureRateLimits', config);
1402
1602
  }
1403
1603
 
1404
1604
  // -----------------------------------------------------------------------
1405
1605
  // Event registration — delegates to the underlying bridge EventEmitter
1406
1606
  // -----------------------------------------------------------------------
1407
1607
 
1608
+ private addManagementHandler(eventName: string, handler: (data: any) => void): void {
1609
+ let handlers = this.managementHandlers.get(eventName);
1610
+ if (!handlers) {
1611
+ handlers = new Set();
1612
+ this.managementHandlers.set(eventName, handlers);
1613
+ }
1614
+ handlers.add(handler);
1615
+ this.bridge?.on(eventName, handler);
1616
+ }
1617
+
1618
+ private removeManagementHandler(eventName: string, handler: (data: any) => void): void {
1619
+ this.managementHandlers.get(eventName)?.delete(handler);
1620
+ this.bridge?.off(eventName, handler);
1621
+ }
1622
+
1408
1623
  /**
1409
1624
  * Register a handler for emailReceived events from the Rust SMTP server.
1410
1625
  * These events fire when a complete email has been received and needs processing.
1411
1626
  */
1412
1627
  public onEmailReceived(handler: (data: IEmailReceivedEvent) => void): void {
1413
- this.bridge.on('management:emailReceived', handler);
1628
+ this.addManagementHandler('management:emailReceived', handler);
1414
1629
  }
1415
1630
 
1416
1631
  /**
1417
1632
  * Register a handler for RCPT TO policy requests from the Rust SMTP server.
1418
1633
  */
1419
1634
  public onRcptToRequest(handler: (data: IRcptToRequestEvent) => void): void {
1420
- this.bridge.on('management:rcptToRequest', handler);
1635
+ this.addManagementHandler('management:rcptToRequest', handler);
1421
1636
  }
1422
1637
 
1423
1638
  /**
@@ -1425,7 +1640,7 @@ export class RustSecurityBridge extends EventEmitter {
1425
1640
  * The handler must call sendAuthResult() with the correlationId.
1426
1641
  */
1427
1642
  public onAuthRequest(handler: (data: IAuthRequestEvent) => void): void {
1428
- this.bridge.on('management:authRequest', handler);
1643
+ this.addManagementHandler('management:authRequest', handler);
1429
1644
  }
1430
1645
 
1431
1646
  /**
@@ -1433,27 +1648,27 @@ export class RustSecurityBridge extends EventEmitter {
1433
1648
  * The handler must call sendScramCredentialResult() with the correlationId.
1434
1649
  */
1435
1650
  public onScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void {
1436
- this.bridge.on('management:scramCredentialRequest', handler);
1651
+ this.addManagementHandler('management:scramCredentialRequest', handler);
1437
1652
  }
1438
1653
 
1439
1654
  /** Remove an emailReceived event handler. */
1440
1655
  public offEmailReceived(handler: (data: IEmailReceivedEvent) => void): void {
1441
- this.bridge.off('management:emailReceived', handler);
1656
+ this.removeManagementHandler('management:emailReceived', handler);
1442
1657
  }
1443
1658
 
1444
1659
  /** Remove an RCPT TO policy request handler. */
1445
1660
  public offRcptToRequest(handler: (data: IRcptToRequestEvent) => void): void {
1446
- this.bridge.off('management:rcptToRequest', handler);
1661
+ this.removeManagementHandler('management:rcptToRequest', handler);
1447
1662
  }
1448
1663
 
1449
1664
  /** Remove an authRequest event handler. */
1450
1665
  public offAuthRequest(handler: (data: IAuthRequestEvent) => void): void {
1451
- this.bridge.off('management:authRequest', handler);
1666
+ this.removeManagementHandler('management:authRequest', handler);
1452
1667
  }
1453
1668
 
1454
1669
  /** Remove a scramCredentialRequest event handler. */
1455
1670
  public offScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void {
1456
- this.bridge.off('management:scramCredentialRequest', handler);
1671
+ this.removeManagementHandler('management:scramCredentialRequest', handler);
1457
1672
  }
1458
1673
  }
1459
1674
 
@@ -1,12 +0,0 @@
1
- charset is copyright 2013-2016 Mozilla Foundation.
2
-
3
- Licensed under the Apache License, Version 2.0
4
- <LICENSE-APACHE or
5
- https://www.apache.org/licenses/LICENSE-2.0> or the MIT
6
- license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
7
- at your option. All files in the project carrying such
8
- notice may not be copied, modified, or distributed except
9
- according to those terms.
10
-
11
- Test code within encoding_rs is dedicated to the Public Domain when so
12
- designated (see the individual files for PD/CC0-dedicated sections).