A year ago, my friend Laura was wishing that email providers could do some tone filtering and reject messages that are too mean. Since I run my own email server, I thought it might be simple to set something like that up easily. Turns out, it’s not that hard, but it wasn’t exactly trivial to figure out.
My setup
I run have postfix running to receive messages from the internet. It passes them through SpamAssassin, which inspects the messages and adds a few headers that indicate whether or not it’s spam. Then it passes them on to dovecot , which stores the messages in mailboxes and then tells with my email client, Thunderbird, that I’ve got mail. I like this setup because I feel like I have a bit more control over my data. Besides, it’s fun!
The plan
The original request is here shown below. I figure, if I could just have the message go through a second filter after it goes through spamassassin, I could make it a custom script that counts swear words.
How to do multiple content filters on postfix
Learning materials include:
- Postfix After-Queue Content Filtering
- Dude who triggered a script via email but didn’t forward it to the mailbox
- Dude on maillist who asked the right question but got mediocre answers
- Another person who tried to figure it out
A lot of answers were along the lines of “Just get amavisd to do the other filter.” Maybe that’s the right answer but I’m not running amavisd and didn’t feel like setting it up just for this.
Then I stared at the postfix config for a while, in particular at this line:
1 |
spamassassin unix - n n - - pipe user=spamd argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient} |
That tells postfix to pipe everything off to spamc for content filtering. Then, once the filtering is done, the -e sendmail thing (according to the man pages for spamc) sends the filtered mail to sendmail, which delivers it to dovecot for filing. Aha! I bet you can just send the processed mail to an intermediate script that does its thing and then sends it on to sendmail! Turns out this works perfectly. Here’s my script (WARNING: it has swear words in it!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#!/usr/bin/python3 """ Check some stuff and modify header of emails coming in from the output of SpamAssassin. This is for Laura H's swearword detecting email system. """ import email import sys import subprocess sender, recipient = sys.argv[1], sys.argv[2] sys.stdin = sys.stdin.detach() msg = email.message_from_binary_file(sys.stdin) msg_st = msg.as_string() count = 0 for swearword in ['shit','fuck','cunt','bitch','bastard']: count += msg_st.count(swearword) if count>10: msg.add_header('X-Too-Many-Swearwords', 'YES') # relay the modified message back to postfix for delivery to dovecot p = subprocess.Popen(['/usr/sbin/sendmail', '-oi', '-f', sender, recipient], stdin=subprocess.PIPE, stderr=subprocess.PIPE) stdout = p.communicate(input=msg.as_bytes()) |
As you can see, this just expects an email to come in on stdin, checks it out, and sends it to the stdin of sendmail.
This is not particularly efficient, because it will take some time for python to process some huge attachment looking for swearwords in binary. I wouldn’t put this on a large-scale system. But the pathway works so adjust to suit your needs!
Your postfix config line changes to:
1 |
spamassassin unix - n n - - pipe user=spamd argv=/usr/bin/spamc -f -e /usr/local/bin/swearfilter.py ${sender} ${recipient} |
Processing in Dovecot
Now that we have a header that indicates swearwords (‘X-Too-Many-Swearwords’), we can take actions with it. The Dovecot PigeonHole extension interprets the Sieve language which is used to do stuff based on email content. I use it to filter emails server-side into folders and stuff, and to reject emails flagged as spam into my spam folder. You can actually adjust the filters directly from Thunderbird with an extension, so that’s cool (uses ManageSieve Dovecot extension too).
Here’s the user-level Pigeonhole/Sieve filter that deals with swearwords and does some other stuff. The spam filtering is in the global one, which I configured to run before this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
require ["fileinto", "envelope", "mailbox","reject"]; if header :contains "From" ["notifications@github.com"] { fileinto "INBOX.Lists"; } elsif header :contains "From" ["paypal.com","facebookmail.com"] { fileinto "INBOX.Lists"; } elsif header :contains "X-Too-Many-Swearwords" "YES" { reject text: Whoops! Your message was a bit too vulgar for the email server to process. Please adjust and resend. Thanks! . ; } |
That last clause simply rejects the email so I never even see it and tells the author to send a new email. Nice! Totally works. Gmail marks the response as spam though… typical!
Anyway, so that was fun, if not totally useless. I had enough trouble configuring that I hope this info helps someone do something actually useful! Good luck.