Custom SubAdressing no longer seems to work after upgrade to 0.16

Issue Description

custom SubAdressing no longer seems to work after upgrade to 0.16

Expected Behavior

username.xxx@domain should be delivered to the existing account username@domain

Actual Behavior

5.5.0, Message: Mailbox not found.

Reproduction Steps

  1. Configure stalwart Domanin → Email → Sub-Addressing → Enable custom sub addressing → if is matches(‘^([^.]+)\\.([^.]+)$’, rcpt) , then is $1 , and else is rcpt.
  2. send a email to username.xxx@domain
  3. “5.5.0, Message: Mailbox not found” error shown in log

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

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 issue is a key behavioral change in how the Custom sub-addressing expression works. The customRule expression now operates only on the local part (rcpt), not the full email address. The domain is handled separately by the server.

Your current expression:

ifis matches('^([^.]+)\.([^.]+)@(.+)$', rcpt), then is $2 + '@' + $3

…is trying to match and return a full localpart@domain string, but rcpt in this context now only contains the local part (no @domain). So $2 + '@' + $3 returns something like xxx@domain, which the server cannot find as a local part, causing “Mailbox not found.”

The fix: Update your customRule to match only the local part and return only the local part. For example, to deliver username.xxx@domain to username@domain:

{
  "subAddressing": {
    "@type": "Custom",
    "customRule": {
      "match": [{"if": "matches('^([^.]+)\\.([^.]+)$', rcpt)", "then": "$1"}],
      "else": "rcpt"
    }
  }
}

Here rcpt is just username.xxx (no domain), and $1 returns username. The server automatically preserves the domain.

Note also that the else should return rcpt (the unchanged local part) rather than false if you want non-matching addresses to pass through normally. Returning false would cause the server to reject addresses that don’t match the pattern.

See the Subaddressing docs and the customRule field reference for full details.

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

Let me look into the sub-addressing configuration to understand how it works in the current version.

The issue is with the custom subAddressing rule expression. There are two key problems in the configuration described:

1. The customRule expression must operate only on the local part (rcpt), not the full email address.

In the current docs, the customRule receives only the local part of the recipient address (e.g. username.xxx), not the full username.xxx@domain. The domain is preserved automatically by the server. This means the regex and the then expression need to be adjusted accordingly.

2. The else value should return rcpt (the unchanged local part), not false.

Returning false causes the address to be treated as unresolvable, which leads to the “Mailbox not found” error.

Here is the corrected configuration based on the current docs:

{
  "subAddressing": {
    "@type": "Custom",
    "customRule": {
      "match": [{"if": "matches('^([^.]+)\\.([^.]+)$', rcpt)", "then": "$2"}],
      "else": "rcpt"
    }
  }
}

Key differences from what you had:

  • The regex is '^([^.]+)\\.([^.]+)$' (no @domain part – just the local part)
  • The then value is "$2" (just the second capture group, e.g. username)
  • The else value is "rcpt" (pass the local part through unchanged), not false

Your original expression included @(.+)$ and referenced $3, but since the domain is not passed into customRule, matching against @domain will never succeed, causing every recipient to fall through to else: false and be rejected.

You can update this through the WebUI under Management > Domains > [your domain] > Sub-Addressing, or via the stalwart-cli.

Reference: Subaddressing documentation

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

Let me look into the sub-addressing configuration to understand what might have changed.

The issue is a key behavioural change in the custom sub-addressing expression: the customRule expression now receives only the local part of the recipient (i.e. just username.xxx), not the full address username.xxx@domain. This means the regex and the output must be adjusted accordingly.

Here is what went wrong with the configuration:

Old/broken rule (using full address rcpt):

"if": "matches('^([^.]+)\\.([^.]+)@(.+)$', rcpt)", "then": "$2 + '@' + $3"

This was written expecting rcpt to be username.xxx@domain, but customRule now receives only username.xxx as rcpt.

Correct rule (matching only the local part):

{
  "subAddressing": {
    "@type": "Custom",
    "customRule": {
      "match": [{"if": "matches('^([^.]+)\\.([^.]+)$', rcpt)", "then": "$2"}],
      "else": "rcpt"
    }
  }
}

From the docs:

The customRule expression receives the recipient local part as rcpt and returns the rewritten local part. The domain is preserved by the server and is not part of the value passed in or out.

So the fix is:

  1. Change your regex to match only username.xxx (no @domain part): '^([^.]+)\.([^.]+)$'
  2. Return only the local part you want to deliver to (e.g. $2 for username), not $2 + '@' + $3
  3. Set else to rcpt (pass through unchanged) rather than false (which would cause rejection)

Using false as the else value may also be contributing to the “Mailbox not found” error for addresses that don’t match the pattern.

Relevant docs: Subaddressing - Inbound RCPT and the Domain object reference.

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

Let me look into the sub-addressing configuration for the current version.

The issue is clear. The custom subaddressing expression in the user’s configuration is using the wrong capture group – it’s matching $1 (the part before the dot) instead of $2 (the part after the dot), and the expression syntax has also changed.

Here’s the problem with the rule configured:

if is_matches('^([^.]+)\.([^.]+)$', rcpt), then $1, else rcpt

$1 captures the prefix (e.g., username from username.xxx), but that would strip the suffix and deliver to username@domain. Actually that should work in theory – but the syntax/function name has changed.

The key issues are:

  1. Function name change: The expression function is matches(...), not is_matches(...). The correct syntax is:

    matches('^([^.]+)\.([^.]+)$', rcpt)
    
  2. Expression format: The customRule must be a structured JSON expression with match/if/then/else fields. The correct working configuration from the current docs is:

{
  "subAddressing": {
    "@type": "Custom",
    "customRule": {
      "match": [{"if": "matches('^([^.]+)\\.([^.]+)$', rcpt)", "then": "$1"}],
      "else": "rcpt"
    }
  }
}

Note "then": "$1" returns the part before the dot (so username.xxx → delivers to username). If you want the part after the dot to be the mailbox, use "then": "$2" instead. Make sure you set this via the WebUI or stalwart-cli on the Domain object under Management > Domains > [your domain] > Sub-Addressing > Custom.

For reference, the docs show an example where [email protected] delivers to [email protected] using $2:

{"if": "matches('^([^.]+)\\.([^.]+)$', rcpt)", "then": "$2"}

…which is the opposite direction – so double-check which capture group you need for your use case.

Docs: RCPT Stage - Subaddressing

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

if “matches(‘^([^.@]+).[^@]+’, rcpt)”
then “$1”
else “rcpt”
this config resolves the issue