1552 Commits

Author SHA1 Message Date
Wez Furlong d2034778e3 trace headers: fold long supplemental trace headers
Base64-encoded supplemental trace headers (X-KumoRef) can exceed the
998-octet SMTP line length limit when they include sizeable metadata,
causing a strict receiver (eg: us, when an ARF comes back to us with
that header) to reject the message at DATA with "line too long".

Fold the encoded value across continuation lines so each physical line
stays within the limit, and strip the folding whitespace before decoding
it back into a feedback report.
2026-07-30 10:41:38 +01:00
Wez Furlong d895ee7afa websockets: fix narrow race at session initiation time
Running down a test flake and diagnosed this one.

This commit closes a narrow race when attaching to the SMTP tracing
(trace-smtp-server, trace-smtp-client) and TSA subscription
(subscribe_suspension_v1, subscribe_event_v1) websocket endpoints. The
server did not finish registering the new subscriber until just after
the connection handshake completed, so any event produced in the brief
window between those two points was not delivered to that client.

In practice this could cause a freshly-attached SMTP trace to miss the
first event or two of a session that happened to start at the same
instant; it did not affect mail flow. The endpoints now register the
subscriber before completing the handshake
2026-07-28 15:21:45 +01:00
Wez Furlong bfdf4ed3e9 mailparsing: rfc2822 date parsing now accepts obsolete timezones
RFC 2822 date parsing now tolerates an obsolete alphabetic time zone
such as the `UTC` that Amazon SES emits in its bounce reports, which
strict parsing would otherwise reject. A recognized abbreviation
resolves to its offset (derived from the IANA time zone database), and
any other alphabetic zone falls back to the `-0000` unknown offset per
RFC 5322 section 4.3 rather than failing the parse.

closes: https://github.com/KumoCorp/kumomta/pull/551
2026-07-28 12:09:53 +01:00
kay ozaki abce86bd24 add check_trailing_bits: false
While testing message parsing code, there's a chance that the sender
isn't fully compliant with base64.  This leads to failure such as below

base64 decode: non-zero trailing bits at 20211 b='i' in HRtbD4NCi==

As long as it's not destructive, its more convenient to be able to
support non-RFC messages so we can extract message artifacts for its
decision making.

closes: https://github.com/KumoCorp/kumomta/pull/558
2026-07-28 08:13:06 +01:00
Alex Burch 0c930b7a1c rfc5321: tolerate stray space after MAIL FROM:/RCPT TO: colon
RFC 5321 does not permit a space between the colon and the reverse/forward
path in MAIL FROM: and RCPT TO:, but a number of legacy clients emit one
(e.g. "MAIL FROM: <addr>"), which kumod rejected with 501 5.1.7.

Relax the grammar to accept and discard an optional run of spaces/tabs after
the colon in both the success and "valid-address + trailing junk" arms for
MAIL FROM and RCPT TO. This is a grammar-level change per review feedback,
replacing the earlier opt-in allow_space_before_path listener option.

Adds parser tests covering the tolerated space for both verbs, including the
null sender, postmaster, and ESMTP parameter cases.

closes: https://github.com/KumoCorp/kumomta/pull/559
2026-07-28 07:13:55 +01:00
Wez Furlong 6b2ddfec41 docs: changelog for #570 2026-07-28 06:59:36 +01:00
Wez Furlong 7d6e6d558d Tidy up Option<PolicyMode> -> PolicyMode
The latter already has a None so it felt cumbersome to layer it up in an
Option.
2026-07-27 08:02:38 +01:00
Wez Furlong 9efc2a031c allow mocking mta-sts end-to-end, add integration test coverage
This sets up plumbing to allow testing the broken mta-sts aliasing
issue, and enables feeding an optional resolver through the mx lookups
as well.

closes: https://github.com/KumoCorp/kumomta/pull/524
2026-07-27 08:02:37 +01:00
Wez Furlong 41be60dfce Evaluate MTA-STS during MX resolution to fix aliasing issue
refs: https://github.com/KumoCorp/kumomta/issues/484

Briefly, the issue is that if some random domain that shares MX records
with another (eg: someone is using google apps or icloud for their
vanity domain) publishes a broken MTA-STS policy that requires eg:
cloudflare MX hosts then because we roll up by site name, that broken
MTA-STS policy bleeds into all the other domains that share those MX
records.

The resolution is simple, but is technically a breaking change.

Moving the policy resolution to happen during site_name resolution
allows us to resolve both per-domain things at the same and have the
MTA-STS policy amend the effective set of MX hosts.  The output of that
is then used for site_name aggregation/rollup.

The consequence of this is quite nice: an MTA-STS policy that is more
restrictive than the full set of MX hosts now prevents delivering to
any of the excluded hosts, and a totally broken policy that prevents all
of its MX hosts is now completely undeliverable and will produce
transient failures.

The downside is that for users that had previously disabled mta-sts in
their default shaping block, they will need to change a different config
option to continue to prevent MTA-STS from being consulted.  One example
of this that I recall is that one user's network posture prevented
MTA-STS from making HTTPS requests to fetch the policy.  Another user
just wanted to cut out the additional DNS traffic.  Those use cases
require altering the new kumo.dns.set_mta_sts_enabled enabled to false
during `init`.
2026-07-27 08:02:37 +01:00
Mike Hillyer 2d9f70dd98 Double quotes in titles break the build. 2026-07-25 22:12:09 -04:00
Mike Hillyer 0acaa1b6a5 Cleanup of FAQ entries. 2026-07-15 17:09:53 -04:00
Mike Hillyer 2790f6dfdb Killing deprecated FAQ 2026-07-15 12:41:28 -04:00
Mike Hillyer fd227fc055 Adding to the FAQ 2026-07-15 11:53:40 -04:00
Mike Hillyer e6bf582beb Add metadata to FAQs. 2026-07-09 21:46:20 -04:00
Mike Hillyer cf83f8ed7e Add meta descriptions to the Userguide and Tutorial. 2026-07-09 18:07:40 -04:00
Mike Hillyer d878f95021 Minor mechanical fixes. Capitalization and punctuation mostly. Also testing defined meta descriptions. 2026-07-09 14:31:23 -04:00
Wez Furlong 3788f838d3 spool: probe directory usability before opening
We spent some time running down an issue where the combination
of the root-user umask and some ad-hoc testing to recreate the
spool directory led to a very difficult to understand situation
where the spool seemed to corrupt itself on the next restart.

The root cause was that the spool directories had been re-created
by the operator with permissions that led to an asymmetric
filesystem view for the dropped privs scenario (eg: the default
when running via systemd).

The crux of it is that the explicit existence check used by rocksdb to
determine if the database was new or pre-existing was inconsistent with
what the kernel actually allowed for the permissions on the directory,
causing the second start to believe that it was a new database, then
surprise itself when it found other evidence that it wasn't new at all.

This commit adds a dir-probe crate that tests for this misconfiguration
and raises an error before rocksdb can try to open and corrupt itself.

We can't reasonably exercise this in CI because of the priv dropping
component, but the included example probe utility confirmed it for
me outside of the harness:

```
bdad88ed680e# install -d -o wez -g wez -m 2700 /tmp/probe-demo
bdad88ed680e# ./target/debug/examples/probe /tmp/probe-demo --user wez
probing /tmp/probe-demo as ruid=0 euid=1000 rgid=1000 egid=1000
FAIL: /tmp/probe-demo (directory owner uid=1000 gid=1000 mode=2700); process ruid=0 euid=1000 rgid=1000 egid=1000: inconsistent
  view of /tmp/probe-demo/.kumo-dir-probe9nMaUj.renamed: access(2) reports the file present=false but open(2) reports it present
=true. This means the process real and effective user ids differ (a privilege drop) and the directory permissions are too restr
ictive for one of those identities. A database opened here would decide to create a fresh instance yet write over the existing
files, corrupting itself on the next startup. Ensure the directory is owned by, and grants rwx to, the identity the service run
s as.
bdad88ed680e# chmod 2755 /tmp/probe-demo
bdad88ed680e# ./target/debug/examples/probe /tmp/probe-demo --user wez
probing /tmp/probe-demo as ruid=0 euid=1000 rgid=1000 egid=1000
PASS: /tmp/probe-demo is usable by this identity
bdad88ed680e# rm -rf /tmp/probe-demo
```
2026-07-08 14:55:02 +01:00
Mike Hillyer 96db828722 Minor wording corrections. 2026-07-07 14:30:02 -04:00
Mike Hillyer 1fac335bc6 Mechanical fixes. 2026-07-07 12:41:36 -04:00
Mike Hillyer 4a4809a320 Additional formatting pass. 2026-07-07 09:57:35 -04:00
Mike Hillyer cdabd937c7 Formatting changes. 2026-07-07 09:16:13 -04:00
Daniel Schaaff c17c123402 improve cgroup memory usage calculation to avoid counting purgeable cache
When running under a cgroup limit, get_cgroup() reported
stat.usage_in_bytes (memory.current on v2, memory.usage_in_bytes on v1)
as the process memory usage. That counter includes all page
cache, including cold reclaimable cache (inactive_file). For a
workload that writes logs and spool to disk, cold cache can dominate:
on a real pod memory.current was ~51GB while inactive_file was ~46GB
and the true non-cache footprint was ~1.8GB. kumod saw usage near its
limit, hit get_headroom() == 0, and ran
shrink_ready_queue_due_to_low_mem
and other reductions against pressure that was almost entirely
reclaimable cache the kernel would drop before any OOM.

Report the working set instead:
```
working_set = max(memory.current - inactive_file, anon)
```
This matches container_memory_working_set_bytes (kubelet/cAdvisor) and
the kernel's own reclaimability accounting. It still counts everything
that can drive an OOM: anonymous memory, dirty page cache, active_file,
tmpfs/shm, and slab.

Signed-off-by: Daniel Schaaff <daniel@danielschaaff.com>
closes: https://github.com/KumoCorp/kumomta/pull/549
2026-07-07 10:02:02 +01:00
Wez Furlong c683c5b274 smtp_server: reject invalid UTF-8 command lines with 501
A command line that decoded to invalid UTF-8 propagated the decode
error out of read_line, which the session handler turned into a
421 technical difficulties response that tore down the connection.
Model it as a ReadLine variant so both the command loop and the AUTH
exchange emit a 501 syntax error and keep the session alive.

Closes #550
2026-07-06 19:21:59 +01:00
Wez Furlong dfb17abd0d dane: engage for secure CNAME into unsigned zone
Per RFC 7672 section 2.2.2, an MX host that is a securely published
CNAME remains DANE-eligible at its original name even when the alias
target lands in an unsigned zone: it is the secure TLSA RRset, not the
address records, that authenticates the peer. When the address chain is
insecure but MX selection was secure, an explicit CNAME query isolates
the alias's own DNSSEC status; a secure alias engages DANE, an
indeterminate status defers for downgrade resistance.

refs: https://github.com/KumoCorp/kumomta/pull/545#discussion_r3472353021
2026-07-06 11:46:51 +01:00
Wez Furlong 4fc3cbca92 add treat_mx_list_as_secure to SmtpProtocol
This allows you to explicit indicate that an mx_list should
be treated as DNSSEC secure.

We cannot simply assume that lua code setting mx_list be treated
as secure, because the lua code may be simply passing through
the result of some other DNS lookup--we need an explicit way
to thread through the overall secure flag in order for the
secure property to be correctly upheld.
2026-07-06 11:46:51 +01:00
Wez Furlong e13734d186 address review feedback on https://github.com/KumoCorp/kumomta/pull/545
Address most of the easy points from the review, others will land
separately.
2026-07-06 11:46:51 +01:00
Wez Furlong 3b61117145 dns: add RFC 5011 managed trust anchor option for unbound
trust_anchor_file now accepts `{ managed = "<path>" }` for an RFC 5011
auto-maintained anchor file (unbound only; hickory errors). Unbound seeds
the file from the bundled root anchors when the file is absent.
2026-07-06 11:46:51 +01:00
Wez Furlong 1874dc09f7 enable DNSSEC when using hickory, allows enable_dane w/o unbound
When we first built out DANE, the hickory implementation was
all or nothing when DNSSEC validation was enabled, making it unsuitable
for use in an MTA.

Since that time hickory has matured; this commit wires up the
DNSSEC signals and that enables its use with DANE.

We do still require openssl for establishing the connection when
DANE is enabled; that's a different constraint.
2026-07-06 11:46:50 +01:00
Wez Furlong 7331cb4ec7 new allow_smtp_auth_plain_without_valid_certificate option
This is technically a breaking change, but in a good way.
We will no longer send SMTP AUTH PLAIN creds when we didn't
validate the peer certificate.  This is what you want in
all production deployments.  The new option facilitates
test setups or other unusual configurations where the certs
cannot be verified but you have contrived to trust the
remote system through some other means.
2026-07-06 11:46:50 +01:00
Wez Furlong a4c4b40fbe add dane_result_count metric
This surfaces aggregate dane results
2026-07-06 11:46:50 +01:00
Wez Furlong d920ffb5c4 fix: DANE downgrade resistance
We weren't checking the DNSSEC status for A/AAAA when considering
DANE, and were overly permissive in a couple of related cases.

This diff ended up fairly large because the bulk of it is to
facilitate testing.

refs: https://github.com/KumoCorp/kumomta/issues/543
refs: https://github.com/KumoCorp/kumomta/pull/517
2026-07-06 11:46:49 +01:00
Wez Furlong 3f11963acd docs: fix broken link 2026-06-27 18:58:12 +01:00
Wez Furlong 407379dd36 track sched->readyq fan in, clean up some internals
This commit removes the queue_name_for_config_change_purposes_only wart
from inside the readyq machinery.  This field had a bunch of caveats
and could in some cases become stale if the originating scheduled
queue had aged out.

It's been replaced by a HashSet of Weak refs back to the associated
scheduled queues so that it is now possible to walk the reverse of
the fan-in graph when we need to.
2026-06-25 13:41:01 +01:00
Mike Hillyer db31223667 Add changelog entry for community shaping file pull request. 2026-06-23 17:24:54 -04:00
Wez Furlong cf5db1a796 fixup doc build 2026-06-23 15:54:06 +01:00
Wez Furlong 4d576922f8 deps: hickory 0.25 -> 0.26
This is fairly big diff because a number of hickory types moved around
or changed signature.

In particular, the config structs changed in a way that would fan out
and require attention by our users.  So the approach I opted to take
here was to define our own types that look like the hickory 0.25
shape and then we have explicit logic to map those to hickory and
also to our unbound resolver crate.

This commit also bumps up the unbound resolver crate because it also
uses hickory's types.  I took the opportunity to upgrade the version
of the embedded unbound resolver as part of that work.
2026-06-23 15:47:08 +01:00
Wez Furlong a1a7e16759 docs: fixup links 2026-06-23 10:13:11 +01:00
Wez Furlong 2a657f68fb included scheduled queue config in resolve-egress-path output
and also in resolve-shaping-domain, for consistency.

We can use the message rate from the scheduled queue config to refine
the computed ceilings/constraints too, which is nice.
2026-06-23 10:00:07 +01:00
Wez Furlong 49e1a62733 add resolve-egress-path API endpoint
This is conceptually equivalent to resolve-shaping-domain but operates
on a live instance, rather than loading up the policy offline.
2026-06-23 10:00:05 +01:00
Wez Furlong 61bb95a558 resolve-shaping-domain: expand output
This changes the output to be a bit more human friendly by default
by rendering as toml.

It also includes the rate ceilings added in the prior commit.
2026-06-23 09:59:32 +01:00
Wez Furlong 0a72ce7f02 introduce EgressPathConfigConstraints concept
This can be computed for an EgressPathConfig and is useful to indicate
what the effective ceilings are for this egress path.

We return this from inspect-ready-q
2026-06-23 09:58:38 +01:00
Wez Furlong b9e677dd16 add abort-ready-q-conn and inspect-readyq endpoints/commands
These build on the infrastructure from the prior commit and improve
the observability of the system.

refs: https://github.com/KumoCorp/kumomta/issues/539
2026-06-23 09:58:36 +01:00
Wez Furlong 423abc4704 add per-dispatcher state tracking and watchdog
The ready queue maintainer now tracks for each dispatcher whether it
is making progress.  If it exceeds dispatcher_progress_watchdog_timeout
then the task will be aborted and any message(s) it held onto will
be returned to the scheduled queue.  This will free up the connection
slot and allow another connection to be attempted, potentially
making further progress.

refs: https://github.com/KumoCorp/kumomta/issues/539
2026-06-23 09:56:45 +01:00
Wez Furlong 002d6593f0 new: suspend_when_unplumbed and suspend_when_proxy_unhealthy source options
Egress sources can now be configured to auto-suspend when their local
bind address appears unplumbed or when their configured proxy server
appears unreachable.  A suspended source is skipped during pool selection
until the configured duration elapses.
2026-06-23 09:22:27 +01:00
Wez Furlong e21bb1c672 ha_proxy_server and socks5_proxy_server now also resolve host names
We'll expand these to a list of candidate addresses, trying each in
turn.  We use the same overall connect timeout regardless of how
many candidates are present, to respect that timeout bound.

closes: https://github.com/KumoCorp/kumomta/pull/402
2026-06-23 09:11:53 +01:00
Wez Furlong 9987248313 spool: surface and handle rocksdb background errors
We recently investigated an issue where a rocksdb had been damaged by
corrupting/removing SST files (it sounded like this was accidentally
self-inflicted by some backup/orchestration infrastructure) leaving the
system in a silently-broken state: writes just wouldn't make progress
and there were no error messages.

Inspecting the `/var/spool/kumomta/data/LOG` log file (which is a
readable text file) revealed messages like:

```
2026/06/12-14:55:31.884227 2875746 [ERROR] [db/compaction/compaction.cc:262] Unable to load table properties for file 29704 --- IO error: No such file or directory: While open a file for random read: /var/spool/kumomta/data/029704.sst: No such file or directory
2026/06/12-14:55:31.884311 2875746 [ERROR] [db/db_impl/db_impl_compaction_flush.cc:3385] Waiting after background compaction error: IO error: No such file or directory: While open a file for random read: /var/spool/kumomta/data/029704.sst: No such file or directory, Accumulated background error counts: 6363
```

This commit improves the observability in this situation by proactively
checking for error conditions:

1. The store() and remove() operations now use our own polling within
   a deadline loop rather than spawning a blocking task and delegating
   to rocksdb's blocking interface.  This allows us to inspect the
   background error count and be cancellable, safely respecting and
   caller provided smtp max transaction duration.

2. All read and write operations check for IO and Corruption errors
   and immediately latch an error state

3. The metrics monitoring task inspects and track background error
   counts and latch us into an unhealthy state when the background
   error state appears unhealthy and persistent.

4. Additional metrics are exposed to help monitoring and alerting

While adding integration test coverage for this, I found a typo that
meant that spool errors were ignored in the message crate; they got
silently converted to `true` in all cases rather than just mapping
the success case to a `true`.

Integration tests handle the case where an SST file is corrupted
(truncated) during runtime, as well as starting up when an SST file
is missing.   These excercise both the foreground and background
error detection paths.
2026-06-23 09:11:48 +01:00
Wez Furlong 9c8183a433 docs: update for release 2026.06.23-f3af1cd0 2026-06-23 09:02:10 +01:00
Wez Furlong d88fee2e0d docs: manually run cargo run -p jsonschematodocs
This somehow got missed in an earlier commit
2026-06-17 22:52:45 +01:00
Mike Hillyer d3d4a4d7c2 Final shuffle and updates prior to publish. 2026-06-11 15:25:53 -04:00
Mike Hillyer 1999ba0251 Continue traffic shaping chapter refactor. 2026-06-09 13:23:56 -04:00