@push.rocks/smartmta 9.4.0 → 9.5.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 (35) hide show
  1. package/changelog.md +23 -0
  2. package/dist_rust/mailer-bin_linux_amd64 +0 -0
  3. package/dist_rust/mailer-bin_linux_arm64 +0 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/index.d.ts +1 -0
  6. package/dist_ts/index.js +2 -1
  7. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +71 -1
  8. package/dist_ts/mail/delivery/classes.delivery.queue.js +794 -179
  9. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +11 -0
  10. package/dist_ts/mail/delivery/classes.delivery.system.js +206 -80
  11. package/dist_ts/mail/delivery/functions.raw-email.d.ts +9 -0
  12. package/dist_ts/mail/delivery/functions.raw-email.js +85 -0
  13. package/dist_ts/mail/delivery/symbols.delivery.queue.d.ts +6 -0
  14. package/dist_ts/mail/delivery/symbols.delivery.queue.js +7 -0
  15. package/dist_ts/mail/index.d.ts +1 -0
  16. package/dist_ts/mail/index.js +1 -1
  17. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +40 -0
  18. package/dist_ts/mail/routing/classes.unified.email.server.js +222 -76
  19. package/dist_ts/mail/routing/symbols.unified.email.server.d.ts +2 -0
  20. package/dist_ts/mail/routing/symbols.unified.email.server.js +3 -0
  21. package/dist_ts/security/classes.rustsecuritybridge.d.ts +11 -0
  22. package/dist_ts/security/classes.rustsecuritybridge.js +1 -1
  23. package/package.json +2 -2
  24. package/readme.hints.md +8 -0
  25. package/readme.md +36 -3
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/index.ts +1 -0
  28. package/ts/mail/delivery/classes.delivery.queue.ts +969 -209
  29. package/ts/mail/delivery/classes.delivery.system.ts +254 -101
  30. package/ts/mail/delivery/functions.raw-email.ts +111 -0
  31. package/ts/mail/delivery/symbols.delivery.queue.ts +6 -0
  32. package/ts/mail/index.ts +1 -0
  33. package/ts/mail/routing/classes.unified.email.server.ts +329 -85
  34. package/ts/mail/routing/symbols.unified.email.server.ts +6 -0
  35. package/ts/security/classes.rustsecuritybridge.ts +6 -0
package/ts/mail/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from './interfaces.storage.js';
3
3
  export * from './routing/index.js';
4
4
  export * from './security/index.js';
5
5
  export type { ISmtpTransactionAttempt } from './delivery/interfaces.js';
6
+ export type { IRawSmtpEnvelope } from './delivery/functions.raw-email.js';
6
7
 
7
8
  // Make the core and delivery modules accessible
8
9
  import * as Core from './core/index.js';
@@ -38,6 +38,7 @@ import type { ISmtpSendResult, IOutboundEmail } from '../../security/classes.rus
38
38
  import { MultiModeDeliverySystem, type IDeliveryStats, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
39
39
  import {
40
40
  ACCEPTED_ENVELOPE_IDEMPOTENCY_RETENTION_MS,
41
+ DEFAULT_MAX_RAW_MESSAGE_BYTES,
41
42
  UnifiedDeliveryQueue,
42
43
  type IQueueItem,
43
44
  type IQueueOptions,
@@ -49,6 +50,20 @@ import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../d
49
50
  import { EmailActionExecutor } from './classes.email.action.executor.js';
50
51
  import { DkimManager } from './classes.dkim.manager.js';
51
52
  import { callObserverSafely, emitSafely } from '../delivery/functions.safe-observers.js';
53
+ import {
54
+ assertCanonicalRawRfc822Message,
55
+ normalizeRawSmtpEnvelope,
56
+ type IRawSmtpEnvelope,
57
+ } from '../delivery/functions.raw-email.js';
58
+ import {
59
+ kQueueCloseAdmission,
60
+ kQueueEnqueueRaw,
61
+ kQueueReserveRawAccess,
62
+ } from '../delivery/symbols.delivery.queue.js';
63
+ import {
64
+ kSendOutboundEmailInternal,
65
+ kSendRawOutboundEmailInternal,
66
+ } from './symbols.unified.email.server.js';
52
67
 
53
68
 
54
69
  /** External DcRouter interface shape used by UnifiedEmailServer */
@@ -282,6 +297,30 @@ export interface IOutboundTopologyIdentity {
282
297
  ipAddresses?: string[];
283
298
  }
284
299
 
300
+ interface IOutboundTransportParameters {
301
+ host: string;
302
+ port: number;
303
+ connectHost?: string;
304
+ connectPort?: number;
305
+ secure: boolean;
306
+ domain: string;
307
+ auth?: { user: string; pass: string };
308
+ connectionTimeoutSecs: number;
309
+ socketTimeoutSecs: number;
310
+ poolKey: string;
311
+ maxPoolConnections: number;
312
+ tlsOpportunistic: boolean;
313
+ disablePooling?: boolean;
314
+ outboundAuthenticationCaptureId?: string;
315
+ }
316
+
317
+ interface IOutboundDkimMaterial {
318
+ domain: string;
319
+ selector: string;
320
+ privateKey: string;
321
+ keyType: 'rsa' | 'ed25519';
322
+ }
323
+
285
324
  export interface ISelfMxIdentityMatch {
286
325
  isSelf: boolean;
287
326
  source: 'explicit' | 'dns-derived' | 'dynamic' | null;
@@ -499,6 +538,10 @@ export class UnifiedEmailServer extends EventEmitter {
499
538
  private smtpListenerActive = false;
500
539
  private smtpListenerShutdown = false;
501
540
  private smtpListenerRestartPromise?: Promise<void>;
541
+ private acceptingOutboundWork = true;
542
+ private readonly outboundWorkFlights = new Set<Promise<unknown>>();
543
+ private readonly outboundAuthenticationFlights = new Set<Promise<IOutboundAuthenticationCheck>>();
544
+ private stopFlight?: Promise<void>;
502
545
  private static readonly VERIFIER_DERIVATION_CONCURRENCY = 4;
503
546
  private static readonly VERIFIER_DERIVATION_MAX_QUEUE = 32;
504
547
  private verifierDerivationActive = 0;
@@ -634,6 +677,23 @@ export class UnifiedEmailServer extends EventEmitter {
634
677
  // We'll create the SMTP servers during the start() method
635
678
  }
636
679
 
680
+ private assertAcceptingOutboundWork(): void {
681
+ if (!this.acceptingOutboundWork) {
682
+ throw new Error('UnifiedEmailServer is not accepting outbound work');
683
+ }
684
+ }
685
+
686
+ private trackOutboundWork<T>(operationArg: () => Promise<T>): Promise<T> {
687
+ let flight: Promise<T>;
688
+ flight = Promise.resolve()
689
+ .then(operationArg)
690
+ .finally(() => {
691
+ this.outboundWorkFlights.delete(flight);
692
+ });
693
+ this.outboundWorkFlights.add(flight);
694
+ return flight;
695
+ }
696
+
637
697
  public onRecipientRejected(
638
698
  listener: (event: IRecipientRejectedEvent) => Promise<void> | void,
639
699
  ): this {
@@ -890,32 +950,30 @@ export class UnifiedEmailServer extends EventEmitter {
890
950
  * Send an outbound email via the Rust SMTP client.
891
951
  * Uses connection pooling in the Rust binary for efficiency.
892
952
  */
893
- public async sendOutboundEmail(host: string, port: number, email: Email, options?: {
953
+ public sendOutboundEmail(host: string, port: number, email: Email, options?: {
894
954
  auth?: { user: string; pass: string };
895
955
  dkimDomain?: string;
896
956
  dkimSelector?: string;
897
957
  tlsOpportunistic?: boolean;
898
958
  }): Promise<ISmtpSendResult> {
899
- // Build DKIM config if domain has keys
900
- let dkim: {
901
- domain: string;
902
- selector: string;
903
- privateKey: string;
904
- keyType: 'rsa' | 'ed25519';
905
- } | undefined;
906
- if (options?.dkimDomain) {
907
- const selector = options.dkimSelector || 'default';
908
- const material = await this.dkimCreator.readValidatedDKIMKeysForSelector(
909
- options.dkimDomain,
910
- selector,
911
- );
912
- dkim = {
913
- domain: options.dkimDomain,
914
- selector,
915
- privateKey: material.privateKey,
916
- keyType: material.keyType,
917
- };
918
- }
959
+ this.assertAcceptingOutboundWork();
960
+ return this.trackOutboundWork(() => this[kSendOutboundEmailInternal](
961
+ host,
962
+ port,
963
+ email,
964
+ options,
965
+ ));
966
+ }
967
+
968
+ public async [kSendOutboundEmailInternal](host: string, port: number, email: Email, options?: {
969
+ auth?: { user: string; pass: string };
970
+ dkimDomain?: string;
971
+ dkimSelector?: string;
972
+ tlsOpportunistic?: boolean;
973
+ }): Promise<ISmtpSendResult> {
974
+ const dkim = options?.dkimDomain
975
+ ? await this.resolveOutboundDkim(options.dkimDomain, options.dkimSelector || 'default')
976
+ : undefined;
919
977
 
920
978
  // Serialize the Email to the outbound format
921
979
  const outboundEmail: IOutboundEmail = {
@@ -935,18 +993,170 @@ export class UnifiedEmailServer extends EventEmitter {
935
993
  })),
936
994
  };
937
995
 
938
- const secure = port === 465;
939
- const tlsOpportunistic = options?.tlsOpportunistic ?? (port === 25);
940
996
  const senderDomain = email.getFromDomain()?.toLowerCase() || undefined;
941
- const proxy = await this.options.outbound?.connectionProxyProvider?.({
997
+ return this.sendWithOutboundTransport({
942
998
  host,
943
999
  port,
1000
+ envelopeFrom: email.from,
1001
+ senderDomain,
1002
+ auth: options?.auth,
1003
+ tlsOpportunistic: options?.tlsOpportunistic,
1004
+ dkim,
1005
+ send: (transport) => this.rustBridge.sendOutboundEmail({
1006
+ ...transport,
1007
+ email: outboundEmail,
1008
+ dkim,
1009
+ }),
1010
+ });
1011
+ }
1012
+
1013
+ /** Send exact RFC822 bytes through the same outbound transport path as structured mail. */
1014
+ public sendRawOutboundEmail(
1015
+ host: string,
1016
+ port: number,
1017
+ rawMessage: Buffer,
1018
+ envelopeArg: IRawSmtpEnvelope,
1019
+ options: {
1020
+ auth?: { user: string; pass: string };
1021
+ dkimDomain: string;
1022
+ dkimSelector: string;
1023
+ tlsOpportunistic?: boolean;
1024
+ },
1025
+ ): Promise<ISmtpSendResult> {
1026
+ this.assertAcceptingOutboundWork();
1027
+ assertCanonicalRawRfc822Message(rawMessage);
1028
+ const maxRawMessageBytes = this.options.queue.maxRawMessageBytes
1029
+ ?? DEFAULT_MAX_RAW_MESSAGE_BYTES;
1030
+ if (rawMessage.length > maxRawMessageBytes) {
1031
+ throw new Error(`Raw outbound message exceeds ${maxRawMessageBytes} bytes`);
1032
+ }
1033
+ const releaseRawReservation = this.deliveryQueue[kQueueReserveRawAccess](rawMessage.length);
1034
+ let exactRawMessage: Buffer;
1035
+ let envelope: IRawSmtpEnvelope;
1036
+ try {
1037
+ exactRawMessage = Buffer.from(rawMessage);
1038
+ assertCanonicalRawRfc822Message(exactRawMessage);
1039
+ envelope = normalizeRawSmtpEnvelope(envelopeArg);
1040
+ } catch (error) {
1041
+ releaseRawReservation();
1042
+ throw error;
1043
+ }
1044
+ const exactOptions = {
1045
+ ...options,
1046
+ ...(options.auth ? { auth: { ...options.auth } } : {}),
1047
+ };
1048
+ return this.trackOutboundWork(async () => {
1049
+ try {
1050
+ return await this[kSendRawOutboundEmailInternal](
1051
+ host,
1052
+ port,
1053
+ exactRawMessage,
1054
+ envelope,
1055
+ exactOptions,
1056
+ );
1057
+ } finally {
1058
+ releaseRawReservation();
1059
+ }
1060
+ });
1061
+ }
1062
+
1063
+ public async [kSendRawOutboundEmailInternal](
1064
+ host: string,
1065
+ port: number,
1066
+ rawMessage: Buffer,
1067
+ envelopeArg: IRawSmtpEnvelope,
1068
+ options: {
1069
+ auth?: { user: string; pass: string };
1070
+ dkimDomain: string;
1071
+ dkimSelector: string;
1072
+ tlsOpportunistic?: boolean;
1073
+ },
1074
+ ): Promise<ISmtpSendResult> {
1075
+ assertCanonicalRawRfc822Message(rawMessage);
1076
+ const maxRawMessageBytes = this.options.queue.maxRawMessageBytes
1077
+ ?? DEFAULT_MAX_RAW_MESSAGE_BYTES;
1078
+ if (rawMessage.length > maxRawMessageBytes) {
1079
+ throw new Error(`Raw outbound message exceeds ${maxRawMessageBytes} bytes`);
1080
+ }
1081
+ const envelope = normalizeRawSmtpEnvelope(envelopeArg);
1082
+ const senderDomain = envelope.from.split('@')[1];
1083
+ const dkimDomain = options.dkimDomain.toLowerCase();
1084
+ if (dkimDomain !== senderDomain) {
1085
+ throw new SmtpDeliveryError({
1086
+ message: `Raw outbound DKIM domain ${dkimDomain} does not match envelope sender domain ${senderDomain}`,
1087
+ errorType: 'dkim',
1088
+ retryable: false,
1089
+ phase: 'message_body',
1090
+ transactionLog: [],
1091
+ recipients: envelope.to,
1092
+ });
1093
+ }
1094
+ const dkim = await this.resolveOutboundDkim(dkimDomain, options.dkimSelector);
1095
+ return this.sendWithOutboundTransport({
1096
+ host,
1097
+ port,
1098
+ envelopeFrom: envelope.from,
1099
+ senderDomain,
1100
+ auth: options.auth,
1101
+ tlsOpportunistic: options.tlsOpportunistic,
1102
+ dkim,
1103
+ send: (transport) => this.rustBridge.sendRawEmail({
1104
+ ...transport,
1105
+ envelopeFrom: envelope.from,
1106
+ envelopeTo: [...envelope.to],
1107
+ rawMessageBase64: rawMessage.toString('base64'),
1108
+ dkim,
1109
+ }),
1110
+ });
1111
+ }
1112
+
1113
+ private async resolveOutboundDkim(
1114
+ domainArg: string,
1115
+ selectorArg: string,
1116
+ ): Promise<IOutboundDkimMaterial> {
1117
+ try {
1118
+ const material = await this.dkimCreator.readValidatedDKIMKeysForSelector(
1119
+ domainArg,
1120
+ selectorArg,
1121
+ );
1122
+ return {
1123
+ domain: domainArg,
1124
+ selector: selectorArg,
1125
+ privateKey: material.privateKey,
1126
+ keyType: material.keyType,
1127
+ };
1128
+ } catch (error) {
1129
+ throw new SmtpDeliveryError({
1130
+ message: getErrorMessage(error),
1131
+ errorType: 'dkim',
1132
+ retryable: false,
1133
+ phase: 'message_body',
1134
+ transactionLog: [],
1135
+ recipients: [],
1136
+ }, error);
1137
+ }
1138
+ }
1139
+
1140
+ private async sendWithOutboundTransport(optionsArg: {
1141
+ host: string;
1142
+ port: number;
1143
+ envelopeFrom: string;
1144
+ senderDomain?: string;
1145
+ auth?: { user: string; pass: string };
1146
+ tlsOpportunistic?: boolean;
1147
+ dkim?: IOutboundDkimMaterial;
1148
+ send: (transport: IOutboundTransportParameters) => Promise<ISmtpSendResult>;
1149
+ }): Promise<ISmtpSendResult> {
1150
+ const secure = optionsArg.port === 465;
1151
+ const tlsOpportunistic = optionsArg.tlsOpportunistic ?? (optionsArg.port === 25);
1152
+ const proxy = await this.options.outbound?.connectionProxyProvider?.({
1153
+ host: optionsArg.host,
1154
+ port: optionsArg.port,
944
1155
  secure,
945
1156
  ehloDomain: this.getOutboundHostname(),
946
1157
  tlsOpportunistic,
947
- senderDomain,
1158
+ senderDomain: optionsArg.senderDomain,
948
1159
  });
949
-
950
1160
  const captureId = proxy?.sourceEvidence ? plugins.uuid.v4() : undefined;
951
1161
  const attachAuthenticationHandle = <T extends ISmtpSendResult | SmtpDeliveryError>(valueArg: T): T => {
952
1162
  if (!captureId || !proxy?.sourceEvidence) return valueArg;
@@ -954,10 +1164,10 @@ export class UnifiedEmailServer extends EventEmitter {
954
1164
  captureId,
955
1165
  sourceEvidence: proxy.sourceEvidence,
956
1166
  heloDomain: proxy.ehloHostname || this.getOutboundHostname(),
957
- receiverHostname: host,
958
- envelopeFrom: email.from,
959
- dkimDomain: dkim?.domain,
960
- dkimSelector: dkim?.selector,
1167
+ receiverHostname: optionsArg.host,
1168
+ envelopeFrom: optionsArg.envelopeFrom,
1169
+ dkimDomain: optionsArg.dkim?.domain,
1170
+ dkimSelector: optionsArg.dkim?.selector,
961
1171
  });
962
1172
  Object.defineProperty(valueArg, 'outboundAuthenticationHandle', {
963
1173
  value: handle,
@@ -968,18 +1178,16 @@ export class UnifiedEmailServer extends EventEmitter {
968
1178
  };
969
1179
 
970
1180
  try {
971
- const result = await this.rustBridge.sendOutboundEmail({
972
- host,
973
- port,
1181
+ const result = await optionsArg.send({
1182
+ host: optionsArg.host,
1183
+ port: optionsArg.port,
974
1184
  ...(proxy ? { connectHost: proxy.connectHost, connectPort: proxy.connectPort } : {}),
975
1185
  secure,
976
1186
  domain: proxy?.ehloHostname || this.getOutboundHostname(),
977
- auth: options?.auth,
978
- email: outboundEmail,
979
- dkim,
1187
+ auth: optionsArg.auth,
980
1188
  connectionTimeoutSecs: Math.floor((this.options.outbound?.connectionTimeout || 30000) / 1000),
981
1189
  socketTimeoutSecs: Math.floor((this.options.outbound?.socketTimeout || 120000) / 1000),
982
- poolKey: proxy?.poolKey || `${host}:${port}`,
1190
+ poolKey: proxy?.poolKey || `${optionsArg.host}:${optionsArg.port}`,
983
1191
  maxPoolConnections: this.options.outbound?.maxConnections || 10,
984
1192
  tlsOpportunistic,
985
1193
  ...(proxy ? { disablePooling: true } : {}),
@@ -1015,7 +1223,8 @@ export class UnifiedEmailServer extends EventEmitter {
1015
1223
  checkedAt: new Date().toISOString(),
1016
1224
  error: errorArg,
1017
1225
  });
1018
- const result = (async (): Promise<IOutboundAuthenticationCheck> => {
1226
+ let result: Promise<IOutboundAuthenticationCheck>;
1227
+ result = (async (): Promise<IOutboundAuthenticationCheck> => {
1019
1228
  let timeout: ReturnType<typeof setTimeout> | undefined;
1020
1229
  try {
1021
1230
  const sourceEvidence = await new Promise<IOutboundConnectionSourceEvidence | null>((resolve, reject) => {
@@ -1071,7 +1280,10 @@ export class UnifiedEmailServer extends EventEmitter {
1071
1280
  } finally {
1072
1281
  if (timeout) clearTimeout(timeout);
1073
1282
  }
1074
- })();
1283
+ })().finally(() => {
1284
+ this.outboundAuthenticationFlights.delete(result);
1285
+ });
1286
+ this.outboundAuthenticationFlights.add(result);
1075
1287
  return { pending, result };
1076
1288
  }
1077
1289
 
@@ -1442,18 +1654,46 @@ export class UnifiedEmailServer extends EventEmitter {
1442
1654
  /**
1443
1655
  * Stop the unified email server
1444
1656
  */
1445
- public async stop(): Promise<void> {
1657
+ public stop(): Promise<void> {
1658
+ if (this.stopFlight) return this.stopFlight;
1446
1659
  logger.log('info', 'Stopping UnifiedEmailServer');
1660
+ this.acceptingOutboundWork = false;
1661
+ this.deliveryQueue[kQueueCloseAdmission]();
1662
+ this.stopFlight = this.stopOnce();
1663
+ return this.stopFlight;
1664
+ }
1447
1665
 
1448
- try {
1666
+ private async stopOnce(): Promise<void> {
1667
+ const errors: Error[] = [];
1668
+ const runStep = async (nameArg: string, stepArg: () => Promise<void> | void): Promise<void> => {
1669
+ try {
1670
+ await stepArg();
1671
+ } catch (error) {
1672
+ const wrapped = new Error(`${nameArg}: ${getErrorMessage(error)}`, { cause: error });
1673
+ errors.push(wrapped);
1674
+ logger.log('error', wrapped.message);
1675
+ }
1676
+ };
1677
+ const runBestEffortStep = async (
1678
+ nameArg: string,
1679
+ stepArg: () => Promise<void> | void,
1680
+ ): Promise<void> => {
1681
+ try {
1682
+ await stepArg();
1683
+ } catch (error) {
1684
+ logger.log('warn', `${nameArg}: ${getErrorMessage(error)}`);
1685
+ }
1686
+ };
1687
+
1688
+ await runStep('accepted-envelope cleanup drain failed', async () => {
1449
1689
  if (this.acceptedEnvelopeDispatchCleanupTimer) {
1450
1690
  clearInterval(this.acceptedEnvelopeDispatchCleanupTimer);
1451
1691
  this.acceptedEnvelopeDispatchCleanupTimer = undefined;
1452
1692
  }
1453
1693
  await this.acceptedEnvelopeDispatchCleanupPromise;
1454
- // Prevent new listener restarts from being scheduled or executed
1455
- // during shutdown, then drain any in-flight reconfiguration before
1456
- // tearing down (the chain may be extended while awaited).
1694
+ });
1695
+
1696
+ await runStep('SMTP listener reconfiguration drain failed', async () => {
1457
1697
  this.smtpListenerShutdown = true;
1458
1698
  this.smtpListenerActive = false;
1459
1699
  while (this.smtpListenerRestartPromise) {
@@ -1463,55 +1703,49 @@ export class UnifiedEmailServer extends EventEmitter {
1463
1703
  this.smtpListenerRestartPromise = undefined;
1464
1704
  }
1465
1705
  }
1466
- await this.stopDnsValidation();
1467
-
1468
- // Stop the Rust SMTP server first
1469
- try {
1470
- await this.rustBridge.stopSmtpServer();
1471
- logger.log('info', 'Rust SMTP server stopped');
1472
- } catch (err) {
1473
- logger.log('warn', `Error stopping Rust SMTP server: ${(err as Error).message}`);
1474
- }
1475
- this.smtpListenerActive = false;
1476
-
1477
- // Clear the servers array - servers will be garbage collected
1478
- this.servers = [];
1706
+ });
1707
+ await runStep('DNS validation shutdown failed', () => this.stopDnsValidation());
1479
1708
 
1480
- this.unregisterBridgeEventHandlers();
1481
- this.unregisterBridgeStateHandler();
1482
- await this.rustBridge.stop();
1709
+ await runBestEffortStep(
1710
+ 'Rust SMTP listener shutdown skipped or failed',
1711
+ () => this.rustBridge.stopSmtpServer(),
1712
+ );
1713
+ this.smtpListenerActive = false;
1714
+ this.servers = [];
1483
1715
 
1484
- // Stop the delivery system
1485
- if (this.deliverySystem) {
1486
- await this.deliverySystem.stop();
1487
- logger.log('info', 'Email delivery system stopped');
1488
- }
1716
+ await runStep('delivery-system drain failed', async () => {
1717
+ await this.deliverySystem?.stop();
1718
+ });
1489
1719
 
1490
- // Shut down the delivery queue
1491
- if (this.deliveryQueue) {
1492
- await this.deliveryQueue.shutdown();
1493
- logger.log('info', 'Email delivery queue shut down');
1494
- }
1720
+ while (this.outboundWorkFlights.size > 0) {
1721
+ await Promise.allSettled([...this.outboundWorkFlights]);
1722
+ }
1723
+ while (this.outboundAuthenticationFlights.size > 0) {
1724
+ await Promise.allSettled([...this.outboundAuthenticationFlights]);
1725
+ }
1495
1726
 
1496
- this.bounceManager.stop();
1497
- logger.log('info', 'Bounce manager stopped');
1727
+ await runStep('delivery-queue shutdown failed', async () => {
1728
+ await this.deliveryQueue?.shutdown();
1729
+ });
1498
1730
 
1499
- this.rateLimiter.stop();
1500
- logger.log('info', 'Rate limiter stopped');
1731
+ await runStep('bounce-manager shutdown failed', () => this.bounceManager.stop());
1732
+ await runStep('rate-limiter shutdown failed', () => this.rateLimiter.stop());
1733
+ await runBestEffortStep(
1734
+ 'SMTP pool shutdown skipped or failed',
1735
+ () => this.rustBridge.closeSmtpPool(),
1736
+ );
1501
1737
 
1502
- // Close all Rust SMTP client connection pools
1503
- try {
1504
- await this.rustBridge.closeSmtpPool();
1505
- } catch {
1506
- // Bridge may already be stopped
1507
- }
1738
+ await runStep('Rust bridge shutdown failed', async () => {
1739
+ this.unregisterBridgeEventHandlers();
1740
+ this.unregisterBridgeStateHandler();
1741
+ await this.rustBridge.stop();
1742
+ });
1508
1743
 
1509
- logger.log('info', 'UnifiedEmailServer stopped successfully');
1510
- await emitSafely(this, 'stopped');
1511
- } catch (error) {
1512
- logger.log('error', `Error stopping UnifiedEmailServer: ${getErrorMessage(error)}`);
1513
- throw error;
1744
+ await emitSafely(this, 'stopped');
1745
+ if (errors.length > 0) {
1746
+ throw new AggregateError(errors, 'UnifiedEmailServer shutdown completed with errors');
1514
1747
  }
1748
+ logger.log('info', 'UnifiedEmailServer stopped successfully');
1515
1749
  }
1516
1750
 
1517
1751
  // -----------------------------------------------------------------------
@@ -3663,6 +3897,15 @@ export class UnifiedEmailServer extends EventEmitter {
3663
3897
  /**
3664
3898
  * Send an email through the delivery system
3665
3899
  */
3900
+ public async enqueueRawEmail(
3901
+ rawMessage: Buffer,
3902
+ envelope: IRawSmtpEnvelope,
3903
+ idempotencyKey: string,
3904
+ ): Promise<string> {
3905
+ this.assertAcceptingOutboundWork();
3906
+ return this.deliveryQueue[kQueueEnqueueRaw]({ rawMessage, envelope, idempotencyKey });
3907
+ }
3908
+
3666
3909
  public async sendEmail(
3667
3910
  email: Email,
3668
3911
  mode: EmailProcessingMode = 'mta',
@@ -3673,6 +3916,7 @@ export class UnifiedEmailServer extends EventEmitter {
3673
3916
  isTransactional?: boolean;
3674
3917
  }
3675
3918
  ): Promise<string> {
3919
+ this.assertAcceptingOutboundWork();
3676
3920
  logger.log('info', `Sending email: ${email.subject} to ${email.to.join(', ')}`);
3677
3921
 
3678
3922
  try {
@@ -0,0 +1,6 @@
1
+ export const kSendOutboundEmailInternal: unique symbol = Symbol(
2
+ 'smartmta.server.sendOutboundEmailInternal',
3
+ );
4
+ export const kSendRawOutboundEmailInternal: unique symbol = Symbol(
5
+ 'smartmta.server.sendRawOutboundEmailInternal',
6
+ );
@@ -446,8 +446,14 @@ interface ISmtpSendRawOptions {
446
446
  envelopeFrom: string;
447
447
  envelopeTo: string[];
448
448
  rawMessageBase64: string;
449
+ dkim: { domain: string; selector: string; privateKey: string; keyType?: string };
450
+ connectionTimeoutSecs?: number;
451
+ socketTimeoutSecs?: number;
449
452
  poolKey?: string;
453
+ maxPoolConnections?: number;
454
+ tlsOpportunistic?: boolean;
450
455
  disablePooling?: boolean;
456
+ outboundAuthenticationCaptureId?: string;
451
457
  }
452
458
 
453
459
  interface ISmtpVerifyOptions {