Commit Graph
155 Commits
Author SHA1 Message Date
Wez Furlong 52e2f95e9f address.rs: add more context to invalid addresses when parsing fails 2025-11-03 21:35:39 +00:00
Wez Furlong 4c6d7ca8ea add policy-extras.mail_auth module
This aggregates the various authentication-results producing auth checks
into a more convenient function.

Part of this change improves some plumbing in the dkim checking (we no
longer raise an error for a missing From, but instead indicate a failed
dkim result), and we now support passing down a resolver name to the
various checking functions, to facilitate testing and other more
advanced use cases.

refs: https://github.com/KumoCorp/kumomta/issues/16
refs: https://github.com/KumoCorp/kumomta/issues/84
2025-11-02 11:56:57 +00:00
Wez Furlong 49cdee1ec5 lruttl: expose pre-defined cache info to the docs
This commit adds some plumbing to allow us to collect metadata
about the caches and spit it out into a json file that we can
then use in the docs to show the predefined caches, capacities
and brief comments about their purpose.

We put that info on the set_lruttl_cache_capacity doc page.
2025-11-01 07:40:40 +00:00
Wez Furlong c085e368b2 dkim_(key|signer)_cache: enable thundering herd protection
Use the managed accessor for thundering herd protection.

May help with a situation as discussed in
https://github.com/KumoCorp/kumomta/pull/438
2025-10-31 18:04:17 +00:00
Wez Furlong e7f4806f1b dkim: add integration test assertions for signing cache
refs: https://github.com/KumoCorp/kumomta/pull/438
2025-10-31 17:27:05 +00:00
Wez Furlong 5ad7df0203 arc: expose arc_seal and arc_verify methods to lua
refs: https://github.com/KumoCorp/kumomta/issues/16
2025-10-31 12:16:32 +00:00
Wez Furlong 67bf14ca20 improve header wrapping functions
Augment the docs to clarify that msg:append_header and
msg:prepend_header do no modification of the value by default.

Adjust the wrapping used by Header:new_unstructured so that it
hard wraps before the SMTP line length limit.

Add optional encode parameter to msg:append_header and
msg:prepend_header so that those methods can opt-in to the
Header:new_unstructured formatting.

Add HeaderMap:append method to mirror the existing HeaderMap:prepend
method.

Add unit tests that demonstrate the effect of wrapping/encoding
for both the "dumb" msg:append_header and msg:prepend_header functions,
and the "smarter" HeaderMap:prepend and HeaderMap:append methods that
always encode their parameters.

Update the docs for the headermap flavor of these methods to indicate
that they will always encode the value.
2025-10-29 11:35:42 +00:00
Wez Furlong 7f4a929ca2 message transfer (xfer) support
This commit implements a kumomta-specific message transfer
protocol that is intended to be used to migrate messages
from one kumomta node to another.

The transfer is carried out using an HTTP POST request
to the destination node's http listener.

The request includes the full message metadata and body,
in a compressed form.

An xfer request can be made via `kcli xfer` (and thus also via an HTTP API
endpoint).  It works similarly to a rebind operation; you specify the
criteria to be used to match scheduled queues, along with the target
node for the xfer, and kumomta will find matching queues, drain out the
messages, make an adjustment to the metadata to capture current
scheduling information, and then place the messages into an xfer queue.

The xfer queue has hard-coded scheduling queue configuration of its own,
with the base retry interval set to 10 seconds, which should be suitably
aggressive for the intended use case.

You may apply shaping to affect the number of concurrent requests in a
similar way to how TSA shaping is configured.

On the receiving side, the incoming xfer sanity check to prohibit
trying to xfer to itself.

The spool id of the Message is not suitable to be reused verbatim on
another node (spool ids include the local mac address and creation
timestamp information, as well as a random component), so the receiving
side will derive an id that should be suitable for use on that node.
The originating node id and spool id will be preserved in metadata to
aid in tracing.

It is possible for an xfer request to target an existing xfer queue, so
that you can correct/update the target in various circumstances. In that
situation the messages will be "simply" moved from the source queue to
the destination queue.

It is possible to cancel an xfer request via `kcli xfer-cancel` (and
thus also via an HTTP API endpoint).  You specify the target queue,
which must be an xfer queue, and it will have its messages drained and
the metadata changes that were applied when the xfer was initiated will
be reversed, allowing the messages to then be reinserted into their
originating queue.

refs: https://github.com/KumoCorp/kumomta/issues/311
2025-10-07 10:58:07 +01:00
Wez Furlong f968935315 add smtp_server_split_transaction event
This provides full control over how an incoming smtp transaction
will be split into batches.
2025-10-07 07:53:15 +01:00
Wez Furlong db5ebb88a7 message: expose recipient list to lua api
message:recipient() may now return an array style table
holding the recipient list, if there is more than a single
recipient on the message.

Since this can be somewhat ambiguous/frustrating to work with,
there is now also a message:recipient_list() that will always
return an array style table, even if it holds just a single
element.

The included helpers have been updated to use `message:recipient_list`.

message:set_recipient() will now optionally accept an array
style table holding the recipient list to be set.
2025-10-07 07:52:58 +01:00
Wez Furlong 888928b60f WIP: basic batch relaying and end-to-end test
This makes a first pass over relaying a batch, with a test
to prove that it worked.

The logic in the smtp client likely needs to be enhanced
to handle hitting recipient-per-transaction limits; tests
for various scenarios need to be added in follow-on commits.
2025-10-07 07:52:57 +01:00
Wez Furlong 9079a9e569 WIP: more multi-recipient type changes
This commit updates the type signatures of some key structures
to allow for the possibility of a Message having multiple recipients:

 * Message (continuing from previous commit)
 * JsonLogRecord
 * MessageInformation

Some logic has been updated to account for multiple recipients,
but critically, nothing in the core will generate them, and none
of the queue management or delivery infrastructure is aware
of recipients beyond the first.
2025-10-07 07:52:57 +01:00
Wez Furlong 055a2d0086 message: allow for multiple recipients in metadata 2025-10-07 07:52:56 +01:00
Wez Furlong 206dc7c7fa optionally detect legacy 8-bit during check-fix 2025-10-07 07:50:46 +01:00
Wez Furlong 7fffccf410 Add support for generating NDRs
This commit adds some plumbing to facilitate generation of RFC 3464
non-delivery reports and some corresponding glue to enable calling
it from lua.

Examples in the docs that are also added.
2025-09-09 14:12:52 +01:00
Wez Furlong 1bf9d138ce extended lua mime parsing API
refs: https://github.com/KumoCorp/kumomta/issues/117

Could also make it possible to do eg:
https://github.com/KumoCorp/kumomta/issues/142
without any explicit direct support in the product.
2025-09-09 09:15:58 +01:00
Wez Furlong 58729832c9 message: unit tests for parsing routing_domain with port
No functional changes, just adds some illustrative unit tests
that show that the routing domain portion of the queue name
string can accept a port number.

The system doesn't understand what to do about that at this
time.

refs: https://github.com/KumoCorp/kumomta/issues/352
2025-07-21 12:34:59 +01:00
Wez Furlong c914f9b045 dns-resolver & EnvelopeAddress: explicitly check and deny ports in domain
This is prep work ahead of https://github.com/KumoCorp/kumomta/issues/352

This commit adds an explicit check for port numbers being passed in
via the domain when setting envelope addresses.

This is forbidden by the SMTP spec. This commit adds an explicit
integration test to confirm that we're reject them there.

For HTTP injection we don't have any other central point to
validate addresses, so this commit expands on the EnvelopeAddress::parse
method to add that centralized check.

dns-resolver's domain name validation is refactored into a domain
name classifier to help reuse that same logic outside the context
of making an actual DNS lookup.

MailExchanger::resolve is now also explicitly checking for and denying
lookups that include a port number.

This commit should have no practical functional impact; all of the
things denied by these checks are illegal in SMTP.
2025-07-21 12:30:29 +01:00
Wez Furlong 4b7577ebbd even more clippy 2025-04-09 10:26:12 -07:00
Wez Furlong e3fdcaa628 cargo clippy --fix 2025-04-09 10:26:07 -07:00
Wez Furlong 8b1b639d98 message: implicitly load data/meta when calling lua get/set meta/data
I saw an instance where the throttle_insert_ready_queue event was
trying to inspect the metadata but failing because the metadata was
not loaded.

This should avoid that problem.
2025-04-06 08:59:44 -07:00
Wez Furlong 3995e62a33 message: disambiguate "metadata must be loaded first" errors
We have a couple of different places that might emit it,
let's annotate each one so that we know where it might
be coming from.
2025-04-04 14:40:19 -07:00
Wez Furlong c7fe9489c3 fix: failure to load ed25519 dkim private key
This was probably the casualty of some earlier refactoring
that has gone unnoticed until now.

Since we don't have explicit context on which key format to
parse in this helper function that is used in multiple places,
let's just make it try to parse both rsa and ed25519.

closes: #368
2025-04-03 10:11:24 -07:00
Wez Furlong 7426d87932 lruttl: de-async get/lookup
These don't need to be async
2025-04-02 05:16:17 -07:00
Wez Furlong 10dbdf1cb7 message/timeq: allow cancellation
When load testing the v2 timerwheel, I was able to produce a panic
when rebinding; the panic was caused by linking an already linked
list through the associated messages.

Digging into the state management, I believe that the issue is
due to a data race between the rebind (and other similar drain
operations) and the timer wheel dispatch.

The timer wheel first locks to pop the currently due items,
unlocks, and then resolves and inspects the per-scheduled-queue
hash sets to fully de-queue the items.

The drain operation locks both the wheel and the hash set and
atomically removes its elements.

Critically, the drain removal doesn't know whether the messages that
were in its hash set either were in the linkage for the timeq
while it held the lock, or were recently removed by the timer
wheel dispatch and are pending the resolve and insertion mentioned
above.

What this commit does is make it possible to reason about that
linkage by introducing a `cancel` method on the timeq. It will
return true/false to indicate whether the message was unlinked.

To facilitate this, we need to add an index to map the message
id to the entry within the timeq, and a way to invalidate that
entry if a message is removed and re-scheduled.

Since we want the cancellation to be ostensibly an O(1) operation,
we implement it with another source of indirection; the entry slot
id. Each time an entry is inserted into a bucket slot we increment
its entry id. When we subsequently iterate over the slots we can
resolve that id to its message id and from there resolve its
entry. If that entry points back to the entry slot that we just
used to discover it, then that entry is current. Otherwise, it
is a stale entry and we can skip over (or remove) it.

Cancellation is therefore a simple O(1) removal of the msg id -> entry
mapping and doesn't need to directly intrude into the wheel buckets.

With a cancellation function in place, it means that the
per-scheduled-queue drain operation can now atomically (while both
the hash set and the wheel locks are held) reconcile the hash set
contents with the corresponding wheel contents while it cancels them: if
the cancellation fails then it means that the wheel runner has de-queued
that particular message and that it will shortly (after we release the
lock on the hash set) then try to check it against the hashset.

That means that we need to retain those particular messages in
the hash set, but drain the rest.
2025-03-26 14:41:42 -07:00
Wez Furlong 98c7831567 message: add unit test to assert size of MessageWithId
no functional change
2025-03-26 14:41:26 -07:00
Wez Furlong 3ac1e969a9 message: refactor: adopt parking_lot::Mutex
Allows removal of some bare unwraps
2025-03-26 14:41:26 -07:00
Wez Furlong c2444b6266 add expiry to per-msg scheduling overrides
This commit allows setting a per-message `expires` timestamp
via msg:set_scheduling (and thus msg:import_scheduling_header).

The expiration takes precedence over max_age; max_age will be
ignored for messages that have configured and expiration time.

The expiration time is independent of the other scheduling
restrictions.
2025-03-15 07:18:13 -07:00
Wez Furlong b138235aa1 set_scheduling: raise error if start > end 2025-03-15 07:18:12 -07:00
Wez Furlong e655236fe0 message: manually deserialize scheduling restrictions
This resolves an issue where the default behavior for serde is to
silently swallow issues with this struct, because we use a flattened
optional structure for those restrictions.
2025-03-15 07:18:12 -07:00
Wez Furlong bcd0e3746c lruttl: add predefined cache registry
This commit adds a declare_cache! macro that serves a couple of
purposes:

* Make it slightly less boilerplatey to define a cache
* Register them explicitly as a list of predefined caches, so that
  we can raise an error if lua code would conflict via memoize
* Enable a future change that allows reconfiguring the cache parameters
  for those predefined caches

Note: if you are using clang as a linker via RUSTFLAGS/.make-env,
the linkme crate that is employed for the registry may result
in linker errors. The solution that I have in my `.make-env` is
to add `-Clink-arg=-Wl,-z,nostart-stop-gc` to RUSTFLAGS so that
its sections are not removed too soon in the build process:

```
; cat .make-env
ROCKSDB_LIB_DIR=/usr/lib/x86_64-linux-gnu
RUSTFLAGS=-C linker=clang -C link-arg=-fuse-ld=lld --cfg tokio_unstable -Clink-arg=-Wl,-z,nostart-stop-gc
```
2025-03-05 07:54:21 -07:00
Wez Furlong 054508321b lruttl: drop moka, roll our own cache on top of dashmap
I've been having trouble with moka's eviction policy leading
to some unwanted duplicate work, especially when the system
is under load and the latency is higher.

This commit swaps it out in favor of a relatively simple
implementation on top of dashmap; that gives us a decent
concurrent base and better respects the concurrency
limit and LRU around the contentious insertion case.

For LRU, since dashmap is a concurrent data structure, it
is ~impossible to use the classic doubly linked LRU approach
safely.

The strategy used here is a proabalistic sampling LRU using a technique
similar to that used in redis.

We use atomics to tag read and write entries with a monotonic counter.
Entries with smaller counter numbers are least-recent than entries with
a larger number.

During eviction we take a random sample of 10 entries from the cache
map, remove any that have expired due to TTL, and if we still need space
after that, we'll pick up to half of those to evict.  That guarantees
that we don't pick the most recent entry of the sample, and on aggregate
this should be a reasonable approximation of the "true" LRU.

In the background, every 30s, a maintenance task will remove expired
entries from the caches.  This is primarily to reduce memory utilization
when the cache is otherwise idle.  When a cache is hot, expiration is
performed as part of the eviction logic described above.
2025-03-04 16:23:16 -07:00
Wez Furlong c95fd24d9e memoize/lruttl: improve robustness
This commit fixes up a few edge cases:

* The recent switch to moka for lruttl adopted an LFU eviction
  policy which could choose not to take a recently updated value
  in some situations, which is undesirable for expensive cache
  population functions. Explicitly configure it to use classic
  LRU for its eviction policy.
* The semaphore based thundering herd control in memoize had
  a very hard to pin down edge case where we could end up
  blocking for an extended period of time.  Switch to the
  `.get_or_try_insert_with_freshness_status` method that moka
  enabled in our lruttl module
* That didn't eliminate the edge case, but rather limited the
  scope of its negative effects. There is still an edge case,
  but now it results in serial duplicate calls to the cache
  population function, which is wasteful but not as egregious
  as the extended period of blocking.
  I've opened https://github.com/moka-rs/moka/discussions/502
  with moka to see what is going wrong there
* Adopt dashmap for the map of caches, to reduce hypothetical
  contention across multiple caches. This is a micro optimization.
* Improved tracing of lruttl and memoize
2025-03-04 16:23:14 -07:00
Wez Furlong 591e9f5380 spool: add optional deadline for store operations
Under high concurrency, a backlog of store operations can build
up, especially if rocksdb isn't tuned for the workload.

In order to work together with the data_processing_timeout of the smtp
server, we'd like to be able to cancel store operations that are past
the deadline, but the spool operations are not guaranteed to be cancel
safe because both the rocksdb and the local disk implementations can
ultimately call into spawn_blocking to perform the critical portion of
the work, and that is not cancel safe.

This means that we cannot simply apply timeout_at to the store
operation: while it may appear to the caller to have been cancelled at
the appropriate time, the actual work storing the spool may be in
progress and continue to store the message to the spool, which could
lead to us thinking that we are accountable for the message when we next
restart, even though we're presumably about to tell the sender that we
did not take the message.

To deal with this situation more safely, we pass the deadline through
to the underlying operation and have it internally decide whether
it should continue.

In practice, this means that the rocksdb store operation will apply
timeout_at to the semaphore operation it does if
`limit_concurrent_stores` has been configured fr that spool.  Otherwise
the deadline is ignored.

The implementation of this is imperfect in a number of ways. A more
robust solution would be to employ something like
<https://docs.rs/cancel-safe-futures/0.1.5/cancel_safe_futures/coop_cancel/index.html>,
but that is a more invasive change and one that is a bit difficult to
wrangle across the spool trait definition in its current form.

If `limit_concurrent_stores` is not in use, this commit doesn't
change the behavior of the spool store operation.
2025-03-04 16:23:13 -07:00
Wez Furlong 49e52a0887 lruttl: adopt moka as backend cache
Switch from the simple lru cache to using the moka crate for
a lower contention cache.

This is more of a micro optimization than an earth-shattering
change, although it did highlight a few caches that were
redundantly wrapped in a Mutex--that wasn't necessary even
before this change.
2025-02-27 09:48:00 -07:00
Wez Furlong 3fc64011eb message: add save_and_shrink_data method
We don't use this anywhere yet
2025-02-21 09:17:27 -07:00
Wez Furlong 45d8449c69 message: add msg:shrink() and msg:shrink_data() methods
These are helpful when explicitly managing memory overheads
2025-02-19 15:55:12 -07:00
Wez Furlong d331df5965 dkim: add separate key cache to signer machinery
The signer cache maps the signer parameters to a pre-made
signing context.

It is essentially a map of (domain, key) -> signer.

Assuming that the (domain,key) tuple is unique, then this is
a good, effective use of that cache.

However, if multiple domains can share the same key then we
can end up re-parsing the same key data for each of them,
which is moderately expensive and a waste of CPU.

This commit introduces an additional cache for the key source to
the resultant compiled key.

This allows sharing of the same compiled key across signing
parameters that otherwise vary, and should help to shave off
some latency.
2025-02-06 16:47:15 -07:00
Wez Furlong 832121e337 dkim: avoid creating unused threads for SIGN_POOL
We only use the blocking threads portion of the thread pool,
so avoid the default of creating number-of-cores threads
for the IO portion.  We have to create one even though we
don't use it, otherwise tokio is unhappy.
2025-02-06 16:47:14 -07:00
Wez Furlong 3d22151d14 add SingletonTimerWheelV2 strategy option
This uses our new leaner timeq data structure; that has lower
memory overhead and is a bit more efficient at ticking.

It is not the default at this time, but ideally will become
the default in the future once it has been proven out.
2025-02-06 13:38:06 -07:00
Wez Furlong 7a12d37b5b roll our own timeq
This one uses our intrusive list so that the memory overhead is
constant, and allows us to iterate the contents, retain() style.
2025-02-06 12:38:08 -07:00
Wez Furlong cf09289579 dkim: allow duration strings to be used for signer ttls
This is a nice convenience
2025-02-05 11:22:46 -07:00
Wez Furlong 41b6e79c4e tidy up chrono timedelta math
I'm investigating an issue where a block of messages appear to be stuck
in a scheduled state.  In looking at the math here, I'm wondering if it
is possible for some overflow to happen when converting from chrono
to std time types, so adjust the logic to be less prone to such a thing,
if it is occurring.
2025-01-24 15:11:03 -07:00
Wez Furlong cf6f3540e7 Add new Delay log record type
The purpose of this record is to log additional context about why
a message might end up in the scheduled queue when it hasn't
logged a TransientFailure.

There are a few situations around handling throttles and limits
where we might put a message back into the scheduled queue, without also
logging a TransientFailure record. It's possible that we should
reconsider some of those, but for the moment, there is an observability
hole that needs to be filled.

What this commit does is introduce an `InsertContext` which can hold one
or more `InsertReason`s about why a message is being inserted into the
scheduled queue.

There are 3 primary reasons for insertion:

* Received - the message was just received/injected
* Enumerated - the message was discovered in spool enumeration
* DueTimeWasReached - the message is now due for delivery and is being
  popped off the scheduled queue

The additional reasons can be added to the context to provide more
color about what happened.

When a message is added to the scheduled queue, the accumulation
in the InsertContext is examined, and if the context doesn't
indicate that the message was Enumerated and it wasn't also
already logged as a TransientFailure, a `Delay` record is
logged.

The `Delay` record includes in its `response.content` the ordered set of
InsertReasons as well as the delay duration and due time.

Logging Delay records might place undesirable pressure on the
logging storage, so you may wish to disable it via:

```lua
kumo.configure_local_logs {
  per_record = {
    Delay = {
      -- Suppress Delay records
      enable = false
    }
  }
}
```

or similar.
2025-01-17 13:21:47 -07:00
Wez Furlong 6efff15983 rfc5321 parser: allow utf8 in mailboxes
This is a step towards supporting SMTPUTF8.

This commit allows MAIL FROM and RCPT TO to contain UTF8.

UTF8 in the domain portion is converted to an A-label form
via the IDNA crate, raising an error if it is invalid.

UTF8 in the local part is unchanged.

The SMTP server now explicitly checks to see if the provided
MAIL FROM or RCPT TO is non-ascii, and rejects it; we won't
try to relay UTF8 local parts as a result of this change,
but will now accept UTF8 domain parts and relay, because we
rewrite those via IDNA.

refs: https://github.com/KumoCorp/kumomta/issues/327
2025-01-11 06:53:43 -07:00
Wez Furlong 68c3606420 ready_queue: use MessageList when taking ready queue
Now that we have doubly-linked-lists of Messages for these things,
it is more efficient to take the whole list when we're doing
bulk ready queue operations, so let's do that.
2024-12-10 17:55:12 -07:00
Wez Furlong c13dde9ecb message: introduce intrusive MessageList type, replace Fifo internals
I've been recently troubleshooting a couple of systems with high memory
usage, and in one of them there were very large amounts of
memory being allocated to ready queues.  That could be partially
mitigated by reducing `max_ready` to a more reasonable and small value,
but it is difficult to compute the right balance between large-enough
for high throughput and small-enough to keep memory usage reasonable.

This commit switches away from crossbeam's ArrayQueue, which
pre-allocates sufficient space to hold exactly `max_ready` messages for
each instantiated ready queue, and to a newly introduced MessageList,
which is an intrusive doubly-linked list.

The intrusive list, in exchange for some small additional overhead
per-Message, requires no auxilliary additional memory allocations to
track the membership of that Message in some other list.

That means that `max_ready` is no longer a pre-allocated minimum amount
of additional storage, and changes the memory overhead from
`O(number-of-queues * max_ready)` to `O(number-of-ready-messages)`,
which is typically a lot smaller.  This is independent of the individual
messages metadata and bodies that are nominally associated with being in
a ready queue.
2024-12-10 14:29:41 -07:00
Wez Furlong 62672a2fbd deps: upgrade mlua to 0.10
This is a reasonably significant upgrade, as it allows for
async functions to be Send, which could unlock more efficiencies
in our overall scheduling.

This commit is just the basic changes required to get the updated
mlua version compiling, it doesn't change anything else.
2024-12-07 09:17:54 -07:00
Dirkjan Ochtman eead204a2f Remove unused dependencies 2024-11-13 13:50:27 -07:00
Dirkjan Ochtman 18db31f204 Manage all external dependencies at the workspace level 2024-11-13 13:50:27 -07:00