Filtering mail with Lua using imapfilter

Recently I came across imapfilter, and it's what I've always been searching for. The problem it solves is pretty simple: Client-side E-Mail filtering with something like Thunderbird is flawed because rules only apply while the application is running, which gets annoying when you also view mail on other devices.

Big E-Mail services of course noticed this issue and offer you to configure server-side filtering rules, however there is no standardization and the extent of options varies greatly.

Imapfilter solves this problem in a very simple way: It is very minimal, really only handling IMAP and exposes APIs for you to interact with it via Lua. Then you can run imapfilter with your config on an always-on machine like a SBC or a VPS and everything just works.

You don't really need to know Lua at all in order to do basic filtering, however it can help if you want to get more advanced.

Getting started

By default imapfilter reads from ~/.imapfilter/config.lua. Here's a minimal example:

options.timeout = 120
options.subscribe = true

account1 = IMAP {
    server = 'imap.mail.server',
    username = 'user@mail.server',
    password = 'secret1',
}

messages = account1["INBOX"]:contain_from("mailinglist@example.com")
messages:move_messages(account1["example-list"])

I think you get the gist of how imapfilter works by just looking at this example. It moves all mails in INBOX which are from mailinglist@example.com to the example-list folder. To test this config simply run

imapfilter

Sometimes the list address will only be in the CC field, we can just combine messages like so:

messages = account1["INBOX"]:contain_from("mailinglist@example.com") + account1["INBOX"]:contain_cc("mailinglist@example.com")

Imapfilter exposes many other functions aside from contain_from and contain_cc, they are documented in man imapfilter_config.

Always-On filtering using IMAP IDLE

I have imapfilter running in a daemon on a Raspberry Pi. To achieve this we use imapfilter's idle functionality:

while true do
	account1["INBOX"]:enter_idle()
	messages = account1["INBOX"]:contain_from("mailinglist@example.com")
	messages:move_messages(account1["example-list"])
end

Here the enter_idle function will hang until new mail is added to INBOX, then the rules are applied and because of the endless loop it then goes into a waiting state again.

My config

Sadly since everything is single-threaded you can't idle in more than one account at once. We solve this by just spinning up one imapfilter process per account using its own config file.

In common.lua I have a few shared configs and functions:

-- Options
options.timeout = 120
options.subscribe = true

-- Functions
function mailinglist(i, address, folder)
    messages = accounts[i]["INBOX"]:contain_cc(address) + accounts[i]["INBOX"]:contain_to(address)
    messages:move_messages(accounts[i][folder])
end

function do_idle(a, f)
    while true do
        accounts[a]["INBOX"]:enter_idle()
        f()
    end
end

Accounts are configured in accounts.lua:

-- Accounts
accounts = {}
credentials = {
    ["account1"] = {
		server = 'imap.mail.server',
		username = 'user1@mail.server',
		password = 'secret1',
    },
    ["account2"] = {
		server = 'imap.mail.server',
		username = 'user2@mail.server',
		password = 'secret2',
    },
}
function use_account(i)
    accounts[i] = IMAP{
        server = credentials[i].server,
        username = credentials[i].username,
        password = credentials[i].password,
        ssl = 'auto',
    }
end

Then each account gets a config file, so for example account1.lua:

package.path = os.getenv("HOME") .. "/.imapfilter/?.lua;" .. package.path

-- Accounts
require "common"
require "accounts"
use_account("account1")

-- Filtering
function filter()
    mailinglist("account1", "list1@example.com", "cool-list")
    mailinglist("account1", "list2@mailserver.org", "other-list")
end

filter()
do_idle("account1", filter)

I wrote a systemd service in /etc/systemd/system/imapfilter@.service:

[Unit]
Description=IMAPFilter for account %I
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/imapfilter -vc .imapfilter/%I.lua
WorkingDirectory=/home/imapfilter
Restart=always
RestartSec=60

SystemCallFilter=@system-service
PrivateTmp=yes
NoNewPrivileges=yes
ProtectSystem=strict

User=imapfilter

[Install]
WantedBy=multi-user.target

This way you can set up a daemon for account1 by simply running

systemctl enable --now imapfilter@account1

Further reading

With imapfilter there are really no limitations as to what kind of filtering you can achieve, you could implement advanced spam-filtering algorithms or call external programs and act upon their results.

You can find a well-commented simple config file here and a more advanced one here.