Tls error messages in MTAOutboundStrategy

Your question

Hi,

I have Stalwart 0.16.11

I want to change the TLS in Outbound Strategies in order to deliver emails to domains with this kind of error:
2026-07-03T11:34:26Z INFO STARTTLS error (delivery.start-tls-error) queueId = 316212629099997184, queueName = “remote”, from = “[email protected]”, to = [“[email protected]”, “[email protected]”], size = 194896, total = 3, domain = “destinationdomain.ro”, hostname = “mail.destinationdomain.ro”, reason = SMTP error occurred (smtp.error) { details = “I/O Error”, reason = “tls handshake eof” }, elapsed = 8ms

I understood from documentation that I have to add condifiton to TLS Output Strategy in Settings->MTA->Outbound->Strategy similar with this one:

if retry_num > 0 && last_error == ‘tls’
‘invalid-tls’

but I don’t know what string should I put in comparison of last_error. It should be

  • tls handshake eof
  • STARTTLS error
    or something else?

Thanks,
Andrei

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The last_error variable holds a category token, not the raw error text, so you compare against 'tls' (not the free-text message shown in the log). Your example config is already correct as-is: last_error == 'tls' will match any TLS-related delivery failure, including the “tls handshake eof” case you’re seeing, since that error is classified internally as a TLS error type.

Let me confirm the full list of possible last_error values.

last_error is a categorical error type, not the literal text of the log message. For a STARTTLS handshake failure like yours, the value to compare against is 'tls' (not the raw string “tls handshake eof” or “STARTTLS error”).

So the condition you found in the docs is already correct as-is:

if retry_num > 0 && last_error == 'tls'
    'invalid-tls'

last_error is a fixed enum populated by Stalwart itself from the delivery outcome category, with possible values: dns, unexpected-reply, connection, tls, dane, mta-sts, rate, concurrency, io, none. Your log entry (“STARTTLS error”, “tls handshake eof”) falls under the tls category, so last_error == 'tls' is exactly the match you want. You don’t need to (and can’t) match on the literal log text.

A documented full example (from the outbound TLS docs) uses progressively relaxed strategies:

"tls": {
  "match": [
    {"if": "retry_num > 0 && last_error == 'tls'", "then": "'invalid-tls'"},
    {"if": "retry_num > 1 && last_error == 'tls'", "then": "'disable-tls'"}
  ],
  "else": "'default'"
}

This sends a first retry using an invalid-tls MtaTlsStrategy (relaxed TLS settings) and a second retry using disable-tls (falls back to unencrypted delivery), while all other cases use default.

Reference: TLS | Stalwart

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Then it is a bug somewhere because I have email with this kind of error in queue for a couple of days. I don’t know how to list the queue from stalwart-cli to show you the number of retries for domains with TLS errors.

Please tell me what kind of info will help you to identify the problem.

The comparison value is just tls, not the log text: the STARTTLS handshake error maps to the last_error value tls. The full list of last_error values (dns, unexpected-reply, connection, tls, dane, mta-sts, rate, concurrency, io, none) is at Variables | Stalwart . In fact the rule you’re writing, retry_num > 0 && last_error == ‘tls’, is already the built-in default outbound strategy.

For it to actually help, the target of that rule (e.g. invalid-tls) has to resolve to a TLS strategy that makes TLS optional or allows invalid certs, otherwise the rule has no effect. The “tls handshake eof” means the remote MX aborts the STARTTLS handshake; the reason a message sits in the queue for days is that a non-strict STARTTLS error is retried as a temporary failure rather than auto-downgrading, so the strategy rule is what triggers the downgrade on retry. You can check retry counts with stalwart-cli queue list and the message details. Strategy reference: MtaOutboundStrategy | Stalwart .

Hi,

I’m using the built-in default outbound strategy:

if retry_num > 0 && last_error == ‘tls’ then

‘invalid-tls’

else

'default'

So I was expecting the after STARTTLS error at the first attempt to retry without STARTLS. Unfortunately this is not happening, it is still attempting using STARTTLS until:

INFO DSN permanent failure notification (delivery.dsn-perm-fail) queueId = 315890354795869184, queueName = “remote”, from = “[email protected]”, to = [“[email protected]”], size = 103112, total = 1, to = “[email protected]”, hostname = “mx1.destination.ro”, details = “TLS error: I/O error: tls handshake eof”, total = 9

I still didn’t understood how to use stalwart-cli for queue list:

sudo /opt/stalwart-cli/stalwart-cli --url https://mail.sourcedomain.ro --user andrei.miron queue list
error: unrecognized subcommand ‘queue’

tip: a similar subcommand exists: ‘query’

Usage: stalwart-cli [OPTIONS]

For more information, try ‘–help’.

Thanks,

The invalid-tls strategy only relaxes certificate validation, and a “tls handshake eof” isn’t a bad-cert error, it’s the remote aborting the STARTTLS handshake. So switching to invalid-tls won’t help that host. To actually deliver to it you need a TLS strategy with STARTTLS set to Disable, targeted from your retry rule; that skips STARTTLS and delivers in plaintext. Making STARTTLS merely Optional isn’t enough, because a STARTTLS handshake error is currently retried as a temporary failure rather than downgraded, which is why the message sits in the queue for days retrying until it expires.

On the CLI: there’s no queue subcommand in this version (hence the “did you mean query” hint). The queue is exposed as an object, so use stalwart-cli query QueuedMessage to list it; each recipient shows its retry count and next retry time. The web UI queue view shows the same. See MtaTlsStrategy | Stalwart.