Stalwart 0.16.22: nested mailing list accepted over SMTP, then inner list bounces with “Mailbox not found”

Issue Description

I’m testing native nested mailing-list delivery using the published Stalwart Docker image. Direct delivery to an inner list works, but delivery through an outer list containing that inner list fails.

The mailing-list documentation explicitly permits nested lists without extra configuration. Could you confirm whether this is a bug or whether I have missed a required setting?

Environment

  • Stalwart executable version: 0.16.22
  • Docker image:
    stalwartlabs/stalwart:v0.16.22@sha256:388dcb75a70727c5b551249a6d34b1f1321294852489e4fa3a4e6be698b7c4f0
    
  • Image source revision:
    474dd0229cb20cf513036619781ed97bd8073c3f
    
  • Reproduction date: September 15, 2026.
  • Native accounts and mailing lists configured through the administrative JMAP API.
  • Unmodified published binary; no source patches.
  • Disposable local Docker environment. Local recipients use Stalwart’s local-delivery route. Diagnostic outbound mail goes to an isolated SMTP capture service.
  • Runtime internet access was disabled. DNS-related authentication checks consequently report temporary errors; the direct inner-list control still delivers successfully.

For this reproduction, all MTA hooks were removed and recipient rewriting was disabled before restarting Stalwart. Readback confirmed an empty hook inventory, and the policy service received no SMTP callbacks. Messages were submitted directly to Stalwart’s SMTP listener.

Minimal group structure

These are the synthetic addresses used in the test:

Local user:
  [email protected]

Inner mailing list:
  [email protected]
  recipients:
    [email protected]

Outer mailing list:
  [email protected]
  recipients:
    [email protected]

Both list memberships were read back after configuration and matched these values. The addresses are direct native list addresses; neither depends on an alias, catch-all, or pattern rewrite.

There are no cyclic memberships in this reproduction.

Steps to reproduce

  1. Create the local domains mydomain.test and public.test.
  2. Create the user and mailing lists shown above.
  3. Send an ordinary SMTP message directly to [email protected].
  4. Verify that the user receives one Inbox copy.
  5. Send a separate message directly to [email protected], also naming that address in the visible To header.
  6. Inspect the user’s mailbox, delivery logs, and generated delivery-status notification.

Expected behavior

Both messages reach the user once:

family → user

person1-person2 → family → user

The second message retains [email protected] in its visible To header.

Actual behavior

SMTP recipient Result
[email protected] The user receives one Inbox copy.
[email protected] SMTP accepts the message, but the user receives no copy. A bounce identifies [email protected] as a missing mailbox.

For the failing message, SMTP returned:

MAIL FROM: 250 2.1.0 OK
RCPT TO:   250 2.1.5 OK
DATA:      250 2.0.0 Message queued with id 49303de81600200.

The queue log shows the inner list itself as the recipient:

queue.message-queued
queueId = 329611451916354048
from = "[email protected]"
to = ["[email protected]"]

Local delivery subsequently reports:

Unexpected response for RCPT TO:<[email protected]>:
Code: 550, Enhanced code: 5.5.0, Message: Mailbox not found.

The captured delivery-status notification contains:

Original-Recipient: rfc822;rfc822;[email protected]
Final-Recipient: rfc822;[email protected]
Action: failed
Status: 5.5.0
Diagnostic-Code: smtp;550 Mailbox not found.
Remote-MTA: dns;localhost

The doubled rfc822; is copied exactly from the notification. I have not established whether it is a separate formatting issue.

Controls performed

  • Direct delivery to the inner list succeeds with the same membership configuration.
  • Failure reproduces with all MTA hooks removed and recipient rewriting disabled.
  • Stalwart was restarted after those changes.
  • Both list memberships were verified through API readback.
  • Delivery was checked through JMAP mailbox inspection, not inferred from SMTP acceptance.
  • The failing case needs only two mailing lists and one user.

Possible source location

In the matching revision’s list-expansion block in crates/smtp/src/inbound/rcpt.rs, each non-account member is resolved using rcpt_resolve.

The code handles RcptResolution::Rewrite specially, while other successful results retain the member’s original address. My interpretation is that a nested list returning RcptResolution::Expand consequently remains queued as a mailbox recipient instead of being expanded recursively.

This is a suspected explanation, not a verified patch, but it matches the queue and bounce observations.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

SQLite

Blob Storage

SQLite

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

Your analysis is correct. In rcpt.rs, when a mailing list member is itself another list, rcpt_resolve is only special-cased for RcptResolution::Rewrite; every other successful outcome (including RcptResolution::Expand, which is what a nested list returns) falls through to Ok(_) => member.to_string(), so the nested list’s own address is queued verbatim as a recipient instead of being recursively expanded into its members. That’s exactly why [email protected] ends up as the final queued recipient and then bounces with “Mailbox not found” once local delivery tries to treat the list address as a mailbox.

This does contradict the documented behavior that nested lists are expanded automatically (see the mailing-lists docs: Mailing lists | Stalwart), so it looks like a genuine bug rather than a missing configuration step on your part. The doubled rfc822;rfc822; in the DSN’s Original-Recipient header is a separate, likely cosmetic, formatting issue in the same code path.

A human maintainer will need to confirm and address the fix for recursive list expansion; your reproduction and source pointer are solid for that follow-up.

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

Fixed in v0.16.23 (yet to be released).