DMARC reports never use a defined 'report' schedule/virtual queue - always enqueued on 'remote' with no warning

Issue Description

This is a follow-up to the finding in “DMARC Report: Strategy Not Found when STARTTLS Fails” (topic 1317), where the suggested remedy for the Strategy not found warning is to create an MtaDeliverySchedule named report.

We have exactly that configuration — a report delivery schedule AND a report virtual queue, with the outbound strategy’s Scheduling expression matching the canonical example in the docs — and the report schedule/queue still never engages. Every outgoing DMARC aggregate report is enqueued and delivered on queueName = "remote", with no warning logged.

Configuration, read back via stalwart-cli:

MtaVirtualQueue:

Id Name Delivery Threads
d report 5
c dsn 5
b remote 50
a local 25

MtaDeliverySchedule: four schedules — local → queue local, remoteremote, dsndsn, reportreport (id: d).

MtaOutboundStrategy (singleton), Scheduling expression:

Conditions:
  0  is_local_domain(rcpt_domain)  'local'
  1  source == 'dsn'               'dsn'
  2  source == 'report'            'report'
Default Value: 'remote'

Observed:

  • This server generates a substantial volume of outgoing DMARC aggregate reports as a receiving MTA (dozens to hundreds of queue.report-queued events per day), so the report branch has had thousands of chances to fire.
  • Across 7 days of logs (~7,200 queue/delivery events carrying a queueName field), the only values that appear are local (4,729) and remote (2,451). report and dsn appear zero times.
  • An end-to-end trace of a single aggregate report on v0.16.17 (log excerpt below) shows every event from queue.report-queued through delivery.delivered with queueName = "remote".
  • Unlike topic 1317 (schedule missing), no smtp.id-not-found warning is ever logged — the message simply never lands on the report queue.
  • The Routing expression on the same strategy object evaluates correctly (is_local_domain(rcpt_domain) routes local vs relay as configured), so expression evaluation as such works; it is specifically the source-based Scheduling branches that appear inert at queue-placement time.

Behaviour is identical on 0.16.15 and 0.16.17 (the trace below is post-upgrade to 0.16.17).

Expected Behavior

Per the Scheduling documentation (Scheduling | Stalwart), a message generated as an aggregate report should match source == 'report' and be placed on the report delivery schedule, whose queueId maps it to the report virtual queue — i.e. log events for report deliveries should carry queueName = "report" (and DSNs queueName = "dsn").

Actual Behavior

Every aggregate report is enqueued and delivered with queueName = "remote" (the expression’s default). The report and dsn queue names have never appeared in any log line — 7 days / ~7,200 queueName-carrying events contain only local and remote — despite dozens-to-hundreds of reports being generated daily. No smtp.id-not-found warning is logged (the schedule exists), the report queue is simply never selected.

Reproduction Steps

  1. Define an MtaVirtualQueue named report and an MtaDeliverySchedule named report referencing it (and likewise dsn), alongside the stock local/remote pair.
  2. Keep the outbound strategy’s Scheduling expression as the documented example: is_local_domain(rcpt_domain) -> 'local', source == 'dsn' -> 'dsn', source == 'report' -> 'report', default 'remote'.
  3. Operate as a receiving MTA with incoming mail from DMARC-publishing domains so outgoing aggregate reports are generated (queue.report-queued events).
  4. Observe queueName on the subsequent delivery events for those reports: always remote, never report.

Relevant Log Output

2026-08-11T18:00:34Z INFO Queued report for delivery (queue.report-queued) listenerId = “smtp”, localPort = 25, remoteIp = x.x.x.x, remotePort = 56268, queueId = 323362456101468672, from = “[email protected]”, to = [“[email protected]”], size = 3865, nextRetry = 2026-08-11T18:00:34Z, nextDsn = 2026-08-12T18:00:34Z, expires = 2026-08-14T18:00:34Z
2026-08-11T18:00:34Z INFO Delivery attempt started (delivery.attempt-start) queueId = 323362456101468672, queueName = “remote”, from = “[email protected]”, to = [“[email protected]”], size = 3865, total = 1
2026-08-11T18:00:34Z INFO New delivery attempt for domain (delivery.domain-delivery-start) queueId = 323362456101468672, queueName = “remote”, from = “[email protected]”, to = [“[email protected]”], size = 3865, total = 1, domain = “redacted.tld”
2026-08-11T18:00:34Z INFO Connecting to remote server (delivery.connect) queueId = 323362456101468672, queueName = “remote”, from = “[email protected]”, to = [“[email protected]”], size = 3865, total = 1, domain = “redacted.tld”, hostname = “mail.btinternet.com”, localIp = x.x.x.x, remoteIp = x.x.x.x, remotePort = 465, elapsed = 6ms
2026-08-11T18:00:38Z INFO Message delivered (delivery.delivered) queueId = 323362456101468672, queueName = “remote”, from = “[email protected]”, to = [“[email protected]”], size = 3865, total = 1, hostname = “mail.btinternet.com”, code = 250, details = “Mail accepted”, elapsed = 186ms
2026-08-11T18:00:38Z INFO Delivery completed (delivery.completed) queueId = 323362456101468672, queueName = “remote”, from = “[email protected]”, to = [“[email protected]”], size = 3865, total = 1, elapsed = 4000ms

7-day queueName histogram (ANSI colour stripped before grepping):

4729 queueName = “local”

2451 queueName = “remote”

(zero occurrences of “report” or “dsn”)

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

Exact versions: observed on v0.16.15 and v0.16.17 (trace in the log output is from 0.16.17). The report MtaDeliverySchedule/MtaVirtualQueue objects were created ~2 weeks before the trace, so this is steady-state behaviour, not a config-propagation race (the server has been restarted several times since, including for the 0.16.17 upgrade). Happy to provide full object dumps or run instrumented tests against this server if useful.

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

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