Announcing the KumoMTA Astronomical Spring 2026 Release

  • April 29, 2026

A Spring Tune-Up: The April 2026 Release

Back when we announced the Spring 2026 release some of you pointed out that it wasn't quite spring yet at the start of March, but there's actually two Springs: Astronomical Spring starting around March 20, and Meterological Spring starting on March 1. Since that release was technically the Meterological Spring release, we're back with the Astronomical Spring release.

Yesterday we tagged the April 2026 release. Here's what's worth your attention.

A new way to ship logs out of band

New for this release is the kumo.jsonl module, which provides utilities for reading and writing zstd-compressed JSONL log segment files. If you've ever wanted to deliver webhooks (or do any other log-driven processing) outside of the in-process log hook mechanism, this is for you.

The use case is familiar to anyone running webhooks at high volume: you want a separate, long-lived process that reads the on-disk logs and forwards them to an HTTP endpoint, with its own checkpointing, its own retry semantics, and its own failure mode that isn't tangled up with the MTA's hot path. With kumo.jsonl.new_tailer, you can now do exactly that. The docs include a working Batched Webhook Example and a Per-Customer Webhook Example that are worth reading even if you're not planning to use this immediately — they're a nice illustration of how the pieces fit together.

MIME handling for non-conformists

Anyone who has spent time relaying mail internationally knows that not every message in the wild is well-formed by the strict reading of the SMTP RFCs. A common example: messages with shift_jis body content that doesn't carry the appropriate MIME markup to declare its encoding. Technically not relayable but... it's common in Japan and you have to deal with it.

Our MIME parser now accommodates non-conforming binary content like this. For most users this change is fully transparent. For users writing Lua policy that touches message content, be aware that some methods on Message and MimePart may now return binary strings where they were previously guaranteed to be UTF-8. If you'd rather rewrite this kind of content into conforming MIME on the way through, msg:check_fix_conformance with charset detection enabled can do exactly that. If you'd rather preserve it for relay, your policy now has the freedom to do that too.

Related: msg:parse_mime() and kumo.mimepart.parse() now preserve binary bytes rather than applying lossy UTF-8 conversion, and we've added kumo.encode.charset_encode and kumo.encode.charset_decode for advanced charset handling.

Per-source DNS resolution strategy

The new ip_lookup_strategy option on the egress path lets you control how A/AAAA lookups are performed when resolving MX hosts. Because it's set in the egress path, you can control resolution on a per-source basis. Handy if you've ever needed one source pool to behave differently from another when it comes to IPv4/IPv6 selection.

Clearer errors when things don't connect

If you run KumoMTA behind kumo-proxy or with multiple sources, you've probably seen connection failures pile up when an IP isn't plumbed somewhere it needs to be. This release rolls those up into clearer summary messages: All failures are related to the proxy server having an unplumbed source address, All failures are related to proxy connection issues, or All failures are related to having an unplumbed source address.

Heads-up for the automation crowd: if you have rules that match on the precise formatting of these failure strings, you'll want to review them. (And for HA Proxy users, this detection isn't possible due to limits of the HA Proxy protocol itself — that's the protocol, not us.)

A DKIM behavior change worth flagging

This one is in the breaking changes section for a reason: DKIM verification no longer implicitly generates a policy failure when the From: domain doesn't match the DKIM signature. That was a non-standard check we'd inherited from a dependency, and it was overly restrictive for what most users actually want. If you need that policy, you can iterate over the authentication results returned by msg:dkim_verify() and apply your own logic against the signature domains.

And a pile of smaller wins

There are several quality-of-life improvements worth a quick mention:

  • msg:get_first_named_header_value and related accessors now fall back to the raw header value when the structured parser fails, instead of raising an error. More resilient handling of messages with non-conforming headers.
  • The queue helper now detects certain misconfigurations at startup, so you find out about them before the server starts receiving production traffic.
  • kumo.serde.json_encode_pretty now sorts keys in JSON object output, which means tools like resolve-shaping-domain produce deterministic, diff-friendly output.
  • New kumo.string.starts_with, kumo.string.ends_with, kumo.fs.metadata_for_path, and kumo.fs.symlink_metadata_for_path functions. Thanks to @kayozaki for these contributions and for the fix that ensures intermediate client certificates are properly loaded and propagated when using openssl!
  • Queue Helper's setup_with_options has a new optional invalidate_with_epoch parameter to invalidate the queue_helper_data cache when the config epoch is bumped. Lower latency on config changes, in exchange for some CPU around update time — pick your trade-off.
  • HTTP injection API now pre-defines a to_header template substitution with the default To header value, which you can reference as and override per-recipient when needed. Thanks to @Harshjha3006!
  • A handful of SMTP server fixes, including correctly returning a 501 permanent failure (rather than a 421) for invalid addresses in MAIL FROM or RCPT TO, and accepting uncommon quoted local parts containing the @ sign in the envelope address parser.

Get it while it's fresh

As always, the release is available now through our package repos and Docker container. The full changelog has every detail, and a few of the breaking changes mentioned above warrant a careful read if you have automation or Lua policy that depends on the prior behavior.

And remember, if you don't want to wait for the next stable release to try the latest, our Dev branch is updated with every commit to main. The latest changes are always at https://docs.kumomta.com/changelog/main/, and you can test them by installing the -dev version of our installers and containers.

Got something you'd like to see in the next release? Tell us about it. The community shapes a lot of what we work on, and the contributions in this release are a good example of why.

- - - - - - - - - 

KumoMTA is the first open-source MTA designed from the ground up for the world's largest commercial senders. We are fueled by Professional Services and Sponsorship revenue.

Community HelpDiscord | Documentation | Blog | Github | Swag