Outgoing messages with an empty body lose the header/body separator (CRLF CRLF)

Issue Description

When relaying a message with an empty body, Stalwart serializes it without the terminating blank line after the header block. The receiving MTA can’t separate headers from body, treats everything as the body, and rejects the message — in our case Scaleway TEM replies:

501 5.6.0 "from": missing mandatory header

The From: header is of course present. Messages with a body are fine.

RFC 5322 §2.1 requires the header block to be terminated by an empty line, even when the body is empty.

Captured the raw BDAT payload Stalwart sends, using a local SMTP sink as a relay target. Same client (Apple Mail), same headers, only the body differs:

With body — correct:

...X-Mailer: Apple Mail (2.3864.600.51.1.1)\r\n\r\nI have a body.
hex tail: 29 0d 0a 0d 0a 49 20 68 61 76 65 ...
          )  \r \n \r \n  I     h  a  v  e

Empty body — broken:

...X-Mailer: Apple Mail (2.3864.600.51.1.1)\r\n
hex tail: 29 0d 0a          <-- message ends here
          )  \r \n

The final \r\n\r\n is missing. The client submitted the message correctly (with the blank line); it is lost when Stalwart re-serializes the message to add Received: and the DKIM signatures.

Expected Behavior

The email should be delivered

Actual Behavior

The email is rejected with 501 5.6.0 "from": missing mandatory header

Reproduction Steps

  1. Configure any relay/smarthost route.
  2. Send a message with a subject but no body text (Apple Mail does this if you leave the text field empty).
  3. Capture the outgoing stream (e.g. point a relay route at a plaintext SMTP sink) and inspect the bytes.

Expected: message ends with \r\n\r\n. Actual: ends with \r\n.

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 is a bug report about message re-serialization, not a documentation question, so I can’t confirm or resolve it from the docs alone.

This isn’t covered in the current docs. A community member or staff member will 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.13 which will be released this week.