Select smtp relay based on sender

Your question

I want to use stalwart as a kind of unified mailbox for my private and business mails. I do not want to operate my own smtp server, but send mails over my private and business servers.

Is there a way to select the outgoing stmp based on the sender address? I tried the following without success, as the mail do not seem to be relayed to the respective smtp sever:

[[directory.in-memory.principals]]
class = "individual"
email = ["[email protected]"]
name = "[email protected]"
secret = "pass"

[[directory.in-memory.principals]]
class = "individual"
email = ["[email protected]"]
name = "[email protected]"
secret = "pass"

[queue.route.private]
address = "smtp.private-server.com"
description = "private smpt relay"
port = 587
protocol = "smtp"
type = "relay"

[queue.route.private.auth]
secret = "pass"
username = "[email protected]"

[queue.route.private.tls]
allow-invalid-certs = true
implicit = true

[queue.route.business]
address = "smtp.business.com"
description = "business smpt relay"
port = 587
protocol = "smtp"
type = "relay"

[queue.route.business.auth]
secret = "pass"
username = "[email protected]"

[queue.route.business.tls]
allow-invalid-certs = true
implicit = false

[[queue.strategy.route]]
if = "sender == '[email protected]'"
then = "'private'"

[[queue.strategy.route]]
if = "sender == '[email protected]'"
then = "'business'"

[[queue.strategy.route]]
else = "'local'"
```

Let me look up how routing/relay selection based on sender works in Stalwart.

There are two issues with the config you posted. Here’s what’s wrong and how to fix it:

1. The configuration format is wrong. Stalwart is no longer configured via TOML files. Routes and routing strategies are managed as JSON objects through the JMAP API, either via the WebUI (Settings > MTA > Outbound > Routes and Settings > MTA > Outbound > Strategy) or via stalwart-cli. The TOML-style [queue.route.*] and [[queue.strategy.route]] blocks you used are from an older version and will not work.

2. The sender variable is valid for routing expressions, so your routing logic itself is conceptually correct. The MtaQueueRcptVariable (used in the route expression) does include a sender variable you can match against.

Here is what you need to create, using the current format:

Two relay routes (create each via the WebUI under Settings > MTA > Outbound > Routes, or via the JMAP API):

{
  "@type": "Relay",
  "name": "private",
  "address": "smtp.private-server.com",
  "port": 587,
  "protocol": "smtp",
  "implicitTls": true,
  "allowInvalidCerts": true,
  "authUsername": "[email protected]",
  "authSecret": {"@type": "Value", "secret": "pass"}
}
{
  "@type": "Relay",
  "name": "business",
  "address": "smtp.business.com",
  "port": 587,
  "protocol": "smtp",
  "implicitTls": false,
  "allowInvalidCerts": false,
  "authUsername": "[email protected]",
  "authSecret": {"@type": "Value", "secret": "pass"}
}

Update the MtaOutboundStrategy route expression (Settings > MTA > Outbound > Strategy) to:

{
  "route": {
    "match": [
      {"if": "is_local_domain(rcpt_domain)", "then": "'local'"},
      {"if": "sender == '[email protected]'", "then": "'private'"},
      {"if": "sender == '[email protected]'", "then": "'business'"}
    ],
    "else": "'mx'"
  }
}

Note that implicitTls is the field name (camelCase JSON), not implicit. Also make sure the route names in your strategy expression exactly match the name fields in your route objects.

You can also manage all of this through the WebUI without writing JSON directly.

Relevant docs:

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

The WebUI says my conifg is valid. So the bot response is not helpful.

Looking at your follow-up, it seems the TOML config is actually being accepted by your instance. Let me check what version of the configuration format is actually in use.

Thank you for the clarification. If the WebUI is already accepting the configuration as valid, then the TOML concern from the previous reply doesn’t apply to your setup. Let’s focus on what could actually be going wrong.

Here are the most likely causes when the routes exist and the strategy expression looks correct but mail is still going out via the wrong path:

1. Verify what the strategy actually saved

The WebUI’s “valid” check only validates the expression syntax, not whether it references routes that exist. Use stalwart-cli get MtaOutboundStrategy (or the equivalent JMAP GET call) to print the live route expression and confirm your sender-matching rules are actually present in the saved object, not just in the editor.

2. Confirm the route names match exactly

The then value in the route expression must be an exact string match to the name field of the MtaRoute object. If you named the route "private" but the expression returns "'Private'" (or vice versa), no relay is selected. Run stalwart-cli get MtaRoute to list all defined routes and check the names.

3. Check what sender actually contains at delivery time

The sender variable in a MtaQueueRcptVariable context holds the SMTP envelope MAIL FROM address. If your mail client sends with a different envelope sender than what you’re matching (e.g. [email protected] vs [email protected]), the condition won’t fire. Check the server logs for the actual envelope sender on a sent message.

4. Confirm the is_local_domain check is not capturing the mail first

If your private or business domains are configured as local domains in Stalwart, the default is_local_domain(rcpt_domain) branch will route outbound mail to 'local' delivery (i.e. into a local mailbox) before your sender conditions are ever evaluated. Since routing is evaluated per recipient, outbound mail to external addresses should not hit this branch - but it is worth confirming the recipient domains are genuinely external.

The server logs (under Management > Logs in the WebUI, or in your log output) will show which route was selected for each delivery attempt, which is the fastest way to pinpoint where the mismatch is.

Docs: MtaOutboundStrategy | MtaRoute | Routing

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

Stalwart seems to try a local delivery instead of using the relay servers.

Let me look into the local delivery vs relay routing issue more carefully.

This is the key issue: the sender address ([email protected] or [email protected]) is a local account in your in-memory directory, so is_local_domain(rcpt_domain) is firing for the recipient’s domain when you’re sending mail to yourself or to other local addresses, routing everything to 'local'.

But more likely, the problem is the order of conditions in the route expression. The default route expression (and likely what you have) checks is_local_domain(rcpt_domain) first. If the recipient’s domain happens to be registered as a local domain in Stalwart, that branch fires and returns 'local' before your sender conditions are ever reached.

The fix is to make your sender-based rules come before the is_local_domain check in the match array, and only fall through to 'local' if the recipient is actually local AND the sender isn’t one of your relay accounts:

{
  "route": {
    "match": [
      {"if": "sender == '[email protected]'", "then": "'private'"},
      {"if": "sender == '[email protected]'", "then": "'business'"},
      {"if": "is_local_domain('', rcpt_domain)", "then": "'local'"}
    ],
    "else": "'mx'"
  }
}

Expressions are evaluated in order - the first matching condition wins. By putting the sender checks first, outbound mail from your relay accounts will always be directed to the correct relay regardless of what the recipient domain is.

You can update this in the WebUI under Settings > MTA > Outbound > Strategy, or via stalwart-cli update MtaOutboundStrategy.

Relevant docs: Strategies | Routing | MtaOutboundStrategy

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

I do not have this line in my config, but else: “‘local’”

The issue is your else clause. When none of the match conditions fire, else: "'local'" routes everything to local delivery as a fallback. Since your sender conditions apparently aren’t matching, all mail falls through to that else and gets delivered locally.

Two things to fix:

  1. Change else from "'local'" to "'mx'" so unmatched mail goes out via MX rather than into the local store.
  2. Make sure your sender conditions are listed first in the match array, before any is_local_domain check.

Your corrected route expression should look like this:

{
  "route": {
    "match": [
      {"if": "sender == '[email protected]'", "then": "'private'"},
      {"if": "sender == '[email protected]'", "then": "'business'"},
      {"if": "is_local_domain('', rcpt_domain)", "then": "'local'"}
    ],
    "else": "'mx'"
  }
}

The is_local_domain line handles mail addressed to users on your own server (so it lands in local mailboxes as expected), while anything sent by your relay accounts goes to the matching relay, and everything else goes out via standard MX. The else: "'local'" you currently have as a catch-all is what’s swallowing your outbound mail.

Update this in the WebUI under Settings > MTA > Outbound > Strategy.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

This bot is not helpful. no-ai.

The user has tagged the topic no-ai and explicitly stated the bot is not helpful. I should not respond further in this topic.

Per the no-ai tag on this topic, I’m stepping back. A human community member or staff member will follow up.

You are using an old Stalwart version, the bot only knows about 0.16.x. Consider upgrading.