Bug: DSN bounces are emitted with a malformed Message-ID (doubled angle brackets)

Issue Description

Delivery Status Notifications are generated with doubled angle brackets around the Message-ID:

Message-ID: <<18cad7807852b60b.f790fe4fd1ab9090.76e3c80726c2224c@mail.example.org>>

RFC 5322 defines msg-id = "<" id-left "@" id-right ">" with id-left = dot-atom-text; < is not valid atext, so this header is malformed. Consumers that key on Message-ID — threading, duplicate suppression, loop detection, log correlation — may fail to match it, and strict parsers reject it.

The message being bounced had a well-formed, single-bracketed Message-ID, so the doubling is introduced by the DSN generator rather than inherited from the original.

Probable cause: crates/smtp/src/queue/dsn.rs:339 wraps the id in angle brackets before handing it to the builder:

.message_id(format!("<{}@{}>", make_boundary("."), reporting_mta))

mail-builder then adds its own, unconditionally, in src/headers/message_id.rs:106:

output.write_all(b"<")?;
output.write_all(id.as_bytes())?;
output.write_all(b">")?;

MessageId takes the bare id and owns the delimiters, so the caller should not supply them. Dropping the brackets at the call site looks like it would fix it:

.message_id(format!("{}@{}", make_boundary("."), reporting_mta))

crates/http/src/form/mod.rs:160 pre-wraps in the same way before calling .message_id(). I have not observed that path in the wild, but it goes through the same builder.

For contrast, crates/imap/src/op/fetch.rs:881 also wraps in angle brackets, but that value is not passed to mail-builder, so it looks correct as written.

Expected Behavior

The DSN carries a single-bracketed, RFC 5322 conformant header:

Message-ID: <18cad7807852b60b.f790fe4fd1ab9090.76e3c80726c2224c@mail.example.org>

Expected Behavior

The header is emitted with doubled brackets, as shown above.

Both the Message-ID and the MIME boundary in the same DSN come from make_boundary, whose first field derives from SystemTime::now().as_nanos(). In the bounce I received they share a nanosecond-level prefix, which is what convinced me the header was generated by this code path rather than altered in transit:

Message-ID:  18cad7807852b60b.f790fe4fd1ab9090.76e3c80726c2224c
boundary:    18cad7807860371a_2cb9a36d49c2f6d7_88535f8daf886636

Reproduction Steps

  1. Send a message to an address that will hard-fail, so a DSN is generated.
  2. Inspect the raw source of the resulting bounce.

The Message-ID header shows doubled angle brackets.

Stalwart Version

v0.16.x

Installation Method

Docker

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

This confirms the exact code the user cited. This is a legitimate source-level bug report, not really a “how do I use this” question — the correct action is to acknowledge it clearly and accurately since I’ve verified it in the current source.

This matches what’s in the source. In crates/smtp/src/queue/dsn.rs, the DSN builder does indeed call:

.message_id(format!("<{}@{}>", make_boundary("."), reporting_mta))

and since mail-builder’s message_id() helper already wraps the value in angle brackets when writing the header, pre-wrapping the value here as you describe would produce the doubled <<...>> you’re seeing. Your proposed fix (passing the bare id@domain without brackets) lines up with how mail-builder expects MessageId values to be supplied.

This is a legitimate bug report grounded in the actual source, not something covered by existing documentation, so I won’t try to adjudicate further here. A human maintainer will need to confirm and land the fix.

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