@push.rocks/smartmta 9.3.0 → 9.4.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 +10 -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/mail/delivery/classes.delivery.queue.d.ts +2 -0
- package/dist_ts/mail/delivery/classes.delivery.queue.js +35 -1
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +4 -0
- package/dist_ts/mail/delivery/classes.delivery.system.js +57 -1
- package/dist_ts/mail/delivery/interfaces.d.ts +2 -1
- package/dist_ts/mail/delivery/interfaces.js +1 -1
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +7 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +100 -3
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +41 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +11 -1
- package/package.json +2 -2
- package/readme.md +17 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/delivery/classes.delivery.queue.ts +45 -0
- package/ts/mail/delivery/classes.delivery.system.ts +82 -0
- package/ts/mail/delivery/interfaces.ts +2 -0
- package/ts/mail/routing/classes.unified.email.server.ts +119 -2
- package/ts/security/classes.rustsecuritybridge.ts +68 -0
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
} from '../../security/index.js';
|
|
10
10
|
import { DKIMCreator } from '../security/classes.dkimcreator.js';
|
|
11
11
|
import type { IStorageManager } from '../interfaces.storage.js';
|
|
12
|
-
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
12
|
+
import { RustSecurityBridge, SmtpDeliveryError } from '../../security/classes.rustsecuritybridge.js';
|
|
13
13
|
import type {
|
|
14
|
+
IOutboundAuthenticationCheck,
|
|
15
|
+
IOutboundAuthenticationHandle,
|
|
14
16
|
IAuthRequestEvent,
|
|
15
17
|
IEmailReceivedEvent,
|
|
16
18
|
IEmailSecurityResult,
|
|
@@ -258,6 +260,12 @@ export interface IOutboundConnectionProxyContext {
|
|
|
258
260
|
senderDomain?: string;
|
|
259
261
|
}
|
|
260
262
|
|
|
263
|
+
export interface IOutboundConnectionSourceEvidence {
|
|
264
|
+
sourceIp: string;
|
|
265
|
+
addressFamily: 'ipv4' | 'ipv6';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
261
269
|
export interface IOutboundConnectionProxy {
|
|
262
270
|
connectHost: string;
|
|
263
271
|
connectPort: number;
|
|
@@ -265,6 +273,8 @@ export interface IOutboundConnectionProxy {
|
|
|
265
273
|
/** Per-connection EHLO identity override (e.g. the selected egress edge's PTR hostname). */
|
|
266
274
|
ehloHostname?: string;
|
|
267
275
|
close?: () => Promise<void> | void;
|
|
276
|
+
/** Actual source identity observed after the proxied connection is established. */
|
|
277
|
+
sourceEvidence?: Promise<IOutboundConnectionSourceEvidence | null>;
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
export interface IOutboundTopologyIdentity {
|
|
@@ -937,8 +947,28 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
937
947
|
senderDomain,
|
|
938
948
|
});
|
|
939
949
|
|
|
950
|
+
const captureId = proxy?.sourceEvidence ? plugins.uuid.v4() : undefined;
|
|
951
|
+
const attachAuthenticationHandle = <T extends ISmtpSendResult | SmtpDeliveryError>(valueArg: T): T => {
|
|
952
|
+
if (!captureId || !proxy?.sourceEvidence) return valueArg;
|
|
953
|
+
const handle = this.createOutboundAuthenticationHandle({
|
|
954
|
+
captureId,
|
|
955
|
+
sourceEvidence: proxy.sourceEvidence,
|
|
956
|
+
heloDomain: proxy.ehloHostname || this.getOutboundHostname(),
|
|
957
|
+
receiverHostname: host,
|
|
958
|
+
envelopeFrom: email.from,
|
|
959
|
+
dkimDomain: dkim?.domain,
|
|
960
|
+
dkimSelector: dkim?.selector,
|
|
961
|
+
});
|
|
962
|
+
Object.defineProperty(valueArg, 'outboundAuthenticationHandle', {
|
|
963
|
+
value: handle,
|
|
964
|
+
enumerable: false,
|
|
965
|
+
configurable: true,
|
|
966
|
+
});
|
|
967
|
+
return valueArg;
|
|
968
|
+
};
|
|
969
|
+
|
|
940
970
|
try {
|
|
941
|
-
|
|
971
|
+
const result = await this.rustBridge.sendOutboundEmail({
|
|
942
972
|
host,
|
|
943
973
|
port,
|
|
944
974
|
...(proxy ? { connectHost: proxy.connectHost, connectPort: proxy.connectPort } : {}),
|
|
@@ -953,12 +983,99 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
953
983
|
maxPoolConnections: this.options.outbound?.maxConnections || 10,
|
|
954
984
|
tlsOpportunistic,
|
|
955
985
|
...(proxy ? { disablePooling: true } : {}),
|
|
986
|
+
...(captureId ? { outboundAuthenticationCaptureId: captureId } : {}),
|
|
956
987
|
});
|
|
988
|
+
return attachAuthenticationHandle(result);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
throw attachAuthenticationHandle(SmtpDeliveryError.from(error));
|
|
957
991
|
} finally {
|
|
958
992
|
await proxy?.close?.();
|
|
959
993
|
}
|
|
960
994
|
}
|
|
961
995
|
|
|
996
|
+
private createOutboundAuthenticationHandle(
|
|
997
|
+
optionsArg: {
|
|
998
|
+
captureId: string;
|
|
999
|
+
sourceEvidence: Promise<IOutboundConnectionSourceEvidence | null>;
|
|
1000
|
+
heloDomain: string;
|
|
1001
|
+
receiverHostname: string;
|
|
1002
|
+
envelopeFrom: string;
|
|
1003
|
+
dkimDomain?: string;
|
|
1004
|
+
dkimSelector?: string;
|
|
1005
|
+
},
|
|
1006
|
+
): IOutboundAuthenticationHandle {
|
|
1007
|
+
const pending: IOutboundAuthenticationCheck = {
|
|
1008
|
+
state: 'pending',
|
|
1009
|
+
overall: 'indeterminate',
|
|
1010
|
+
checkedAt: new Date().toISOString(),
|
|
1011
|
+
};
|
|
1012
|
+
const indeterminate = (errorArg: string): IOutboundAuthenticationCheck => ({
|
|
1013
|
+
state: 'indeterminate',
|
|
1014
|
+
overall: 'indeterminate',
|
|
1015
|
+
checkedAt: new Date().toISOString(),
|
|
1016
|
+
error: errorArg,
|
|
1017
|
+
});
|
|
1018
|
+
const result = (async (): Promise<IOutboundAuthenticationCheck> => {
|
|
1019
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1020
|
+
try {
|
|
1021
|
+
const sourceEvidence = await new Promise<IOutboundConnectionSourceEvidence | null>((resolve, reject) => {
|
|
1022
|
+
timeout = setTimeout(() => {
|
|
1023
|
+
reject(new Error('Timed out waiting for the actual outbound source identity'));
|
|
1024
|
+
}, 40_000);
|
|
1025
|
+
timeout.unref?.();
|
|
1026
|
+
optionsArg.sourceEvidence.then(resolve, reject);
|
|
1027
|
+
});
|
|
1028
|
+
if (!sourceEvidence) {
|
|
1029
|
+
await this.rustBridge.discardCapturedOutboundEmail(optionsArg.captureId).catch(() => undefined);
|
|
1030
|
+
return indeterminate('The outbound proxy did not provide source identity evidence');
|
|
1031
|
+
}
|
|
1032
|
+
const verification = await this.rustBridge.verifyCapturedOutboundEmail({
|
|
1033
|
+
captureId: optionsArg.captureId,
|
|
1034
|
+
ip: sourceEvidence.sourceIp,
|
|
1035
|
+
heloDomain: optionsArg.heloDomain,
|
|
1036
|
+
hostname: optionsArg.receiverHostname,
|
|
1037
|
+
mailFrom: optionsArg.envelopeFrom,
|
|
1038
|
+
});
|
|
1039
|
+
if (verification.state !== 'completed' || !verification.result) {
|
|
1040
|
+
return indeterminate(verification.error || 'The outbound authentication self-check was not completed');
|
|
1041
|
+
}
|
|
1042
|
+
const security = verification.result;
|
|
1043
|
+
const dkimPass = !optionsArg.dkimDomain || security.dkim.some((resultArg) => (
|
|
1044
|
+
resultArg.is_valid
|
|
1045
|
+
&& resultArg.domain?.toLowerCase() === optionsArg.dkimDomain?.toLowerCase()
|
|
1046
|
+
&& (!optionsArg.dkimSelector || resultArg.selector === optionsArg.dkimSelector)
|
|
1047
|
+
));
|
|
1048
|
+
const spfPass = security.spf?.result.toLowerCase() === 'pass';
|
|
1049
|
+
const dmarcPass = security.dmarc?.passed === true;
|
|
1050
|
+
return {
|
|
1051
|
+
state: 'completed',
|
|
1052
|
+
overall: dkimPass && spfPass && dmarcPass ? 'pass' : 'fail',
|
|
1053
|
+
checkedAt: new Date().toISOString(),
|
|
1054
|
+
signedMessageSha256: verification.signedMessageSha256,
|
|
1055
|
+
identity: {
|
|
1056
|
+
sourceIp: sourceEvidence.sourceIp,
|
|
1057
|
+
addressFamily: sourceEvidence.addressFamily,
|
|
1058
|
+
heloDomain: optionsArg.heloDomain,
|
|
1059
|
+
receiverHostname: optionsArg.receiverHostname,
|
|
1060
|
+
envelopeFrom: optionsArg.envelopeFrom,
|
|
1061
|
+
...(optionsArg.dkimDomain ? { dkimDomain: optionsArg.dkimDomain } : {}),
|
|
1062
|
+
...(optionsArg.dkimSelector ? { dkimSelector: optionsArg.dkimSelector } : {}),
|
|
1063
|
+
},
|
|
1064
|
+
dkim: security.dkim,
|
|
1065
|
+
spf: security.spf,
|
|
1066
|
+
dmarc: security.dmarc,
|
|
1067
|
+
};
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
await this.rustBridge.discardCapturedOutboundEmail(optionsArg.captureId).catch(() => undefined);
|
|
1070
|
+
return indeterminate(getErrorMessage(error).slice(0, 512));
|
|
1071
|
+
} finally {
|
|
1072
|
+
if (timeout) clearTimeout(timeout);
|
|
1073
|
+
}
|
|
1074
|
+
})();
|
|
1075
|
+
return { pending, result };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
|
|
962
1079
|
/**
|
|
963
1080
|
* Start the unified email server
|
|
964
1081
|
*/
|
|
@@ -242,6 +242,7 @@ export interface ISmtpDeliveryErrorDetails {
|
|
|
242
242
|
recipients?: string[];
|
|
243
243
|
recipientDomain?: string;
|
|
244
244
|
recipientResults?: ISmtpRecipientResult[];
|
|
245
|
+
outboundAuthenticationHandle?: IOutboundAuthenticationHandle;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
interface ISmtpRecipientResult {
|
|
@@ -259,6 +260,7 @@ export class SmtpDeliveryError extends Error implements ISmtpDeliveryErrorDetail
|
|
|
259
260
|
public readonly recipients: string[];
|
|
260
261
|
public readonly recipientDomain?: string;
|
|
261
262
|
public readonly recipientResults: ISmtpRecipientResult[];
|
|
263
|
+
public readonly outboundAuthenticationHandle?: IOutboundAuthenticationHandle;
|
|
262
264
|
|
|
263
265
|
constructor(details: ISmtpDeliveryErrorDetails, cause?: unknown) {
|
|
264
266
|
const transactionLog = normalizeSmtpTranscript(details.transactionLog);
|
|
@@ -277,6 +279,7 @@ export class SmtpDeliveryError extends Error implements ISmtpDeliveryErrorDetail
|
|
|
277
279
|
this.recipients = details.recipients || [];
|
|
278
280
|
this.recipientDomain = details.recipientDomain;
|
|
279
281
|
this.recipientResults = (details.recipientResults || []).map((result) => ({ ...result }));
|
|
282
|
+
this.outboundAuthenticationHandle = details.outboundAuthenticationHandle;
|
|
280
283
|
if (cause !== undefined && !authFailure) {
|
|
281
284
|
(this as Error & { cause?: unknown }).cause = cause;
|
|
282
285
|
}
|
|
@@ -370,6 +373,31 @@ interface IOutboundEmail {
|
|
|
370
373
|
}>;
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
export interface IOutboundAuthenticationCheck {
|
|
377
|
+
state: 'pending' | 'completed' | 'indeterminate' | 'not-evaluated';
|
|
378
|
+
overall: 'pass' | 'fail' | 'indeterminate';
|
|
379
|
+
checkedAt: string;
|
|
380
|
+
signedMessageSha256?: string;
|
|
381
|
+
error?: string;
|
|
382
|
+
identity?: {
|
|
383
|
+
sourceIp: string;
|
|
384
|
+
addressFamily: 'ipv4' | 'ipv6';
|
|
385
|
+
heloDomain: string;
|
|
386
|
+
receiverHostname: string;
|
|
387
|
+
envelopeFrom: string;
|
|
388
|
+
dkimDomain?: string;
|
|
389
|
+
dkimSelector?: string;
|
|
390
|
+
};
|
|
391
|
+
dkim?: IDkimVerificationResult[];
|
|
392
|
+
spf?: ISpfResult | null;
|
|
393
|
+
dmarc?: IDmarcResult | null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface IOutboundAuthenticationHandle {
|
|
397
|
+
pending: IOutboundAuthenticationCheck;
|
|
398
|
+
result: Promise<IOutboundAuthenticationCheck>;
|
|
399
|
+
}
|
|
400
|
+
|
|
373
401
|
interface ISmtpSendResult {
|
|
374
402
|
accepted: string[];
|
|
375
403
|
rejected: string[];
|
|
@@ -378,6 +406,7 @@ interface ISmtpSendResult {
|
|
|
378
406
|
response: string;
|
|
379
407
|
envelope: { from: string; to: string[] };
|
|
380
408
|
transactionLog: ISmtpTranscriptEntry[];
|
|
409
|
+
outboundAuthenticationHandle?: IOutboundAuthenticationHandle;
|
|
381
410
|
}
|
|
382
411
|
|
|
383
412
|
interface ISmtpSendOptions {
|
|
@@ -396,6 +425,14 @@ interface ISmtpSendOptions {
|
|
|
396
425
|
maxPoolConnections?: number;
|
|
397
426
|
tlsOpportunistic?: boolean;
|
|
398
427
|
disablePooling?: boolean;
|
|
428
|
+
outboundAuthenticationCaptureId?: string;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
interface IVerifyCapturedOutboundEmailResult {
|
|
432
|
+
state: 'completed' | 'indeterminate';
|
|
433
|
+
signedMessageSha256?: string;
|
|
434
|
+
error?: string;
|
|
435
|
+
result?: IEmailSecurityResult;
|
|
399
436
|
}
|
|
400
437
|
|
|
401
438
|
interface ISmtpSendRawOptions {
|
|
@@ -634,6 +671,20 @@ type TMailerCommands = {
|
|
|
634
671
|
params: ISmtpSendRawOptions;
|
|
635
672
|
result: ISmtpSendResult;
|
|
636
673
|
};
|
|
674
|
+
verifyCapturedOutboundEmail: {
|
|
675
|
+
params: {
|
|
676
|
+
captureId: string;
|
|
677
|
+
ip: string;
|
|
678
|
+
heloDomain: string;
|
|
679
|
+
hostname: string;
|
|
680
|
+
mailFrom: string;
|
|
681
|
+
};
|
|
682
|
+
result: IVerifyCapturedOutboundEmailResult;
|
|
683
|
+
};
|
|
684
|
+
discardCapturedOutboundEmail: {
|
|
685
|
+
params: { captureId: string };
|
|
686
|
+
result: { discarded: boolean };
|
|
687
|
+
};
|
|
637
688
|
verifySmtpConnection: {
|
|
638
689
|
params: ISmtpVerifyOptions;
|
|
639
690
|
result: ISmtpVerifyResult;
|
|
@@ -1189,6 +1240,7 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
1189
1240
|
this.ensureRunning();
|
|
1190
1241
|
try {
|
|
1191
1242
|
const result = await this.bridge.sendCommand('sendRawEmail', opts);
|
|
1243
|
+
|
|
1192
1244
|
return {
|
|
1193
1245
|
...result,
|
|
1194
1246
|
transactionLog: normalizeSmtpTranscript(result.transactionLog),
|
|
@@ -1198,6 +1250,22 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
1198
1250
|
}
|
|
1199
1251
|
}
|
|
1200
1252
|
|
|
1253
|
+
public async verifyCapturedOutboundEmail(opts: {
|
|
1254
|
+
captureId: string;
|
|
1255
|
+
ip: string;
|
|
1256
|
+
heloDomain: string;
|
|
1257
|
+
hostname: string;
|
|
1258
|
+
mailFrom: string;
|
|
1259
|
+
}): Promise<IVerifyCapturedOutboundEmailResult> {
|
|
1260
|
+
this.ensureRunning();
|
|
1261
|
+
return this.bridge.sendCommand('verifyCapturedOutboundEmail', opts);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
public async discardCapturedOutboundEmail(captureId: string): Promise<void> {
|
|
1265
|
+
this.ensureRunning();
|
|
1266
|
+
await this.bridge.sendCommand('discardCapturedOutboundEmail', { captureId });
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1201
1269
|
/** Verify connectivity to an SMTP server. */
|
|
1202
1270
|
public async verifySmtpConnection(opts: ISmtpVerifyOptions): Promise<ISmtpVerifyResult> {
|
|
1203
1271
|
this.ensureRunning();
|