DMARC Invalid Report Schema Causing Loop

Issue Description

A piece of spam email triggered a DMARC report to Cloudflare which has been floating in my queue. I notice three issues:

  1. The next DSN delivery date is 2083,
  2. The DMARC report is rejected due to a schema error, and,
  3. After being rejected the report goes back to the queue for further rejection instead of permanently dying.

Expected Behavior

DSNs should have a reasonable date, the schema requires examination/correction, and if a DMARC report is rejected it shouldn’t try again.

Reproduction Steps

I cannot reproduce manually, this was an automated response to a piece of spam that has a DMARC rua configured.

Relevant Log Output

2026-08-02T21:00:00Z INFO New delivery attempt for domain (delivery.domain-delivery-start) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, domain = "dmarc-reports.cloudflare.net"
2026-08-02T21:00:00Z INFO MTA-STS policy not found (mta-sts.policy-not-found) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, domain = "dmarc-reports.cloudflare.net", strict = false, elapsed = 62ms
2026-08-02T21:00:00Z INFO Fetching TLSA record (dane.tlsa-record-fetch) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, domain = "dmarc-reports.cloudflare.net", hostname = "route2.mx.cloudflare.net", details = "Tlsa { entries: [TlsaEntry { is_end_entity: false, is_spki: true, matching: Sha256, data: [89, 231, 56, 230, 116, 34, 23, 2, 175, 30, 219, 135, 197, 32, 12, 26, 75, 117, 246, 79, 174, 61, 44, 61, 38, 81, 36, 198, 27, 216, 60, 121] }, TlsaEntry { is_end_entity: true, is_spki: true, matching: Sha256, data: [15, 12, 108, 22, 74, 54, 249, 126, 123, 76, 90, 91, 105, 214, 244, 242, 57, 212, 34, 252, 62, 194, 89, 32, 114, 236, 250, 184, 194, 113, 196, 82] }, TlsaEntry { is_end_entity: true, is_spki: true, matching: Sha256, data: [155, 229, 183, 212, 127, 158, 255, 147, 12, 17, 235, 4, 210, 124, 174, 107, 221, 208, 211, 117, 205, 251, 204, 246, 139, 38, 239, 230, 118, 209, 208, 219] }], has_end_entities: true, has_intermediates: true }", strict = false, elapsed = 55ms
2026-08-02T21:00:00Z INFO Connecting to remote server (delivery.connect) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, domain = "dmarc-reports.cloudflare.net", hostname = "route2.mx.cloudflare.net", localIp = 10.0.1.100, remoteIp = 162.159.205.17, remotePort = 25, elapsed = 16ms
2026-08-02T21:00:00Z INFO SMTP STARTTLS command (delivery.start-tls) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, domain = "dmarc-reports.cloudflare.net", hostname = "route2.mx.cloudflare.net", version = "TLSv1_2", details = "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", elapsed = 49ms
2026-08-02T21:00:00Z INFO TLSA record match found (dane.tlsa-record-match) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, hostname = "route2.mx.cloudflare.net", type = "end-entity"
2026-08-02T21:00:00Z INFO DANE authentication successful (dane.authentication-success) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, hostname = "route2.mx.cloudflare.net"
2026-08-02T21:00:01Z INFO Message rejected by remote server (delivery.message-rejected) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, hostname = "route2.mx.cloudflare.net", code = 555, details = "invalid_report_schema. LwoVAX8t53au", elapsed = 435ms
2026-08-02T21:00:01Z INFO DSN permanent failure notification (delivery.dsn-perm-fail) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, to = "[email protected]", hostname = "route2.mx.cloudflare.net", details = "Unexpected response for DATA: Code: 555, Enhanced code: 5.7.1, Message: invalid_report_schema. LwoVAX8t53au", total = 0
2026-08-02T21:00:01Z INFO Delivery completed (delivery.completed) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, elapsed = 1598000ms
2026-08-02T21:00:01Z INFO Delivery attempt ended (delivery.attempt-end) queueId = 321750938324828161, queueName = "remote", from = "[email protected]", to = ["[email protected]"], size = 2886, total = 1, elapsed = 1075ms

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

PostgreSQL

Blob Storage

S3-compatible

Search Engine

Internal

Directory Backend

Internal

Additional Context

v0.16.15

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

Thanks for the report, only the first one was a bug though:

On the 2083 date: this was the bug, and it affected every queued message, not just DMARC reports. When a recipient is added to the queue we compute its next DSN notification time, and the current timestamp was being added twice: once explicitly, and once again inside the helper that builds the schedule. The result is a due time of roughly twice the current epoch, which at today’s timestamps lands in 2083. That is why the queue view showed that date. Beyond the cosmetic oddity it also meant the notification time never came due, so delay DSNs were never generated and only the first entry of the queue notify list was ever used. The fix removes the duplicate addition. Messages already sitting in your queue keep the stored value, so if you want the display corrected for them you will need to let them drain or requeue them, but nothing else about their delivery or expiry was affected: retry scheduling and expiration are computed independently, and the queue always takes the earliest of the three.

On the schema rejection: this is about DMARCbis. Stalwart generates aggregate reports in the format defined by RFC 9990, which is the aggregate reporting half of DMARCbis. That format declares the XML namespace urn:ietf:params:xml:ns:dmarc-2.0, drops the pct element, and adds np, testing, discovery_method and generator. The report we sent is valid against that schema. Cloudflare is validating against the original RFC 7489 Appendix C schema, which has no target namespace at all, requires pct, and knows nothing about the new elements. A namespace-aware validator using the old schema fails on the root element before it even reaches the contents, so the 555 invalid_report_schema response is what you would expect from any consumer that has not yet moved to DMARCbis. Nothing about the spam message or its DMARC record triggered this; the format does not vary by reported domain, so every aggregate report we emit is in the new form. Most consumers parse leniently and are unaffected, but strict validators like Cloudflare will reject all of them.

There is no signal in a published DMARC record that tells us which format a given consumer expects, since DMARCbis kept the v=DMARC1 tag, and RFC 9990 offers no transition guidance. We are looking at making the output format selectable, defaulting to the RFC 7489 compatible form until DMARCbis support is widespread, and flipping the default later. That means emitting the report without the new namespace, omitting the DMARCbis only elements, and including pct.

On the third point, the report is not being requeued. Your log actually shows the opposite: after the 555 you get delivery.dsn-perm-fail followed by delivery.completed, and that completion event is only emitted on the branch where the message has no further scheduled events and is deleted from the queue. The message was removed at that point. What made it look like it was still alive was the 2083 notification date from the first issue, which is exactly the confusion that bug was causing.

Appreciate the detailed explanations. It’s very refreshing to learn what is actually happening inside the machine.

I almost wrote off the 2083 date as meaningless but I’m glad I spoke up since that was the actual root.