-
DcRouter v33.0.0
StableRelease / build-and-release (push) Successful in 19m49sreleased this
2026-10-16 23:29:55 +00:00 | 5 commits to main since this release2026-10-16 - 33.0.0
Breaking Changes
@push.rocks/smartmta^9.5.0 → ^13.0.1. dcrouter re-exportsUnifiedEmailServer,IUnifiedEmailServerOptions,IEmailRouteandIEmailDomainConfigfrom it and@serve.zone/dcrouter-apiclienttypes the email settings'emailConfigagainst it, so consumers see SmartMTA 10–13's changes: inbound mail is parsed with@push.rocks/smartmail3 (postal-mime) instead ofmailparser;Email.headersholds the raw field values as written (unfolded, a repeated field joined with,) instead of mailparser's interpreted forms;Email.fromis the first mailbox of the From fields in message order with groups flattened, and group members count as header To/Cc;baseLoggeris a@push.rocks/smartlog5 instance;Email.toSmartmail()/fromSmartmail()use smartmail 3; the Linux mail engine is a static musl executable nameddist_rust/mailer-bin_linux_amd64_musl/mailer-bin_linux_arm64_muslwith its third-party notices next to it (<binary>.third-party-notices.{json,txt}) and innative-notices/, instead ofmailer-bin_linux_amd64/_arm64,notices/andthird-party-notices.md; therawMessageSmartMTA hands toonAcceptEnvelope/onMessageDatakeeps the CRLF that ends the message's last line. dcrouter itself references none of those paths.- Email route
match.headerssemantics follow the raw headers: mailparser's derived keyspriorityandlistno 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.nodeis>=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 plainauth.users, and their outbound relay routes matchauthenticatedUser.emailConfig.auth.usersandauthUserCountingetEmailServerSettingstherefore 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.userslogin 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.userslogin or an unscoped SMTP account could send as a WorkApp address through that identity's route and DKIM key. The routes now also matchauthenticatedUser; 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,DATAand 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
smtpAuthAccountsAndScopescapability and its fallback, which dropped stored SMTP accounts and madecreateAccountrefuse; SmartMTA 12 and later always have the capability. - The production dependency tree no longer contains
mailparser,nodemaileror theirhtml-to-text/deepmerge-tssubtree. - 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-Idheader 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.tscovers 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/interfacescontract defines. - Write the
X-Dcrouter-Cached-Email-Idheader into the header section only (email). Before prepending its own, the spool removed every line starting withX-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/tstest7.2.0 → 8.0.0,@git.zone/tsbundle3.2.0 → 3.7.0,@git.zone/tswatch7.0.0 → 7.1.1,@git.zone/tsdocker3.11.0 → 3.12.0 and@types/node26.6.3 → 26.6.5;@git.zone/tsbuild6.0.2 and@git.zone/tsrun3.0.3 are current. tstest 8 drops the Deno runtime and refuses a test file whose name namesdeno, so the new sender-scope test istest/test.smtp-sender-scope.node.tsand 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 thetsbundle noticescommand; 3.6.2 fixes esbuild browser bundles that failed withENOENTon a module a package'sbrowserfield disables), and the tswatch and tsdocker updates change nothing dcrouter configures.@git.zone/tsdenostays on 1.15.0: it only drivesbuild:binaryand leaves the project with the move to a Node.js single executable application. The Dockerfile's base image digests are unchanged.
Downloads