@push.rocks/smartmta 9.1.1 → 9.3.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/routing/classes.unified.email.server.d.ts +43 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +102 -7
- package/dist_ts/mail/routing/functions.routing.validation.d.ts +22 -1
- package/dist_ts/mail/routing/functions.routing.validation.js +44 -1
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +2 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +1 -1
- package/package.json +1 -1
- package/readme.md +36 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/routing/classes.unified.email.server.ts +124 -9
- package/ts/mail/routing/functions.routing.validation.ts +53 -0
- package/ts/security/classes.rustsecuritybridge.ts +2 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-07-26 - 9.3.0
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- a submission-permission route no longer claims a recipient this server terminates (routing)
|
|
8
|
+
- Found empirically in a dcrouter deployment: the same message from the same sender to `admin@central.eu`, a domain that server hosts, was accepted and stored when submitted unauthenticated but permanently lost when submitted over authenticated SMTP minutes later. Log chain: `MTA: skipped MX mx.lossless.directory for central.eu: ... points back at this server because it exactly matches a configured listener identity` → `all MX hosts failed` → `Terminal delivery failure persisted for admin@central.eu`, all after the client had already been given a `250`.
|
|
9
|
+
- Root cause: `resolveRecipientPolicy` let ANY matched route win over the domain inbound policy. An authenticated submission route (`match: { authenticated: true, senders }`, `action: { type: 'process', allowRelay: true }`, no recipient scope) therefore claimed locally hosted recipients too, and `process` transports via DNS MX — which for such a recipient resolves straight back to this listener, where the self-MX loop guard terminates it permanently. The domain-policy `store` fallback was only reachable when no route matched at all, which is why the unauthenticated path worked.
|
|
10
|
+
- Resolution now falls through to the domain's inbound policy when the recipient domain is registered AND stores inbound mail locally (`inboundPolicy.defaultAction: 'store'`), the action is `process`, and the route names no recipient scope (`match.recipients` absent, empty, or only match-all patterns `*` / `*@*`).
|
|
11
|
+
- Deliberately narrow, so nothing that works today starts bouncing: recipient-scoped `process` routes still win (consumers deliver specific local addresses through their own acceptance pipeline that way), `store`/`deliver`/`forward`/`reject` always win, non-local recipients are untouched, and a domain registered for outbound identity only — no `inboundPolicy`, or the send-only `defaultAction: 'reject'` — keeps relaying because its real MX legitimately lives elsewhere. The requirement that the fall-through target be a `store` policy means the change can never turn a working relay into a `550`.
|
|
12
|
+
- The self-MX loop guard is unchanged; local recipients simply stop reaching it. Authenticated relay to remote domains is unchanged. Mixed envelopes now resolve per recipient: local recipients get `store` and remote recipients keep the submission route, so an envelope addressed to both is handled through the durable `hooks.onAcceptEnvelope` path with only the remote recipients dispatched to the queue.
|
|
13
|
+
- Encoding recipient intent in `match.headers` is explicitly not recognised for this purpose.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- AUTH is no longer advertised or accepted on an unencrypted session (smtp)
|
|
18
|
+
- Found in the same deployment: ports 25 and 587 advertised `AUTH PLAIN LOGIN SCRAM-SHA-256` while offering no `STARTTLS`, so credentials could be solicited over a cleartext channel. `build_capabilities` pushed the AUTH capability whenever auth was configured, ignoring `already_secure`, and `handle_auth`/`handle_auth_scram` only checked `config.auth_enabled`, never `session.secure`.
|
|
19
|
+
- `AUTH` is now advertised only on an encrypted transport, and an `AUTH` command on a cleartext session is refused with `538 5.7.11 Encryption required for requested authentication mechanism` — so a client that ignores the absent capability cannot send credentials in the clear either. Applies to `PLAIN`, `LOGIN` and `SCRAM-SHA-256`.
|
|
20
|
+
- BREAKING for deployments that relied on cleartext AUTH: a plain port with no TLS material now offers neither `STARTTLS` nor `AUTH`. Verify that submission ports actually receive `tls.certPem`/`tls.keyPem`.
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
- `smtp.tlsTerminatedPorts` for edge-terminated submission ports (smtp)
|
|
25
|
+
- Declares listener ports whose inbound transport a trusted upstream TLS terminator already encrypted, which is how a proxy-fronted deployment keeps implicit-TLS submission working under the new AUTH gate: the backend leg is plaintext, but the client's channel is encrypted. Sessions on such a port count as encrypted, report `secure: true` to hooks, and do not advertise `STARTTLS`.
|
|
26
|
+
- Because it is a trust assertion about the peer, it is only honoured together with `smtp.proxyProtocol.required` and a non-empty `trustedIps` — the exact-peer PROXY check is what proves the connection came from the terminator. `start()` throws when a listed port is not among the configured `ports`, is the implicit-TLS `securePort`, or PROXY protocol is not required. Validated on both the TypeScript and the Rust side.
|
|
27
|
+
- `updateOptions` now restarts the Rust SMTP listener when TLS material or `smtp.tlsTerminatedPorts` change, alongside the existing AUTH-presence transition (routing)
|
|
28
|
+
- Both are listener-level Rust configuration. Previously they were merged into the options and silently never took effect on the running listener, so a renewed certificate could not be applied without a full process restart.
|
|
29
|
+
|
|
30
|
+
### Known limitations (unchanged, documented)
|
|
31
|
+
|
|
32
|
+
- `tls.caPem`, `tls.minVersion` and `tls.ciphers` are accepted but not consumed by the Rust listener; only `certPem`/`keyPem` take effect.
|
|
33
|
+
- `auth.required` only turns AUTH availability on — it does not require authentication. A global require-auth flag is incoherent for port 25, which must accept anonymous inbound mail; relay permission is expressed per route via `allowRelay` plus local-recipient resolution. The option name remains misleading.
|
|
34
|
+
|
|
35
|
+
## 2026-07-26 - 9.2.0
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
- add SCRAM-SHA-256 verifier derivation helper (auth)
|
|
40
|
+
- `deriveScramVerifier(password, iterations = SCRAM_DEFAULT_ITERATIONS)` is the canonical embedder-facing RFC 5802 derivation (fresh random 16-byte salt), matching exactly what `UnifiedEmailServer` verifies at AUTH time; bounds are enforced at derivation.
|
|
41
|
+
- `SCRAM_DEFAULT_ITERATIONS` is 600,000 (OWASP-aligned for PBKDF2-HMAC-SHA256) — machine-generated high-entropy secrets gain nothing from high counts and callers may pass lower values within the accepted bounds.
|
|
42
|
+
- Covered by validation, reference-derivation, and end-to-end PLAIN authentication tests.
|
|
43
|
+
- Known follow-ups (not in this release): the private verify-side `computeScramCredentials` still duplicates the derivation formula and should converge onto the shared helper; the legacy plaintext-credential path also still derives SCRAM fields with a hardcoded 4096 iterations — one more reason for embedders to finish moving to stored verifiers.
|
|
44
|
+
|
|
3
45
|
## 2026-07-26 - 9.1.1
|
|
4
46
|
|
|
5
47
|
### Changes
|
|
Binary file
|
|
Binary file
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartmta',
|
|
6
|
-
version: '9.
|
|
6
|
+
version: '9.3.0',
|
|
7
7
|
description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxzQkFBc0I7SUFDNUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHlIQUF5SDtDQUN2SSxDQUFBIn0=
|
|
@@ -231,6 +231,19 @@ export interface IUnifiedEmailServerOptions {
|
|
|
231
231
|
recipientValidation?: boolean;
|
|
232
232
|
/** Listener port that should use implicit TLS. Defaults to 465 when present. */
|
|
233
233
|
securePort?: number;
|
|
234
|
+
/**
|
|
235
|
+
* Listener ports whose inbound transport is already encrypted by a trusted
|
|
236
|
+
* upstream TLS terminator (an edge proxy that terminates TLS and forwards
|
|
237
|
+
* the decrypted stream here). Sessions on these ports count as encrypted:
|
|
238
|
+
* AUTH is offered even though this process performed no handshake, and
|
|
239
|
+
* STARTTLS is not advertised.
|
|
240
|
+
*
|
|
241
|
+
* Only honoured together with `proxyProtocol.required` — the exact-peer
|
|
242
|
+
* PROXY check is what proves the connection came from the terminator.
|
|
243
|
+
* Startup fails when a listed port is not bound, is the implicit-TLS
|
|
244
|
+
* `securePort`, or PROXY protocol is not required.
|
|
245
|
+
*/
|
|
246
|
+
tlsTerminatedPorts?: number[];
|
|
234
247
|
/** Timeout for RCPT policy callbacks. Defaults to 5000 ms. */
|
|
235
248
|
recipientValidationTimeoutMs?: number;
|
|
236
249
|
/** Timeout for message acceptance callbacks. Defaults to 30000 ms. */
|
|
@@ -405,6 +418,13 @@ export declare class UnifiedEmailServer extends EventEmitter {
|
|
|
405
418
|
private unregisterBridgeStateHandler;
|
|
406
419
|
private unregisterBridgeEventHandlers;
|
|
407
420
|
private startSmtpServer;
|
|
421
|
+
/**
|
|
422
|
+
* Validate and resolve the ports whose transport an upstream proxy already
|
|
423
|
+
* encrypted. Misconfiguration here would let a peer claim an encrypted
|
|
424
|
+
* channel and be offered AUTH in cleartext, so every case fails startup
|
|
425
|
+
* rather than degrading silently.
|
|
426
|
+
*/
|
|
427
|
+
private resolveTlsTerminatedPorts;
|
|
408
428
|
/** Whether the current options require SMTP AUTH support on the listener. */
|
|
409
429
|
private isAuthConfigured;
|
|
410
430
|
/**
|
|
@@ -429,6 +449,29 @@ export declare class UnifiedEmailServer extends EventEmitter {
|
|
|
429
449
|
private handleRustRcptToRequest;
|
|
430
450
|
private evaluateDefaultRcptToPolicy;
|
|
431
451
|
private resolveRecipientPolicy;
|
|
452
|
+
/**
|
|
453
|
+
* Whether a matched route is a pure submission-permission route that must
|
|
454
|
+
* not claim a recipient whose inbound mail this server terminates.
|
|
455
|
+
*
|
|
456
|
+
* A route qualifies only when ALL of the following hold:
|
|
457
|
+
* - the recipient domain is registered here AND its inbound policy stores
|
|
458
|
+
* mail locally, i.e. this server really is the destination. A domain
|
|
459
|
+
* registered for outbound purposes only (`inboundPolicy` absent, or
|
|
460
|
+
* `defaultAction: 'reject'` for send-only domains) keeps relaying, because
|
|
461
|
+
* its real MX legitimately lives elsewhere;
|
|
462
|
+
* - the action is `process`, the only action whose transport is DNS-MX
|
|
463
|
+
* relay. `store`, `deliver`, `forward` and `reject` all express a
|
|
464
|
+
* deliberate disposition for the recipient and always win;
|
|
465
|
+
* - the route names no recipient scope. `match.recipients` is the only way a
|
|
466
|
+
* route states which recipients it is for, and a pattern that matches every
|
|
467
|
+
* address states nothing. Recipient-scoped `process` routes keep winning —
|
|
468
|
+
* consumers use them for local addresses handled by their own acceptance
|
|
469
|
+
* pipeline. Encoding recipient intent in `match.headers` is not supported
|
|
470
|
+
* for this purpose.
|
|
471
|
+
*/
|
|
472
|
+
private isSubmissionPermissionRouteForLocalRecipient;
|
|
473
|
+
/** Whether a recipient pattern list narrows the route to specific recipients. */
|
|
474
|
+
private routeNamesRecipientScope;
|
|
432
475
|
private getRejectionReasonForResolution;
|
|
433
476
|
private notifyRecipientRejected;
|
|
434
477
|
private buildPolicySession;
|