Stalwart allows any mail to be the sender

Issue Description

Stalwart lets anything to be typed as the sender e-mail. Like for example when I am composing an e-mail, overriding the sender identity and typing in something like [email protected], Stalwart will accept this and send the mail without a problem. But of course the receiver receives it right into the spam/junk directory because of obvious reasons.

Expected Behavior

Stalwart should block users from using unauthorized addresses.

Actual Behavior

In the mean time, when a user is a member of a mailing list, and the user tries to use that mailing list address as their sender address (by overriding the sender address, just as I described in the above example), the receiver servers return:

host ‘mx.yandex.net’ rejected command ‘DATA’ with code 550 (5.7.1) 'Email rejected per DMARC policy for example.com

OR:

host ‘gmail-smtp-in.l.google.com’ rejected command ‘BDAT 1288 LAST’ with code 550 (5.7.26) 'Unauthenticated email from example.com is not accepted due to domain’s DMARC policy. Please contact the administrator of example.com domain if this was a legitimate mail.

BUT, when the very same user receives a message that is sent to the mailing list, then the user can reply to that message and for example Bulwark automatically overrides the user identity to use the mailing list address as the sender; and it is then sent.

The receiver this time receives the message legitimately and doesn’t complain about dmarc policy; although both situations have overridden the sender address with the mailing list address. When I check the original message, I can see the user identity in the headers (that’s why they don’t complain about the dmarc policy this time because the user is present and they are authorized now); but the mail clients like Google and Yandex does not indicate that the message is actually sent by the user on behalf of the mailing list; rather they show the mailing list as the sender.

I think this should be addressed so that the receiving party would know who from the mailing list actually replied. Otherwise they don’t know who replied their message.

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

PostgreSQL

Blob Storage

PostgreSQL

Search Engine

PostgreSQL

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

By default Stalwart does not restrict what address an authenticated user puts in MAIL FROM, it only checks whether the sender’s domain is in a spam-block list unless the connection is authenticated (isSenderAllowed defaults to {"else":"!is_empty(authenticated_as) || !key_exists('spam-block', sender_domain)"}). Any authenticated user is therefore allowed to use any envelope sender by default. This is intentional (to support aliases, mailing lists, etc.) but it means you need to add your own policy if you want to lock users to their own address(es).

You can enforce this with the isSenderAllowed expression on the MtaStageMail singleton (Settings › MTA › Session › MAIL FROM Stage in the WebUI). It has access to sender, sender_domain, and authenticated_as variables, so you can write a rule such as requiring sender == authenticated_as (or checking membership in the user’s configured email/alias list) before allowing the MAIL FROM to proceed, rejecting anything else. See:

Regarding the second part of your report, the behavior where replying to a mailing list message automatically substitutes the mailing list address as the sender (and how mail clients then display “sent by” the list without indicating the underlying member) isn’t something covered in the current documentation.

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

Stalwart restricts envelopes, not From addresses in the body. And, for the problem you describe there’s DMARC.

It’s probably I don’t get it well. Thank you for taking to reply, please bear with me when I say most of the things I said are not addressed.

1- When a user tries to override their from address as [email protected], address of a mail group they are member of, the receiving party rejects the mail per DMARC policy saying the user is not authenticated to send a mail from that address. (I guess this is because both envelope and “from” addresses are overridden in this case.) BUT;

1a- The receiving party has no problem receiving the mail when the sender replies to a message of this mailing list although the “from” address is still overridden as “[email protected]” (I guess here the envelope carries the user’s address but the “from” label carries the mail list address. But both situations are actually the same on the client side. So I don’t get how the client sets both the envelope and the from address on the first case but only the “from” address and not the envelope on the second case; without any apparent difference on the interface.)

1b- The receiving party also has no problem receiving the mail when the sender spoofs the “from” address to anything else with the domain extension. (Like [email protected]) And DMARC policy doesn’t apply. (Probably again because envelope is not overridden but from address is.)

So; how does this dmarc policy apply; considering it causes the first case to be rejected but not the second and third? And why would the mail server (Stalwart in this case) allow a user to override the envelope and the from address by default? (To be honest I thought “mustMatchSender” was used to block this, which is true by default.)

2- When a user receives a mail that is sent to a mailing list that the user is member of; they can then reply it. (I mean they can’t spoof to use the mailing list address when sending a new mail; but they can do this when they are responding to an existing mail.) But the receiving party doesn’t know who in this mailing list replied because from address is mailing list’s and from name is empty. This is not a convenient way for a mailing list, without knowing who is replying, is it?

Some clients sometimes show from address differently with a note saying “this person sent the mail on behalf of that person or this group”. Isn’t that facility a remedy to what I’m describing here or is it something different?

In the end, I don’t think a mail server should allow a user to spoof the envelope or “from” addresses by default and even not limiting the domain. Do you think it is?

The check you are thinking of is envelope-only. mustMatchSender runs at MAIL FROM and compares that address against what you authenticated as; it returns a 501 if they disagree and never looks at the From: header in the message body. Keeping those separate is deliberate, because forwarding and list resending legitimately need a From: that differs from the envelope.

The addresses that count as yours are: your own address, every enabled alias on your domains, and the addresses of any Group you belong to. A mailing list is not a group in that sense. A MailingList object holds bare addresses with no link back to an account, and it only rewrites the recipient side, so being on one never authorizes you to send as it. If the address you can send as belongs to a Group you are a member of, that explains it; if it is a MailingList, it should not be accepted as an envelope at all.

Now the part that ties your three cases together: DKIM signs with the envelope domain, not the header domain. So whatever ends up in MAIL FROM decides both the domain SPF is checked against and the d= on the signature, and the receiver’s DMARC check then asks whether either of those lines up with the From: header it sees. Your case 1b passes because the envelope is still your own address on the same domain as the spoofed From:, so everything aligns. So does the reply case. The one that fails has to be a case where the From: domain aligns with nothing that was authenticated, which usually means the list address is on a domain this server does not treat as local, so it goes out unsigned and SPF for that domain does not list your IP.

To say which of those it is for your setup I need headers: Return-Path, From, the DKIM-Signature d= and Authentication-Results from one rejected message and one accepted reply, plus whether that list is a MailingList or a Group and which domain it is on.

On knowing who replied: Stalwart does not add a Sender: header, rewrite From: for list posts, or seal with ARC. A client can set Sender: itself and most will show “on behalf of” when it is present, but nothing on the server side will do it for you. If you want the From: header policed as well as the envelope, that is a DATA-stage filter comparing the header against env.authenticated_as.

How can I know the rejected message’s headers, as the other party directly rejects and discards it. So I don’t know what headers it carries.

But I know the one gets accepted.

[email protected] sends a mail to [email protected] which has [email protected] as a member. (It is a mailing list, not a group.)

[email protected] responds to the message. Client already shows the “from” address is spoofed to the mailing list’s, when composing a reply.

Now [email protected] receives the response, showing it is sent from [email protected] without a name, and all SPF, DKIM and DMARC pass.

Here are the headers:

From: <[email protected]>
Return-Path: <[email protected]>
DKIM-Signature: v=1; a=rsa-sha256; s=v1-rsa-20260717; d=example.com; c=relaxed/relaxed; r=y;
Authentication-Results: mx.google.com; dkim=pass [email protected] header.s=v1-rsa-20260717 header.b=dAtitG0i; spf=pass (google.com: domain of [email protected] designates xxx.xxx.xxx.xxx as permitted sender) [email protected]; dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com
FpEqaEejKONLUmM4ThGXkazfsVzPPIJHS07bXuOfNdnEeuaVbPKuYmsIBef/6BczmP/i tH7A==; dara=google.com
ARC-Authentication-Results: i=1; mx.google.com; dkim=pass [email protected] header.s=v1-rsa-20260717 header.b=dAtitG0i; spf=pass (google.com: domain of [email protected] designates xxx.xxx.xxx.xxx as permitted sender) [email protected]; dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com

By the way I thought mail group cannot have a mail distribution to its users. Can it? I first created a mail group, but the members I added to that group did not receive any message I sent to that group address from outside (like gmail).

Then in 10 minutes I deleted the group and created a mailing list with the same name. Then it started distributing the mails to the members’ addresses. Oddly, I tried sending mails three times; but it distributed about 40 times. So I thought, the first time when it was a group, Gmail could not deliver the message because it was a group, but it didn’t mark it not-deliverable, rather it reserved it for a retry. Then when I created a mailing list with the same name, this retries delivered the message successfully. Though this doesn’t explain why so many times I received the message for 2 days in a row.

Shall I use groups for something like shared inbox? If yes, then why didn’t members receive the messages sent to group address, until I removed it and created a mailing list instead?

What I want is, have a team mail address, which will route the incoming messages to the inboxes of the team members and then any of them will be able to reply to it, under their name (or something like “on behalf of”..

I said the failing case was probably a list on a domain this server does not treat as local, but the headers you posted are all on one local domain and everything aligns, so that explanation does not hold. With the domains redacted I cannot tell the rejected case apart from the accepted one, and you do not need the receiving side for that anyway: our bounce carries the first 4 KB of the original message as an attachment, so Return-Path, From and the DKIM-Signature d= for the rejected message are already sitting in your mailbox.

On the group behaviour, you have the wrong object for what you are trying to do. A Group in Stalwart is a shared mailbox. Mail sent to it is accepted and delivered into the group’s own inbox, which members see as a shared folder over IMAP or JMAP; it never fans out to their personal mailboxes. Only a MailingList expands to its recipients, which is why converting it changed what you saw.

For a team address that several people can send from, though, the Group is the object you want and the MailingList is not. Group members already have the group address in their authorised sender list, so each of them can hold their own identity on it with their own display name. That is also the answer to knowing who replied: the address is shared, the display name on the identity is not, and recipients see the latter. The server still will not add a Sender: header for you.

The forty deliveries from three sends is a different problem. Loop detection is only a count of Received headers, default 50, so a short cycle amplifies a long way before anything stops it. What is in that list’s recipients, and is any entry the list’s own address, an alias of it, or another list?

Hello, I´m facing the exact same issue that in my setup the user can “fake” its identity by overriding the “FROM”-Header (e.g. via the “Override” button available in Bulwark webmail application).

Can anybody post a working solution about how to configure Stalwart to prevent this kind of address spoofing (the user should only be allowed to use its own mail address or any assigned alias address in “FROM” header!).

A From header policy takes a trusted Sieve script on the DATA stage. mustMatchSender covers MAIL FROM and stops there, and nothing else looks at the header. Store the script as a SieveSystemScript record and set MtaStageData.script to an expression returning its name; it defaults to no script at all, which is why nothing runs today.

Something like this, gated so it only applies to authenticated sessions:

require ["variables", "envelope", "reject"];

if allof(not string :is "${env.authenticated_as}" "",
         not address :all :comparator "i;ascii-casemap" :is "from" "${envelope.from}") {
    reject "550 5.7.1 The From header must match the address you authenticated as.";
}

Comparing against ${envelope.from} is deliberate: mustMatchSender has already tied the envelope to an address the session is allowed to use, including aliases, so this inherits that work. Do not use ${env.from}; our Sieve variables page lists it but it was never implemented, and a script testing it compares against an empty string and passes everything. I will get that page corrected.

This covers webmail and JMAP submissions too, because EmailSubmission/set replays the message through a local SMTP session that hits the same DATA stage. Be aware of what it costs you: legitimate send-on-behalf-of, forwarding and list resending all break under it, as does sending from alias B while the envelope is alias A.