• DcRouter v33.0.0
    Release / build-and-release (push) Successful in 19m49s
    Stable

    jkunz released this 2026-10-16 23:29:55 +00:00 | 5 commits to main since this release

    2026-10-16 - 33.0.0

    Breaking Changes

    • @push.rocks/smartmta ^9.5.0 → ^13.0.1. dcrouter re-exports UnifiedEmailServer, IUnifiedEmailServerOptions, IEmailRoute and IEmailDomainConfig from it and @serve.zone/dcrouter-apiclient types the email settings' emailConfig against it, so consumers see SmartMTA 10–13's changes: inbound mail is parsed with @push.rocks/smartmail 3 (postal-mime) instead of mailparser; Email.headers holds the raw field values as written (unfolded, a repeated field joined with , ) instead of mailparser's interpreted forms; Email.from is the first mailbox of the From fields in message order with groups flattened, and group members count as header To/Cc; baseLogger is a @push.rocks/smartlog 5 instance; Email.toSmartmail()/fromSmartmail() use smartmail 3; the Linux mail engine is a static musl executable named dist_rust/mailer-bin_linux_amd64_musl / mailer-bin_linux_arm64_musl with its third-party notices next to it (<binary>.third-party-notices.{json,txt}) and in native-notices/, instead of mailer-bin_linux_amd64 / _arm64, notices/ and third-party-notices.md; the rawMessage SmartMTA hands to onAcceptEnvelope/onMessageData keeps the CRLF that ends the message's last line. dcrouter itself references none of those paths.
    • Email route match.headers semantics follow the raw headers: mailparser's derived keys priority and list no longer exist, so a route that names them never matches again; name the raw field (x-priority, importance, list-id, list-unsubscribe, …) instead. dcrouter's own generated routes use no header matches.
    • engines.node is >=22.12.0 (was >=22), the floor SmartMTA 11.1 and its dependencies need.
    • WorkApp SMTP identities are composed as scoped auth.accounts (sender scope = the identity's address) instead of plain auth.users, and their outbound relay routes match authenticatedUser. emailConfig.auth.users and authUserCount in getEmailServerSettings therefore no longer include WorkApp credentials.

    Security

    • The two sender-authorization issues below need a valid SMTP credential of the dcrouter instance (a WorkApp credential, an operator auth.users login or an SMTP account); an unauthenticated sender cannot use them. Upgrade to 33.0.0.
    • Enforce WorkApp sender authorization in SmartMTA's scope checker (email). A WorkApp SMTP credential could put another tenant's address into the header From in forms dcrouter's own From parsing misread (a quoted display name or comment that looks like an address, a group, a second From field far into the header section) and have the message relayed through the other tenant's route and signed with that tenant's DKIM key. dcrouter no longer parses From itself: each WorkApp credential is an SMTP account scoped to its own address, and SmartMTA refuses with 553 any MAIL FROM and any header From mailbox outside that scope.
    • Bind WorkApp outbound relay routes to their own credential (email). The routes matched any authenticated session whose sender was the identity's address, so an operator auth.users login or an unscoped SMTP account could send as a WorkApp address through that identity's route and DKIM key. The routes now also match authenticatedUser; other credentials relay only through routes the operator configured for them.
    • Refuse SMTP envelope addresses that carry CR, LF or NUL before connecting (email). SmartMTA 13's Rust SMTP client refuses such an envelope sender or recipient, which could otherwise end the MAIL FROM or RCPT TO command and inject further SMTP commands, as a permanent delivery failure (errorType: 'envelope', not retried). dcrouter does not call SmartMTA's Rust bridge directly: its outbound mail goes through SmartMTA's server and durable queue, whose raw path (CoreMail handoffs) already refused CR and LF in TypeScript, so no dcrouter path is known to have passed such an address; the check closes the gap below that layer.
    • Read an oversized message to its end before refusing it (email). dcrouter's SMTP listener sent 552 as soon as a message crossed its size limit (emailConfig.maxMessageSize, 10 MiB by default) and then read the rest of the message as SMTP commands, so lines in the body (RSET, MAIL FROM, RCPT TO, DATA and a further message) were executed in that session. Any SMTP client can trigger this without a credential, on every listener port. A client gains nothing it could not send as commands itself; the harm is to a client that relays a body someone else wrote, such as an authenticated submission client or an upstream MTA forwarding mail: commands hidden in an oversized body ran with that client's authentication and connection source, so a third party could inject messages under the client's identity, and the client received replies out of step. SmartMTA 13.0.1 reads the message to its end-of-data marker, refuses it with 552 and continues the session; commands pipelined after the end-of-data marker are answered in order instead of being dropped.

    Fixes

    • Remove the probe for SmartMTA's smtpAuthAccountsAndScopes capability and its fallback, which dropped stored SMTP accounts and made createAccount refuse; SmartMTA 12 and later always have the capability.
    • The production dependency tree no longer contains mailparser, nodemailer or their html-to-text/deepmerge-ts subtree.
    • Store received mail with the CRLF that ends its last line (email). SmartMTA's DATA reader removed it with the end-of-data marker, so the accepted-email spool stored each SMTP message without its final CRLF and the CoreMail inbound handoff passed that copy on. With SmartMTA 13.0.1 and the byte-exact spool below, the stored copy is the received message exactly (dot-stuffing removed), behind the X-Dcrouter-Cached-Email-Id header dcrouter prepends, and the CoreMail inbound handoff carries those bytes, final CRLF included. dcrouter never compensated for the missing CRLF, so nothing else changes; messages stored before the upgrade are not rewritten. test/test.smtp-received-bytes.node.ts covers it end to end.
    • Store and hand on received mail byte for byte (email). The accepted-email spool decoded every received message as UTF-8 and encoded it again, so each byte that is not valid UTF-8 (an 8-bit body in ISO-8859-1 or another legacy charset, raw binary content) was replaced with U+FFFD in the stored copy, in the CoreMail inbound handoff (the bytes it hashes and uploads) and in every relay of the stored copy (SmartMTA handoff, durable envelope dispatch and their retries). Such mail reached CoreMail mailboxes and relay recipients altered; 7-bit and valid UTF-8 mail was not affected. The spool now keeps the message as bytes from SmartMTA to storage and to every relay. Messages stored before the upgrade stay as stored: their replaced bytes cannot be recovered. WorkApp typed endpoints still receive the message as the UTF-8 string their @serve.zone/interfaces contract defines.
    • Write the X-Dcrouter-Cached-Email-Id header into the header section only (email). Before prepending its own, the spool removed every line starting with X-Dcrouter-Cached-Email-Id: anywhere in the message, so such a line in the body (a quoted or forwarded header, for example) was deleted from the stored and relayed copy. Only fields of that name in the header section, with their folded lines, are now replaced. The header line ends like the message's first line, so a message with bare-LF line endings no longer gets one CRLF line.

    Maintenance

    • Release tooling: pnpm 12.8.2 → 12.11.2, @git.zone/tstest 7.2.0 → 8.0.0, @git.zone/tsbundle 3.2.0 → 3.7.0, @git.zone/tswatch 7.0.0 → 7.1.1, @git.zone/tsdocker 3.11.0 → 3.12.0 and @types/node 26.6.3 → 26.6.5; @git.zone/tsbuild 6.0.2 and @git.zone/tsrun 3.0.3 are current. tstest 8 drops the Deno runtime and refuses a test file whose name names deno, so the new sender-scope test is test/test.smtp-sender-scope.node.ts and runs on Node.js, the runtime dcrouter ships; no file carries a // tstest:deno:* directive. dcrouter's tsbundle configuration (one esbuild browser bundle) uses none of the options tsbundle 3.3–3.7 added (3.7.0 adds the tsbundle notices command; 3.6.2 fixes esbuild browser bundles that failed with ENOENT on a module a package's browser field disables), and the tswatch and tsdocker updates change nothing dcrouter configures. @git.zone/tsdeno stays on 1.15.0: it only drives build:binary and leaves the project with the move to a Node.js single executable application. The Dockerfile's base image digests are unchanged.
    Downloads