@push.rocks/smartmta 9.4.1 → 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.
- package/changelog.md +14 -0
- package/dist_rust/mailer-bin_linux_amd64 +0 -0
- package/dist_rust/mailer-bin_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +63 -2
- package/dist_ts/mail/delivery/classes.delivery.queue.js +749 -193
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +6 -0
- package/dist_ts/mail/delivery/classes.delivery.system.js +146 -62
- package/dist_ts/mail/delivery/functions.raw-email.d.ts +9 -0
- package/dist_ts/mail/delivery/functions.raw-email.js +85 -0
- package/dist_ts/mail/delivery/symbols.delivery.queue.d.ts +6 -0
- package/dist_ts/mail/delivery/symbols.delivery.queue.js +7 -0
- package/dist_ts/mail/index.d.ts +1 -0
- package/dist_ts/mail/index.js +1 -1
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +40 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +222 -76
- package/dist_ts/mail/routing/symbols.unified.email.server.d.ts +2 -0
- package/dist_ts/mail/routing/symbols.unified.email.server.js +3 -0
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +11 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +1 -1
- package/package.json +2 -2
- package/readme.hints.md +8 -0
- package/readme.md +34 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -0
- package/ts/mail/delivery/classes.delivery.queue.ts +923 -226
- package/ts/mail/delivery/classes.delivery.system.ts +192 -80
- package/ts/mail/delivery/functions.raw-email.ts +111 -0
- package/ts/mail/delivery/symbols.delivery.queue.ts +6 -0
- package/ts/mail/index.ts +1 -0
- package/ts/mail/routing/classes.unified.email.server.ts +329 -85
- package/ts/mail/routing/symbols.unified.email.server.ts +6 -0
- package/ts/security/classes.rustsecuritybridge.ts +6 -0
|
@@ -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
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
-
|
|
1497
|
-
|
|
1727
|
+
await runStep('delivery-queue shutdown failed', async () => {
|
|
1728
|
+
await this.deliveryQueue?.shutdown();
|
|
1729
|
+
});
|
|
1498
1730
|
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
}
|
|
1738
|
+
await runStep('Rust bridge shutdown failed', async () => {
|
|
1739
|
+
this.unregisterBridgeEventHandlers();
|
|
1740
|
+
this.unregisterBridgeStateHandler();
|
|
1741
|
+
await this.rustBridge.stop();
|
|
1742
|
+
});
|
|
1508
1743
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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 {
|
|
@@ -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 {
|