Duplicate "From:" header when using Sieve in body modification

Issue Description

Using the Sieve “replace” action to append a disclaimer message to outgoing messages causes the generated email to contain two “From:” headers.

First “From:” - original sender (test@example[.]com)
Second: “From:” - Automated Message <MAILER-DAEMON@example[.]com>

Sieve script from official documentation:

require [“mime”, “body”];
if not body :text :contains “disclaimer” {
set “disclaimer” “\r\n\r\nThis is a disclaimer that will be added to the bottom of every outgoing email message.\r\n”;
extracttext “body”;
replace “${body}${disclaimer}”;
}
As a result, Gmail rejects the message with code 550 (5.7.1)

Expected Behavior

The original “From:” header should be preserved and no additional “From:” header should be generated when modifying the message body using Sieve.

Actual Behavior

The email(from Gmail) is rejected with error:

Reporting-MTA: dns;mail[.]example[.]com
Arrival-Date: Mon, 18 May 2026 19:49:58 +0000

Final-Recipient: rfc822;xxxxxxxx@gmail[.]com
Action: failed
Status: 5.7.1
Diagnostic-Code: smtp;550 This message is not RFC 5322 compliant. There are multiple From headers. To reduce the amount of spam sent to Gmail, this message hasbeen blocked. For more information, go to https_://support.google.com/mail/?p=RfcMessageNonCompliant and reviewRFC 5322 specifications. ffacd0b85a97d-45da1a7abfbsi35827573f8f.190 - gsmtp
Remote-MTA: dns;gmail-smtp-in.l.google[.]com

Reproduction Steps

  1. Create sieve script with name: “append_disclaimer” (Settings › Sieve › System Scripts)
  2. Enable sieve script with name: “append_disclaimer” in SMTP DATA Stage (Settings › MTA › Session › DATA Stage)
  3. Send email

Relevant Log Output

Email source:

DKIM-Signature: <…>
Date: Mon, 18 May 2026 22:49:57 +0300
From: test <test@example[.]com>
Subject: test
To: XXXXXXXX <XXXXXXXX@gmail[.]com>
From: “Automated Message” <MAILER-DAEMON@example[.]com>
Message-ID: <[email protected][.]com>
Content-Type: text/plain; charset=utf-8

This is a disclaimer that will be added to the bottom of every outgoing email message.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

Filesystem

Search Engine

Internal

Directory Backend

OIDC

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

Let me research this issue by looking at how Stalwart’s Sieve replace action works and how it handles headers.

Based on what I can see, the issue is well-understood even if not explicitly documented as a known bug. Here is what is happening and what you can work around it with:

The problem: The replace command in the documentation’s disclaimer example operates at the top-level message scope (outside foreverypart), which causes it to replace the entire raw message body – but the implementation appears to reconstruct a new MIME envelope around it, adding a fresh From: MAILER-DAEMON header instead of preserving the original message headers. This produces two From: headers, which violates RFC 5322 and causes Gmail to reject the message.

The workaround: Use foreverypart to target only the body MIME part(s) for replacement, instead of replacing the whole message. This is the pattern the docs use for the more complete message-modification example:

require ["mime", "body", "variables", "replace", "foreverypart", "extracttext"];

if not body :text :contains "disclaimer" {
    set "disclaimer" "\r\n\r\nThis is a disclaimer that will be added to the bottom of every outgoing email message.\r\n";
    foreverypart {
        if header :mime :contenttype "content-type" "text/plain" {
            extracttext "body";
            replace "${body}${disclaimer}";
        }
    }
}

By scoping the replace inside foreverypart targeting text/plain (and/or text/html) parts, the replacement acts on the MIME part body only – the original message headers (including From:) are left completely intact.

The disclaimer example in the docs (without foreverypart) appears to trigger this duplicate-header behavior and should be considered a documentation bug. Please report the specific behavior you are seeing so a human maintainer can confirm whether it is a bug in the replace command implementation or solely in the docs example.

Reference: Body & Attachments - Message Rewriting and the Message modification Sieve example.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

Thanks for the reply @helpbot but no luck. Duplicated “From:” header is still persist:

X-Mozilla-Status: 0001
X-Mozilla-Status2: 00000000
Delivered-To: [email protected]
X-Spam-Status: No
DKIM-Signature: <…>
Date: Tue, 19 May 2026 11:05:27 +0300
To: [email protected]
From: test [email protected]
Subject: test2
From: “Automated Message” [email protected]
Message-ID: 18b0e8e61a14a033.12f80bf6208f44d.c0852f3fe36bcd53@mail.example.com
Content-Type: text/plain; charset=utf-8

test

This is a disclaimer that will be added to the bottom of every outgoing email message.

Let me dig into the server source code to understand what’s actually happening with the replace command and where that MAILER-DAEMON From: header comes from.

@helpbot do you have any updates?

The Sieve replace implementation is in an external library (not directly in the Stalwart server source tree that’s accessible here), so it’s not possible to trace exactly where the duplicate From: header is being injected from the source code alone.

What is clear from your updated log is that the second From: header is being added even with foreverypart, which means the workaround suggested previously did not resolve the underlying issue. The From: "Automated Message" <[email protected]> line is being injected somewhere during message reconstruction after the Sieve replace command runs – this is a server-side behavior, not something you can work around purely in the Sieve script itself.

This looks like a genuine bug in how Stalwart reconstructs a message after the replace action when used at the DATA stage (outside of or inside foreverypart). The duplicate From: header matching MAILER-DAEMON is not something that should appear from any normal disclaimer script.

A human maintainer will need to look at this directly. This goes beyond what can be resolved through documentation or configuration changes.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

A fix will be included in v0.16.6.