@push.rocks/smartmta 9.2.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 +42 -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 +50 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +201 -9
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +43 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +11 -1
- package/package.json +2 -2
- package/readme.md +51 -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 +243 -11
- package/ts/security/classes.rustsecuritybridge.ts +70 -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 {
|
|
@@ -334,6 +344,19 @@ export interface IUnifiedEmailServerOptions {
|
|
|
334
344
|
recipientValidation?: boolean;
|
|
335
345
|
/** Listener port that should use implicit TLS. Defaults to 465 when present. */
|
|
336
346
|
securePort?: number;
|
|
347
|
+
/**
|
|
348
|
+
* Listener ports whose inbound transport is already encrypted by a trusted
|
|
349
|
+
* upstream TLS terminator (an edge proxy that terminates TLS and forwards
|
|
350
|
+
* the decrypted stream here). Sessions on these ports count as encrypted:
|
|
351
|
+
* AUTH is offered even though this process performed no handshake, and
|
|
352
|
+
* STARTTLS is not advertised.
|
|
353
|
+
*
|
|
354
|
+
* Only honoured together with `proxyProtocol.required` — the exact-peer
|
|
355
|
+
* PROXY check is what proves the connection came from the terminator.
|
|
356
|
+
* Startup fails when a listed port is not bound, is the implicit-TLS
|
|
357
|
+
* `securePort`, or PROXY protocol is not required.
|
|
358
|
+
*/
|
|
359
|
+
tlsTerminatedPorts?: number[];
|
|
337
360
|
/** Timeout for RCPT policy callbacks. Defaults to 5000 ms. */
|
|
338
361
|
recipientValidationTimeoutMs?: number;
|
|
339
362
|
/** Timeout for message acceptance callbacks. Defaults to 30000 ms. */
|
|
@@ -924,8 +947,28 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
924
947
|
senderDomain,
|
|
925
948
|
});
|
|
926
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
|
+
|
|
927
970
|
try {
|
|
928
|
-
|
|
971
|
+
const result = await this.rustBridge.sendOutboundEmail({
|
|
929
972
|
host,
|
|
930
973
|
port,
|
|
931
974
|
...(proxy ? { connectHost: proxy.connectHost, connectPort: proxy.connectPort } : {}),
|
|
@@ -940,12 +983,99 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
940
983
|
maxPoolConnections: this.options.outbound?.maxConnections || 10,
|
|
941
984
|
tlsOpportunistic,
|
|
942
985
|
...(proxy ? { disablePooling: true } : {}),
|
|
986
|
+
...(captureId ? { outboundAuthenticationCaptureId: captureId } : {}),
|
|
943
987
|
});
|
|
988
|
+
return attachAuthenticationHandle(result);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
throw attachAuthenticationHandle(SmtpDeliveryError.from(error));
|
|
944
991
|
} finally {
|
|
945
992
|
await proxy?.close?.();
|
|
946
993
|
}
|
|
947
994
|
}
|
|
948
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
|
+
|
|
949
1079
|
/**
|
|
950
1080
|
* Start the unified email server
|
|
951
1081
|
*/
|
|
@@ -1182,6 +1312,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1182
1312
|
? configuredSecurePort
|
|
1183
1313
|
: (this.options.ports as number[]).find(p => p === 465);
|
|
1184
1314
|
const smtpPorts = (this.options.ports as number[]).filter(p => p !== securePort);
|
|
1315
|
+
const tlsTerminatedPorts = this.resolveTlsTerminatedPorts(smtpPorts, securePort);
|
|
1185
1316
|
|
|
1186
1317
|
const started = await this.rustBridge.startSmtpServer({
|
|
1187
1318
|
hostname: this.getAdvertisedHostname(),
|
|
@@ -1189,6 +1320,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1189
1320
|
securePort: securePort,
|
|
1190
1321
|
tlsCertPem,
|
|
1191
1322
|
tlsKeyPem,
|
|
1323
|
+
...(tlsTerminatedPorts.length > 0 ? { tlsTerminatedPorts } : {}),
|
|
1192
1324
|
maxMessageSize: this.options.maxMessageSize || 10 * 1024 * 1024,
|
|
1193
1325
|
maxConnections: this.options.maxConnections || this.options.maxClients || 100,
|
|
1194
1326
|
maxRecipients: 100,
|
|
@@ -1222,6 +1354,44 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1222
1354
|
logger.log('info', `Rust SMTP server listening on ports: ${smtpPorts.join(', ')}${securePort ? ` + ${securePort} (TLS)` : ''}`);
|
|
1223
1355
|
}
|
|
1224
1356
|
|
|
1357
|
+
/**
|
|
1358
|
+
* Validate and resolve the ports whose transport an upstream proxy already
|
|
1359
|
+
* encrypted. Misconfiguration here would let a peer claim an encrypted
|
|
1360
|
+
* channel and be offered AUTH in cleartext, so every case fails startup
|
|
1361
|
+
* rather than degrading silently.
|
|
1362
|
+
*/
|
|
1363
|
+
private resolveTlsTerminatedPorts(smtpPorts: number[], securePort: number | undefined): number[] {
|
|
1364
|
+
const configured = this.options.smtp?.tlsTerminatedPorts;
|
|
1365
|
+
if (!configured || configured.length === 0) return [];
|
|
1366
|
+
if (!this.options.smtp?.proxyProtocol?.required) {
|
|
1367
|
+
throw new Error(
|
|
1368
|
+
'SMTP tlsTerminatedPorts requires smtp.proxyProtocol.required so the upstream TLS terminator is proven by the trusted-peer PROXY check',
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
if (!this.options.smtp.proxyProtocol.trustedIps?.length) {
|
|
1372
|
+
throw new Error(
|
|
1373
|
+
'SMTP tlsTerminatedPorts requires at least one smtp.proxyProtocol.trustedIps entry',
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
const resolved: number[] = [];
|
|
1377
|
+
for (const port of configured) {
|
|
1378
|
+
if (securePort !== undefined && port === securePort) {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`SMTP tlsTerminatedPorts names port ${port}, which is already the implicit-TLS securePort terminated by this server`,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
if (!smtpPorts.includes(port)) {
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
`SMTP tlsTerminatedPorts names port ${port}, which is not one of the configured SMTP listener ports`,
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
if (!resolved.includes(port)) {
|
|
1389
|
+
resolved.push(port);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return resolved;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1225
1395
|
/** Whether the current options require SMTP AUTH support on the listener. */
|
|
1226
1396
|
private isAuthConfigured(options: IUnifiedEmailServerOptions): boolean {
|
|
1227
1397
|
return !!options.auth?.required
|
|
@@ -1452,10 +1622,18 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1452
1622
|
text: '',
|
|
1453
1623
|
});
|
|
1454
1624
|
const session = sessionArg || this.buildPolicySession(currentRecipientContext);
|
|
1455
|
-
const
|
|
1625
|
+
const matchedRoute = await this.emailRouter.evaluateRoutes({ email, session });
|
|
1456
1626
|
const recipientDomain = this.getAddressDomain(context.rcptTo);
|
|
1457
1627
|
const isLocalRecipient = !!recipientDomain && this.domainRegistry.isDomainRegistered(recipientDomain);
|
|
1458
1628
|
const localDomain = isLocalRecipient ? recipientDomain! : null;
|
|
1629
|
+
// A submission-permission route must never claim a recipient this server
|
|
1630
|
+
// itself terminates: its transport is DNS-MX relay, which for such a
|
|
1631
|
+
// recipient resolves straight back to this listener. Authentication grants
|
|
1632
|
+
// permission to relay, it does not turn a local delivery into a remote one.
|
|
1633
|
+
const route = matchedRoute
|
|
1634
|
+
&& this.isSubmissionPermissionRouteForLocalRecipient(matchedRoute, recipientDomain, isLocalRecipient)
|
|
1635
|
+
? null
|
|
1636
|
+
: matchedRoute;
|
|
1459
1637
|
|
|
1460
1638
|
if (route) {
|
|
1461
1639
|
if (route.action.type === 'reject') {
|
|
@@ -1585,6 +1763,49 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1585
1763
|
};
|
|
1586
1764
|
}
|
|
1587
1765
|
|
|
1766
|
+
/**
|
|
1767
|
+
* Whether a matched route is a pure submission-permission route that must
|
|
1768
|
+
* not claim a recipient whose inbound mail this server terminates.
|
|
1769
|
+
*
|
|
1770
|
+
* A route qualifies only when ALL of the following hold:
|
|
1771
|
+
* - the recipient domain is registered here AND its inbound policy stores
|
|
1772
|
+
* mail locally, i.e. this server really is the destination. A domain
|
|
1773
|
+
* registered for outbound purposes only (`inboundPolicy` absent, or
|
|
1774
|
+
* `defaultAction: 'reject'` for send-only domains) keeps relaying, because
|
|
1775
|
+
* its real MX legitimately lives elsewhere;
|
|
1776
|
+
* - the action is `process`, the only action whose transport is DNS-MX
|
|
1777
|
+
* relay. `store`, `deliver`, `forward` and `reject` all express a
|
|
1778
|
+
* deliberate disposition for the recipient and always win;
|
|
1779
|
+
* - the route names no recipient scope. `match.recipients` is the only way a
|
|
1780
|
+
* route states which recipients it is for, and a pattern that matches every
|
|
1781
|
+
* address states nothing. Recipient-scoped `process` routes keep winning —
|
|
1782
|
+
* consumers use them for local addresses handled by their own acceptance
|
|
1783
|
+
* pipeline. Encoding recipient intent in `match.headers` is not supported
|
|
1784
|
+
* for this purpose.
|
|
1785
|
+
*/
|
|
1786
|
+
private isSubmissionPermissionRouteForLocalRecipient(
|
|
1787
|
+
route: IEmailRoute,
|
|
1788
|
+
recipientDomain: string | undefined,
|
|
1789
|
+
isLocalRecipient: boolean,
|
|
1790
|
+
): boolean {
|
|
1791
|
+
if (!isLocalRecipient || !recipientDomain) return false;
|
|
1792
|
+
if (route.action.type !== 'process') return false;
|
|
1793
|
+
if (this.domainRegistry.getDomainConfig(recipientDomain)?.inboundPolicy?.defaultAction !== 'store') {
|
|
1794
|
+
return false;
|
|
1795
|
+
}
|
|
1796
|
+
return !this.routeNamesRecipientScope(route.match.recipients);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/** Whether a recipient pattern list narrows the route to specific recipients. */
|
|
1800
|
+
private routeNamesRecipientScope(patterns: string | string[] | undefined): boolean {
|
|
1801
|
+
if (patterns === undefined) return false;
|
|
1802
|
+
const patternArray = Array.isArray(patterns) ? patterns : [patterns];
|
|
1803
|
+
return patternArray.some((pattern) => {
|
|
1804
|
+
const normalized = String(pattern || '').trim().toLowerCase();
|
|
1805
|
+
return normalized.length > 0 && normalized !== '*' && normalized !== '*@*';
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1588
1809
|
private getRejectionReasonForResolution(
|
|
1589
1810
|
resolution: IRecipientPolicyResolution,
|
|
1590
1811
|
): TRecipientRejectionReason {
|
|
@@ -3321,6 +3542,16 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3321
3542
|
(!this.options.ports ||
|
|
3322
3543
|
JSON.stringify(options.ports) !== JSON.stringify(this.options.ports));
|
|
3323
3544
|
const authEnabledBefore = this.isAuthConfigured(this.options);
|
|
3545
|
+
// TLS material and the upstream-terminated port list are listener-level
|
|
3546
|
+
// Rust configuration too. Without a restart a renewed certificate or a
|
|
3547
|
+
// changed port list would be merged into options and silently never take
|
|
3548
|
+
// effect on the running listener.
|
|
3549
|
+
const tlsMaterialChanged = options.tls !== undefined
|
|
3550
|
+
&& (options.tls.certPem !== this.options.tls?.certPem
|
|
3551
|
+
|| options.tls.keyPem !== this.options.tls?.keyPem);
|
|
3552
|
+
const tlsTerminatedPortsChanged = options.smtp !== undefined
|
|
3553
|
+
&& JSON.stringify(options.smtp.tlsTerminatedPorts ?? [])
|
|
3554
|
+
!== JSON.stringify(this.options.smtp?.tlsTerminatedPorts ?? []);
|
|
3324
3555
|
|
|
3325
3556
|
if (portsChanged) {
|
|
3326
3557
|
this.stop().then(() => {
|
|
@@ -3361,15 +3592,16 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
3361
3592
|
// auth presence transitions (zero -> first account/user or reverse)
|
|
3362
3593
|
// the running listener must be restarted for AUTH to take effect
|
|
3363
3594
|
// without a full process restart.
|
|
3364
|
-
|
|
3365
|
-
options.auth
|
|
3366
|
-
&& this.smtpListenerActive
|
|
3367
|
-
&& !this.smtpListenerShutdown
|
|
3595
|
+
const listenerRestartReason = options.auth
|
|
3368
3596
|
&& this.isAuthConfigured(this.options) !== authEnabledBefore
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3597
|
+
? `SMTP AUTH ${this.isAuthConfigured(this.options) ? 'enabled' : 'disabled'} by configuration update`
|
|
3598
|
+
: tlsMaterialChanged
|
|
3599
|
+
? 'SMTP TLS material replaced by configuration update'
|
|
3600
|
+
: tlsTerminatedPortsChanged
|
|
3601
|
+
? 'SMTP upstream-terminated port list changed by configuration update'
|
|
3602
|
+
: undefined;
|
|
3603
|
+
if (listenerRestartReason && this.smtpListenerActive && !this.smtpListenerShutdown) {
|
|
3604
|
+
this.scheduleSmtpListenerRestart(listenerRestartReason);
|
|
3373
3605
|
}
|
|
3374
3606
|
}
|
|
3375
3607
|
}
|
|
@@ -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 {
|
|
@@ -448,6 +485,8 @@ interface ISmtpServerConfig {
|
|
|
448
485
|
securePort?: number;
|
|
449
486
|
tlsCertPem?: string;
|
|
450
487
|
tlsKeyPem?: string;
|
|
488
|
+
/** Ports whose transport a trusted upstream proxy already encrypted. */
|
|
489
|
+
tlsTerminatedPorts?: number[];
|
|
451
490
|
additionalTlsCerts?: Array<{ domains: string[]; certPem: string; keyPem: string }>;
|
|
452
491
|
maxMessageSize?: number;
|
|
453
492
|
maxConnections?: number;
|
|
@@ -632,6 +671,20 @@ type TMailerCommands = {
|
|
|
632
671
|
params: ISmtpSendRawOptions;
|
|
633
672
|
result: ISmtpSendResult;
|
|
634
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
|
+
};
|
|
635
688
|
verifySmtpConnection: {
|
|
636
689
|
params: ISmtpVerifyOptions;
|
|
637
690
|
result: ISmtpVerifyResult;
|
|
@@ -1187,6 +1240,7 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
1187
1240
|
this.ensureRunning();
|
|
1188
1241
|
try {
|
|
1189
1242
|
const result = await this.bridge.sendCommand('sendRawEmail', opts);
|
|
1243
|
+
|
|
1190
1244
|
return {
|
|
1191
1245
|
...result,
|
|
1192
1246
|
transactionLog: normalizeSmtpTranscript(result.transactionLog),
|
|
@@ -1196,6 +1250,22 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
1196
1250
|
}
|
|
1197
1251
|
}
|
|
1198
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
|
+
|
|
1199
1269
|
/** Verify connectivity to an SMTP server. */
|
|
1200
1270
|
public async verifySmtpConnection(opts: ISmtpVerifyOptions): Promise<ISmtpVerifyResult> {
|
|
1201
1271
|
this.ensureRunning();
|