The spam filter ran without errors, and phishing still reached the inboxes

Services up, configuration valid, no errors in the system status. Yet phishing landed in the inboxes while partners' newsletters went to spam. Our engineers measured which detection mechanisms actually fired, repaired the causes, and three months later found and fixed a regression that only showed up hour by hour.

  • An example phishing message: from −2.7 to 9.1 points
  • The Bayes filter scores 100 % of messages
  • Database rebuilt in the background, no interruption
A score scale from −5 to 10 with a dashed threshold: the envelope of a phishing message moves in an orange arc from below the threshold to far above it

A German-speaking company with about a dozen domains and several dozen mailboxes, around 600 external messages a day

Challenge

Users reported two problems at once: spam and phishing got past the filter, and part of the legitimate mail from partners was marked as spam. Earlier attempts had added custom rules and raised scores, which made both problems worse.

The environment: Debian 12, Postfix with postscreen, Dovecot with LMTP and Sieve, SpamAssassin 4.0 and amavis with ClamAV.

Solution

Data instead of intuition. The header the filter adds to every message lists the rules that fired. Collected over real mail and several weeks of logs, it shows which detection mechanisms work and which stay silent. No setting was changed without evidence in that data.

The first repair removed the root causes: DNS blacklists that never answered, a poisoned and almost empty Bayes database, auto-learning that taught the filter phishing was fine, whitelists for whole freemail domains and exaggerated custom rules. When spam returned after three months, an hourly analysis of the logs found forgotten cron jobs and a shrunken database, and the database was rebuilt in the background without interrupting the mail.

Results

Spam before the repair
Rated as clean, a median of 1.1 points against a threshold of 3.1
Scored by the Bayes filter
100 % of messages after the repair, verified hour by hour
Rebuilt Bayes database
5.3 million tokens, from 47,828 spam and 110,302 legitimate messages
Example phishing message
From −2.7 points in the inbox to 9.1 points in the spam folder
False alarms on partner newsletters
None after the repair
Nightly training
About 2 minutes instead of several hours

How we worked

  1. Measure first

    The filter headers of real mail, then several weeks of logs: which rules fire, which stay silent.

  2. Root causes, not symptoms

    An own recursive resolver for the DNS blacklists, a rebuilt Bayes database, safe learning, whitelists only with a valid DKIM or SPF, calibrated default scores.

  3. Verify on real mail

    Partner newsletters that had been flagged as spam dropped to about −3 points, and no legitimate message crossed the threshold.

  4. Three months later: the regression

    The configuration was unchanged and the resolver worked. Only hourly statistics showed the filter running without Bayes for whole nights.

  5. Repair in steps

    Two forgotten cron jobs removed and permissions repaired; auto-learning off and database limits set; a full rebuild from about 158,000 messages in a separate database, about five hours in the background, then an atomic swap.

  6. New nightly training

    It runs as the service user, only on the last three days, in about two minutes instead of hours.

Challenges we overcame

The DNS blacklists ran, they just never said anything

The server used a public resolver. Operators of lists such as Spamhaus block queries from public resolvers because of their volume. The filter does not report an error in that case: it adds a rule ending in BLOCKED with zero points and carries on. The strongest reputation signals were dead, and the system looked healthy. SpamAssassin 4.0 also no longer ships the Spamhaus ZEN rules by default; they were added so that only real listing codes score, never error or rate-limit codes.

The filter had learned that phishing is fine

Modern spam is often perfectly authenticated: valid DKIM, SPF and DMARC, sent from large mail platforms. It collects negative points for that, falls below the auto-learning threshold as legitimate mail, and the Bayes filter learns that such content is fine. Before the first repair the database held 174 spam messages against 139,600 supposedly legitimate ones. Auto-learning is now off for good, and only the Sent folders serve as the pattern for legitimate mail. The inbox does not qualify, because it contains exactly the spam that got through.

A default limit silently discarded almost all of the training

The default size limit of the Bayes database is 150,000 tokens; above it the filter removes the oldest entries, and the sync after a large training run is enough to trigger that. A database built from about 147,000 messages had 134,000 tokens right after training; the same amount of mail, with automatic expiry off, gives 5.3 million. More than 95 percent of the first training had been thrown away before the filter could use it.

A forgotten cron job switched the filter off every hour

Two old entries in root's crontab trained the filter from the Spam folders every hour. Because the database path was set globally, they wrote into the same database the service uses, and files created by root became unreadable for the service user. Daily statistics looked fine. Only the breakdown by hour showed nights in which the filter ran without Bayes for seven hours, and one day on which that affected half of all messages.

A false alarm that could have cost a rebuild of the whole setup

At one point about 95 percent of the messages in the mailboxes had no filter header at all, which looked like mass bypassing. The Postfix logs explained it: these were the internal system notifications, about 12,000 a day, which take a separate trusted path. All external mail, about 600 messages a day, went through the filter. A real but smaller gap surfaced at the same time: the client program passes messages larger than 500 KB without checking, and about half of the external messages without a header were above that limit, including spam with a 1.9 MB attachment.

The service configuration overrode the global one

The global configuration named amavis with ClamAV as the content filter. The services that accept mail from the internet had their own parameter pointing at SpamAssassin, which takes precedence, so incoming mail never passed amavis while the documentation assumed it did. That matters for the antivirus assumptions as much as for spam.

The mail server asks the DNS blacklist on two even lanes: through the public resolver, drawn as a cloud, the query ends before the list at a cross; through the own resolver, an orange box, it arrives

Before and after

Indicator Before After
DNS blacklists Blocked for 100 percent of messages Answer correctly; no block in four weeks of logs
Scoring by Bayes No message scored; later gaps of several hours 100 percent of messages, verified hour by hour
Bayes database 174 spam and 139,600 legitimate messages, poisoned 47,828 spam and 110,302 legitimate messages, 5.3 million tokens
False alarms on newsletters Partner newsletters marked as spam None after the repair
Example phishing message −2.7 points, inbox 9.1 points, spam folder
Nightly training Many hours, blocked the database About 2 minutes, no blocking

What we take from it

  • No errors is not effectiveness. Measure which rules actually fire, not only whether the service runs.
  • One database, one owner. Every process that trains the filter runs as the service user.
  • Legitimate mail only from a trustworthy source: the Sent folders, never the inbox.
  • Monitor by the hour. Daily statistics hide gaps of several hours.
  • Review old cron jobs at every change of the filter architecture.

A cheap and precise measure of misses needs no survey: the filter tags the subject of every message it recognises, so a message in the Spam folder without that tag was moved there by a user. On top of the repair we recommended closing a few remaining points: a 3 MB limit for the client program, daily rule updates, three narrow rules for the most frequent phishing patterns, and training in real time when a user moves a message into or out of the Spam folder.

24 hourly bars from 0 to 24; an orange frame marks seven night hours without a bar, in which the filter ran without Bayes

Related situations

  • Invoices, quotes or one-time passwords land in spam or come back? Since November 2025 Gmail has enforced its sender rules more strictly, including rejections (As of 2026-10). We check SPF, DKIM, DMARC and reputation and give you a written action list.

  • Your system runs, but nobody knows it in detail. We record application, database, server and email, assess the risks, recommend the next step in writing and, if you wish, take over operations.

More projects

  • A phone with a QR code, a file split into chunks, three parallel streams to a server with a disk and a gallery grid

    A photo upload app for wedding guests

    Wedding guests scan a QR code, open one secret address and upload photos and videos straight from the phone: no app, no account, no login. Files of up to 5 GB land on the client's own disk, and the couple watches the gallery fill up during the party. One evening, no second chance, and phones from six years back.

  • Four envelopes pass the gates SPF, DKIM and DMARC, each ticked in orange, on the way into the inbox

    Email deliverability for four company domains

    A mail server for four company domains signed nothing, published no DMARC policy and was rejected by Microsoft. Our engineers audited it, piloted the change on one domain and rolled it out in batches. The result: all three checks pass at Gmail, the Microsoft block was lifted, and the users noticed nothing.

In 20 minutes we work out which first step fits your system

You tell us the goal and the current state, and we say openly whether and how we can help. Reply within one working day, no obligation.

Phone (German, Polish, English) +48 501 250 220
Email Email

Reachable: Monday to Friday, 9:00 to 17:00 Central European Time; support and emergencies around the clock (24/7)