Users can forge From: address in JMAP Email/set without sender verification

Issue Description

When using Bulwark to compose and send an email, there is a UI button that allows the user to override the sender address. Despite the MTA being configured with mustMatchSender = true, the server accepts forged From: headers in the JMAP Email/set request and delivers the message without validation.

Expected Behavior

When mustMatchSender = true, the server must reject any Email/set/EmailSubmission/set request where:

The from.email in the envelope does not match the authenticated user's identity or verified sender addresses
This check should occur before message ingestion, regardless of JMAP layer

Actual Behavior

Bulwark (connected from OIDC) allows UI override → client sends from address [email protected]
JMAP handler accepts the Email/set request and passes it to EmailSubmission/set
SMTP MAIL FROM uses the authenticated user’s address (e.g., [email protected])
Message is queued and delivered, but From: header is forged
mustMatchSender = true has no effect — the MTA stage checks the envelope sender only, not the From: header in the JMAP message content

Reproduction Steps

 Log in via Bulwark as authenticated user (account ID X, email [email protected])
 Compose a new email
 Click the "override sender" button in the UI and set From: to an unverified address (e.g., [email protected])
 Send the message
 Message is accepted and delivered — no rejection despite mustMatchSender = true

Relevant Log Output

2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, id = “Http with id singleton”, key = “allowedEndpoints”, result = “Integer(200)”
2026-07-01T07:32:53Z TRACE HTTP request body (http.request-body) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, details = [[“host”, “stalwart.localdomain”], [“user-agent”, “Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0”], [“content-length”, “928”], [“accept”, “/”], [“accept-encoding”, “gzip, deflate, br, zstd”], [“accept-language”, “en-US,en;q=0.9”], [“authorization”, “Bearer mybearer”], [“content-type”, “application/json”], [“origin”, “http”], [“priority”, “u=4”], [“referer”, “https”], [“sec-fetch-dest”, “empty”], [“sec-fetch-mode”, “cors”], [“sec-fetch-site”, “cross-site”], [“te”, “trailers”], [“via”, “2.0 Caddy”], [“x-forwarded-for”, “10.10.10.10”], [“x-forwarded-host”, “stalwart.localdomain”], [“x-forwarded-proto”, “https”]], contents = “{“using”:[“urn:ietf:params:jmap:core”,“urn:ietf:params:jmap:mail”,“urn:ietf:params:jmap:submission”],“methodCalls”:[[“Email/set”,{“accountId”:“f”,“destroy”:[“vyaaaafo”]},“0”],[“Email/set”,{“accountId”:“f”,“create”:{“send-1782891173173”:{“from”:[{“name”:“donotexist”,“email”:“[email protected]”}],“to”:[{“name”:“Louis B”,“email”:“[email protected]”}],“subject”:“wdcs”,“keywords”:{”$seen":true,“$draft”:true},“mailboxIds”:{“d”:true},“bodyValues”:{“text”:{“value”:“efefef”},“html”:{“value”:“

efefef

”}},“textBody”:[{“partId”:“text”,“type”:“text/plain”}],“htmlBody”:[{“partId”:“html”,“type”:“text/html”}]}}},“1”],[“EmailSubmission/set”,{“accountId”:“f”,“create”:{“1”:{“emailId”:“#send-1782891173173”,“identityId”:“c”,“envelope”:{“mailFrom”:{“email”:“[email protected]”},“rcptTo”:[{“email”:“[email protected]”}]}}},“onSuccessUpdateEmail”:{“#1”:{“mailboxIds/d”:null,“mailboxIds/e”:true,“keywords/$draft”:null}}},“2”]]}“, size = 928
2026-07-01T07:32:53Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-07-01T07:32:53Z DEBUG JMAP method call (jmap.method-call) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, id = “Email/set”, accountId = 5, elapsed = 1ms
2026-07-01T07:32:53Z INFO Message appended via JMAP (message-ingest.jmap-append) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, accountId = 5, documentId = 175, mailboxId = [3], blobId = “1ce39a3d7ef32229964a5471487c4e90f5b07a277137d6d9792c01e7539c6fc4”, changeId = 395, messageId = “18be1a136c6db6ae.f4934807f0a8449e.870229071add5395@12b8726fd2c2”, size = 686, elapsed = 1ms
2026-07-01T07:32:53Z DEBUG JMAP method call (jmap.method-call) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, id = “Email/set”, accountId = 5, elapsed = 2ms
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageMail with id singleton”, key = “isSenderAllowed”, result = “Integer(1)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageMail with id singleton”, key = “script”, result = “Integer(0)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageMail with id singleton”, key = “script”, result = “”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageMail with id singleton”, key = “rewrite”, result = “Integer(0)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageMail with id singleton”, key = “rewrite”, result = “”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageAuth with id singleton”, key = “mustMatchSender”, result = “Integer(1)”
2026-07-01T07:32:53Z INFO SMTP MAIL FROM command (smtp.mail-from) from = “[email protected]”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageRcpt with id singleton”, key = “maxFailures”, result = “Integer(5)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageRcpt with id singleton”, key = “waitOnFail”, result = “Integer(5000)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageRcpt with id singleton”, key = “maxRecipients”, result = “Integer(100)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaExtensions with id singleton”, key = “dsn”, result = “Integer(1)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageData with id singleton”, key = “maxMessageSize”, result = “Integer(104857600)”
2026-07-01T07:32:53Z TRACE Raw SMTP output sent (smtp.raw-output) size = 14, contents = “250 2.1.0 OK\r\n”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageRcpt with id singleton”, key = “script”, result = “Integer(0)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageRcpt with id singleton”, key = “script”, result = “”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageRcpt with id singleton”, key = “rewrite”, result = “Integer(0)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageRcpt with id singleton”, key = “rewrite”, result = “”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = “mail.localdomain”, collection = “domainName”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = “myuser”, domain = 1, collection = “email”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 5, collection = “account”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaInboundThrottle with id ivqqsqpoabqb”, key = “match”, result = “Integer(1)”
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 2
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 2
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 2
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 1
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 1
2026-07-01T07:32:53Z INFO SMTP RCPT TO command (smtp.rcpt-to) to = “[email protected]”
2026-07-01T07:32:53Z TRACE Raw SMTP output sent (smtp.raw-output) size = 14, contents = “250 2.1.5 OK\r\n”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageData with id singleton”, key = “maxReceivedHeaders”, result = “Integer(50)”
2026-07-01T07:32:53Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-07-01T07:32:53Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 1
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “SenderAuth with id singleton”, key = “dkimVerify”, result = “Constant(Relaxed)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “SenderAuth with id singleton”, key = “dmarcVerify”, result = “Constant(Disable)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “DkimReportSettings with id singleton”, key = “sendFrequency”, result = “Array([Integer(1), Integer(86400000)])”
2026-07-01T07:32:53Z INFO DKIM verification failed (smtp.dkim-fail) strict = false, result = , elapsed = 0ms
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “SenderAuth with id singleton”, key = “arcVerify”, result = “Constant(Disable)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageData with id singleton”, key = “addReceivedHeader”, result = “Integer(0)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageData with id singleton”, key = “addAuthResultsHeader”, result = “Integer(0)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageData with id singleton”, key = “enableSpamFilter”, result = “Integer(0)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageData with id singleton”, key = “script”, result = “Integer(0)”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageData with id singleton”, key = “script”, result = “”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = “mail.localdomain”, collection = “domainName”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaOutboundStrategy with id singleton”, key = “schedule”, result = “String(Borrowed(“local”))”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “MtaStageData with id singleton”, key = “addReturnPathHeader”, result = “Integer(0)”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = “mail.localdomain”, collection = “domainName”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) id = “SenderAuth with id singleton”, key = “dkimSignDomain”, result = “String(Borrowed(“mail.localdomain”))”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = “mail.localdomain”, collection = “domainName”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “dkimSigners”
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 1ms, total = 1
2026-07-01T07:32:53Z TRACE Blob write operation (store.blob-write) key = base64:ZLwG+hlG+fewfwefwfwef=, elapsed = 0ms, size = 1082
2026-07-01T07:32:53Z INFO Queued message submission for delivery (queue.authenticated-message-queued) queueId = 315854523132316674, from = “[email protected]”, to = [“[email protected]”], size = 1545, nextRetry = 2026-07-01T07:32:53Z, nextDsn = 2026-07-02T07:32:53Z, expires = 2026-07-04T07:32:53Z
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 5
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 3
2026-07-01T07:32:53Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 6
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 2
2026-07-01T07:32:53Z DEBUG JMAP method call (jmap.method-call) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, id = “EmailSubmission/set”, accountId = 5, elapsed = 6ms
2026-07-01T07:32:53Z INFO Delivery attempt started (delivery.attempt-start) queueId = 315854523132316674, queueName = “local”, from = “[email protected]”, to = [“[email protected]”], size = 1545, total = 1
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = “mail.localdomain”, collection = “domainName”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) queueId = 315854523132316674, queueName = “local”, from = “[email protected]”, to = [“[email protected]”], size = 1545, total = 1, id = “MtaOutboundStrategy with id singleton”, key = “route”, result = “String(Borrowed(“local”))”
2026-07-01T07:32:53Z INFO New delivery attempt for domain (delivery.domain-delivery-start) queueId = 315854523132316674, queueName = “local”, from = “[email protected]”, to = [“[email protected]”], size = 1545, total = 1, domain = “mail.localdomain”
2026-07-01T07:32:53Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-07-01T07:32:53Z DEBUG Cache update (store.cache-update) accountId = 5, collection = “email”, changeId = 395, details = [1, 0], total = [107, 5], elapsed = 0ms
2026-07-01T07:32:53Z TRACE Blob read operation (store.blob-read) key = base64:ZLwG+hlG+fewfwefwfwef=, elapsed = 0ms, size = 1082
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = “mail.localdomain”, collection = “domainName”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = “myuser”, domain = 1, collection = “email”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 5, collection = “accessToken”
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 4
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 5, collection = “account”
2026-07-01T07:32:53Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 7
2026-07-01T07:32:53Z DEBUG JMAP method call (jmap.method-call) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, id = “Email/set”, accountId = 5, elapsed = 0ms
2026-07-01T07:32:53Z TRACE HTTP response body (http.response-body) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, contents = “{“methodResponses”:[[“Email/set”,{“accountId”:“f”,“oldState”:“srebq”,“newState”:“sribq”,“destroyed”:[“vyaaaafo”]},“0”],[“Email/set”,{“accountId”:“f”,“oldState”:“sribq”,“newState”:“srmbq”,“created”:{“send-1782891173173”:{“id”:“v2aaaafp”,“threadId”:“fp”,“blobId”:“caoohgr0p9zsekmwjjkhcsd2j7iplmd7e0ytpvwzpewadz7ttrx2ibiav2aq”,“size”:686}}},“1”],[“EmailSubmission/set”,{“accountId”:“f”,“newState”:“srqbq”,“created”:{“1”:{“id”:“bh”,“sendAt”:“2026-07-01T07:32:53Z”,“undoStatus”:“final”}}},“2”],[“Email/set”,{“accountId”:“f”,“oldState”:“srmbq”,“newState”:“srubq”,“updated”:{“v2aaaafp”:null}},“2”]],“sessionState”:“731974b5”}”, code = 200, size = 618
2026-07-01T07:32:53Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-07-01T07:32:53Z DEBUG HTTP request URL (http.request-url) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, remoteIp = 10.10.10.10, url = “/jmap/”
2026-07-01T07:32:53Z TRACE Expression evaluation result (eval.result) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, id = “Http with id singleton”, key = “allowedEndpoints”, result = “Integer(200)”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 5, collection = “accessToken”
2026-07-01T07:32:53Z DEBUG Cache update (store.cache-update) accountId = 5, collection = “email”, changeId = 397, details = [1, 0], total = [107, 5], elapsed = 0ms
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 4, collection = “account”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “domainId”
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 2
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 6
2026-07-01T07:32:53Z TRACE HTTP request body (http.request-body) listenerId = “http”, localPort = 8080, remoteIp = 10.10.10.10, remotePort = 49806, details = [[“host”, “stalwart.localdomain”], [“user-agent”, “Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0”], [“content-length”, “481”], [“accept”, “/”], [“accept-encoding”, “gzip, deflate, br, zstd”], [“accept-language”, “en-US,en;q=0.9”], [“authorization”, “Bearer mybearer”], [“content-type”, “application/json”], [“origin”, “https”], [“priority”, “u=4”], [“referer”, “https”], [“sec-fetch-dest”, “empty”], [“sec-fetch-mode”, “cors”], [“sec-fetch-site”, “cross-site”], [“te”, “trailers”], [“via”, “2.0 Caddy”], [“x-forwarded-for”, “10.10.10.10”], [“x-forwarded-host”, “stalwart.localdomain”], [“x-forwarded-proto”, “https”]], contents = “{“using”:[“urn:ietf:params:jmap:core”,“urn:ietf:params:jmap:mail”],“methodCalls”:[[“Email/query”,{“accountId”:“f”,“filter”:{“inMailbox”:“a”},“sort”:[{“property”:“receivedAt”,“isAscending”:false}],“limit”:50,“position”:0,“calculateTotal”:true},“0”],[“Email/get”,{“accountId”:“f”,”#ids”:{“resultOf”:“0”,“name”:“Email/query”,“path”:“/ids”},“properties”:[“id”,“threadId”,“mailboxIds”,“keywords”,“size”,“receivedAt”,“from”,“to”,“cc”,“subject”,“preview”,“hasAttachment”,“blobId”]},“1”]]}", size = 481
2026-07-01T07:32:53Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 12
2026-07-01T07:32:53Z INFO Message ingested (message-ingest.ham) queueId = 315854523132316674, queueName = “local”, from = “[email protected]”, to = [“[email protected]”], size = 1545, total = 1, accountId = 5, documentId = 176, mailboxId = [0], blobId = “64bc06fa1946faf159ba4845e50569bf49b7b2491ac714b19670c49fb7e939bb”, changeId = 398, messageId = “18be1a136c6db6ae.f4934807f0a8449e.870229071add5395@12b8726fd2c2”, size = 1545, elapsed = 1ms
2026-07-01T07:32:53Z INFO DSN success notification (delivery.dsn-success) queueId = 315854523132316674, queueName = “local”, from = “[email protected]”, to = [“[email protected]”], size = 1545, total = 1, to = “[email protected]”, hostname = “localhost”, code = 250, details = “OK”
2026-07-01T07:32:53Z INFO Delivery completed (delivery.completed) queueId = 315854523132316674, queueName = “local”, from = “[email protected]”, to = [“[email protected]”], size = 1545, total = 1, elapsed = 0ms

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

OIDC

Additional Context

Environment

Stalwart version: docker compose v0.16.11
Deployment type: Self-hosted, behind reverse proxy, TLS enabled
Auth method: JWT (OAuth2-style)
Email client: Bulwark (web UI) with JMAP
MTA config: mustMatchSender = true (enabled in MtaStageAuth)
Defaults spam rules activated

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 how Stalwart handles the mustMatchSender setting and JMAP EmailSubmission/set validation.

This is a well-analyzed and accurate bug report. Here’s what the source and documentation confirm about the current behavior:

mustMatchSender only checks the SMTP envelope MAIL FROM, not the From: message header. This is a by-design (or at least an architectural) limitation: the MtaStageAuth check runs at the SMTP MAIL FROM stage and only has access to MtaMailFromVariable variables (sender, sender_domain, authenticated_as), which refer to the SMTP envelope sender, not the RFC 5322 From: header inside the message body.

The logs in the report confirm exactly this: mustMatchSender evaluates to 1 (true) and the SMTP MAIL FROM is [email protected] (which does match the authenticated user), so the check passes. The From: header inside the email containing [email protected] is never compared against the authenticated identity at the SMTP or JMAP layer.

The gap is specifically:

  • JMAP Email/set accepts the email object with any from value without validating it against the user’s identity.
  • JMAP EmailSubmission/set uses the identityId and envelope.mailFrom for the SMTP envelope, which is correct, but never cross-checks the From: header in the stored email against the identity.
  • mustMatchSender then sees a legitimate envelope sender and passes.

As a workaround, you could write a Sieve script on the MtaStageData hook that compares the From: header against authenticated_as and rejects mismatches, since SpamEmailVariable (available during data stage) does expose from and authenticated_as. However, this is not a built-in feature and requires custom scripting.

This appears to be a genuine missing enforcement point in the JMAP submission pipeline that would need to be addressed in the server code itself. A human staff member will need to confirm whether this is a known gap being tracked or a new finding.

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

The mustMatchSender setting validates the envelope from, not the From header in the email. This is the same check the MTA does.

Hello,

Thank you for your time.

Even if mustMatchSender behaves like the MTA’s envelope-MAIL FROM check, it still leaves a major security gap: attackers can send emails with a forged envelope MAIL FROM.

Is there a workaround or a solution to prevent cases where the authenticated user and the envelope sender are different?

The AI mentioned a Sieve script. Is this really possible? If so, because I’m not familiar with this kind of script, can you guide me on how to do it?

What you’re describing is what DMARC prevents. Even if they change the headers, DMARC won’t be aligned and you can set your policy to reject those messages.