How to I correctly configure the Stalwart spam filter, if it operates behind an outer incoming Postfix MX server with spamassasin?
I have already tried to tell Stalwart that there is an upstead server in the Stalwart settings, but apparently to no avail.
The spam filter is entirely random. My Spam folder is full of ham. I received over 20000 mails in over 3 months, so plenty of training data, but the Spam folder contains about 50% are ham, which is the definition of random.
To avoid more problems, I put the reject threshold at 100 (!) (instead of 7), and people still get “Non-delivery” bounces that blame their mail server’s reputation. It’s like the spam filter is randomly assigning spam scores.
I suspect that Stalwart’s spam filter primarily looks at the previous hop mail server and how much spam it got from that server. If it doesn’t know that the previous hop is my own server, then it will blame my own incoming server as the spam origin and wildly attribute all my mail as spam just for coming from that server.
In theory, Stalwart could learn that 100% of all its incoming mail is from that server, but I guess it’s not that smart. And I haven’t found the settings knob to tell it that this server is the actual MX.
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.
The fix is to configure Postfix to speak the Proxy Protocol to Stalwart (Postfix supports emitting it via upstream_proxy_protocol = haproxy on the smtp(d) service that relays to Stalwart), and add Postfix’s IP to Stalwart’s trusted proxy networks. Just telling Stalwart “there’s an upstream server” via a trusted-networks entry without the corresponding protocol on the wire won’t work, since Stalwart still needs an actual signal (a Proxy Protocol header) carrying the real client IP, not merely permission to trust the connecting address.
Without this, the spam filter (and SPF/DMARC) evaluate every message as if it came from Postfix’s own IP, which explains erratic scoring and false “sender reputation” bounces blamed on the wrong host: Stalwart is scoring Postfix’s reputation, not the real sender’s, and that score is essentially meaningless/random from the recipient’s point of view.
Steps:
On Postfix, enable Proxy Protocol on the SMTP client connecting to Stalwart (or, if going through an intermediate TCP proxy, configure that layer instead).
On Stalwart, set proxyTrustedNetworks on SystemSettings (Network › General in the WebUI) to include Postfix’s IP/CIDR, or set overrideProxyTrustedNetworks on the specific smtpNetworkListener.
Confirm with a test message that headers/logs show the real client IP, not Postfix’s, before trusting the spam score again. Consider reverting your scoreReject threshold back down to something sane (e.g. 7) once real IPs are flowing through, since 100 defeats the purpose of the filter entirely.
No, the fix should be entirely in Stalwart. Postfix is innocent here. Stalwart needs to learn what an MX is. And not assume that it’s the MX, “or else I will destroy your mail!”
Plus, the setting should be a single text field for the hostname, and should ideally already be part of the initial Stalwart setup dialog sequence.
This is a very very common case. Almost nobody will put their production mail server aside and start 100% Stalwart as their main mail server, directly on the internet, as only MX. There will always be at least a testing period where Stalwart gets a copy of the incoming mail. This setup should be obvious and simple, not difficult to find, and should not require upstream changes in the other mail servers.
The spam filter has no IP reputation scoring in it at all, so that is not what is happening. What breaks behind a relay is authentication: SPF is evaluated against the IP that connected, which is your Postfix, so it fails for almost every sender and the DMARC verdict follows it down.
The knobs are expressions on the SenderAuth object and they all have remote_ip in scope: set spfEhloVerify, spfFromVerify, dmarcVerify and reverseIpVerify to disable when remote_ip is your upstream MX. Leave dkimVerify alone, since DKIM signatures survive relaying intact and are the one authentication result still worth trusting in your setup. If you would rather skip filtering entirely for relayed mail, MtaStageData.enableSpamFilter takes the same variables.
What does the verbatim NDR text say? With your reject threshold at 100 the spam score path is unreachable, and DMARC only rejects when dmarcVerify is set to strict, which is not the default. Our own reject line reads “550 5.7.1 Message rejected due to excessive spam score.” If yours does not look like that, the bounces are coming from the Postfix in front.
Either way, three months of training behind the relay has taught the classifier a skewed picture of what authenticated mail looks like, so once the verify expressions are right the model will want resetting and retraining.