header_checks and Spamassassin headers in Postfix 2.6

images.duckduckgo.comI’ve had this mail server of mine for some time. I was an early adopter of Gmail back then, but as years went on and it became obvious that messages were data-mined by Gmail, I eventually started running my own Postfix server. Not just for me, but for family and eventually other people. Now, the thing is that some folks insist on having their emails forwarded to another service, like Gmail, Yahoo, etc. I can understand that. The problem is that if such mailbox receives a lot of spam messages, those messages get forwarded to Gmail and Yahoo as well, and as a result, my mail server can get bad reputation because of that – I can’t just explain to the other side that those spams are only forwarded.

I’m using Spamassassin to mark spam, but all it can do is to mark the messages for users’ MUAs, it can’t do anything else, like drop or reject unsolicited bulk email so that it doesn’t get forwarded. I’ve used Amavis somewhere in the past and it could have solved the problĂ©m here, too, but here it felt as too large a gun for the task. All I wanted was to prevent the most obvious spam with high score points from being forwarded. So I created a file with a regular expression to catch all messages marked as Spamassassin with help of X-Spam-Level header.

#cat /etc/postfix/header_checks
/^X-Spam-Level: \*\*\*\*\*\*\*.*/ HOLD custom spam rule

and uncommented this line in:

#grep header_checks /etc/postfix/main.cf
header_checks = regexp:/etc/postfix/header_checks

The above mentioned HOLD action will put the messages into the hold queue for further inspection. Other option is to REJECT the messages (more here external_link).

To my dismay, it just didn’t work when I gave it a try with help of Gtube. What I didn’t realize was that header_checks happen while message is being received. Spamassassin, however, works as a milter that adds extra headers later, so it couldn’t work. There is a Postfix feature designed to solve this problem – milter_header_checks – which does the same thing, except it takes headers added by milters into account, too. The only tiny drawback was that this feature was added to Postfix 2.7 and my Centos 6 had Postfix 2.6 running. There was a patch on Postfix page which backported milter_header_checks into version 2.6, but I just didn’t have enough courage to go for it. Instead I used the workaround discussed here external_link (many thanks). The trick is to create another service in master.cf and use it as a content filter for the main smtp service. That way, the Spamassassin headers get applied and on the second run, they get noticed by header_checks.

#master.cf
smtp      inet  n       -       n       -       -       smtpd
  -o content_filter=smtp:127.0.0.1:10025
127.0.0.1:10025 inet  n -       n       -       -       smtpd
  -o content_filter=

I also had to add permit_mynetworks in recipient restrictions, but that was probably relevant just to my particular setup.

#main.cf
smtpd_recipient_restrictions = permit_mynetworks, 

After the postfix reload, the spam messages with score higher than defined in /etc/postfix/header_checks finally got caught and stopped.

Update 21. 2. 2017:

There was one unintended side effect with the above mentioned solution – all forwarded emails were duplicated and arrived into the final mailbox twice. When there was forwarding set in the aliases file, the rule got applied on both smtpd service, the external one, as well as the internal one, where the header_checks happened. So this behaviour had to be suppressed using receive_override_options=no_address_mappings option to prevent this unintended duplication:

smtp      inet  n       -       n       -       -       smtpd
  -o content_filter=smtp:127.0.0.1:10025
  -o receive_override_options=no_address_mappings
127.0.0.1:10025 inet  n -       n       -       -       smtpd
  -o content_filter=