Filtering emails with sieve scripts and SourceHut on tomleb's blog
One of the annoyances I have had with emails has been filtering and managing messages. This usually rely either on email clients’ or the email provider’s filtering feature. Both have their pros and cons, but both are usually extremely limited. This is where sieve filters come in. We will go over how I use sieve to filter my messages, and how I configure and deploy my filter with SourceHut’s CI.
Sieve filters
Sieve is a programming language purposely designed for email filtering. It can be run on the email server as well as on the client side. It is also quite limited for security reasons; it does not have a loop mechanism or ways to run programs. The sieve language is an open standard described in RFC 5228. As such, any email provider1 that implements it will work similarly, depending on the extensions supported. This also prevents being vendor locked to proprietary systems.
Sieve allows us to filter incoming messages. We can discard messages based on
certain criteria like From
or Subject
headers and move messages into
specific folders. If the proper extensions are supported, it is also possible to
add flags to messages, like marking a message as seen, or tagging it as
important.
A sieve script is composed of three kinds of commands: action commands, control commands and test commands. Action commands are used to describe what to do with the message (eg: whether to keep it or discard it). Control commands are used to control which part of the script gets executed. It is possible to stop execution, require an extension or add a conditional branch. Finally, test commands describe the conditionals that must evaluate to true to take a branch.
Enough of vague descriptions, let’s see an example. Below you can see a simplified sieve script that I use to move messages from a mailing list to a specific folder on the mail server. The example is good enough to introduce few concepts from the sieve RFC.
# example.sieve
require ["fileinto"];
if header :contains ["From", "To", "Cc"] "~sircmpwn/sr.ht-discuss@lists.sr.ht"
{
fileinto "mailing_lists.~sircmpwn.srht-discuss";
stop;
}
fileinto "INBOX";
The line starting with the require control declares a list of extension dependencies. If not all the extensions are supported by the sieve server, then there will be an error when deploying the script. In this case, we are requesting the extension fileinto, allowing us to move a message to a specific mailbox.
Then, we have a conditional using the if control where we test if any of the
headers From
, To
or Cc
contains the value
~sircmpwn/sr.ht-discuss@lists.sr.ht
. If it does, then the branch is taken.
Otherwise, we continue evaluating the rest of the script.
In this case, when the branch is taken, we use the fileinto action to move the
message to the nested mailbox mailing_lists/~sircmpwn/srht-discuss
. Note that
my email provider uses a dot as path separator.
Then, we use the stop control to stop the execution of the sieve script to avoid hitting more rules.
The very last action moves messages to the INBOX
mailbox.
Advanced filters
The example above was a simple script that covers basic functionality. However, filters can be more complex to enable multiple different use cases. For example, there are test commands that check the size of the message. There are also test commands that evaluates to true if all tests commands evaluates to true, or if any tests commands evaluates to true.
Below is a more advanced sieve script that I currently use. It makes use of three extensions: fileinto, mailbox and imap4flags. One of which we have seen previously.
require ["fileinto", "mailbox", "imap4flags"];
# 1
if allof(header :contains ["From"] "builds@sr.ht",
header :contains ["To"] "me@tomlebreux.com")
{
# 2
if header :contains "Subject" "build failed" { addflag "$label1"; }
# 3
fileinto :create "builds";
# 4
stop;
}
-
We use the allof test to execute the block only if both of
header
tests are true. This means the allof evaluates to true only if theFrom
header containsbuilds@sr.ht
and theTo
header containsme@tomlebreux.com
. -
Inside the block, we check to see of the
Subject
header contains the stringbuild failed
, which would mean a build from SourceHut failed. In that case, we use the addflag action from the imap4flags extension to add a flag to the message. We add the flag$label1
which corresponds to theImportant
flag in Thunderbird. -
We use the fileinto action that we have seen previously to move the message to the
builds
mailbox. However, this time we use the:create
tagged argument from the mailbox extension. This ensure that the mailboxbuilds
will be created if it does not exist. -
We stop the execution of the sieve script.
Let’s see one last example that I really like to use when working with mailing lists.
require ["fileinto", "mailbox", "imap4flags"];
if header :contains ["From", "To", "Cc", "Bcc"] "~sircmpwn/public-inbox@lists.sr.ht"
{
if header :contains ["From", "To", "Cc", "Bcc"] "me@tomlebreux.com" { addflag "$label3"; }
fileinto :create "mailing_lists.~sircmpwn.public-inbox";
stop;
}
This example uses the concepts mentioned in the previous example. In this case,
I am moving messages from a mailing list to an assigned mailbox. I am also
adding a flag (Personal
green flag in Thunderbird) for messages in which I am
personally involved. This helps to see at a glance which messages might be more
of interest to me.
So far, we have seen the general format of the sieve language and some of the things you can do with it. The next section will cover how to tests scripts, how they can be deployed in general and how I deploy mine automatically using SourceHut.
Deploying Sieve scripts with SourceHut
You can test your sieve scripts with fastmail’s Sieve Tester. This is useful to quickly test scripts with your own email messages.
Alright, now that this is out of the way, let me explain how to deploy the sieve scripts on your email provider’s server. Each email service provider may implement its own way to let users upload sieve scripts2. This may be through GUI, or other means. Fortunately, there also exists the ManageSieve protocol documented in RFC 5804. This provides us with a standardized way to manage our sieve scripts if we have carefully chosen our email provider.
A ManageSieve service typically listens on TCP port 4190. I use the
sieve-connect utility to connect to a ManageSieve daemon. In the case of my
mail provider, the sieve server can be access at sieve.emailarray.com:4190
.
Here is an example usage of the sieve-connect
utility. The first command
validates the syntax. The second command uploads the script on the sieve server.
The third command activates the script.
# Syntax checking the script
sieve-connect --server sieve.emailarray.com --user 'me@tomlebreux.com' --localsieve example.sieve --checkscript
# Uploading the script on the server
sieve-connect --server sieve.emailarray.com --user 'me@tomlebreux.com' --localsieve example.sieve --upload
# Activate the sieve script
sieve-connect --server sieve.emailarray.com --user 'me@tomlebreux.com' --remotesieve example.sieve --activate
We have the necessary commands to automate the deployment process of our sieve script. Now we just need to create a git repository and add a SourceHut build manifest. Here is the build manifest I am using.
image: archlinux
sources:
- https://git.sr.ht/~tomleb/sieve
secrets:
# git.sr.ht ssh key
- 469ae73d-769d-4345-924e-1e7191f5de8f
# sieve password
- 3ae40a88-8fbd-4322-a1ef-78ce956a462d
tasks:
- sieve-connect: |
cd sieve/
makepkg --syncdeps --install --rmdeps --noconfirm
- check: |
cd sieve/
<~/.sieve sieve-connect --server sieve.emailarray.com --user 'me@tomlebreux.com' --passwordfd=0 --localsieve emailarray --checkscript
- upload: |
cd sieve/
<~/.sieve sieve-connect --server sieve.emailarray.com --user 'me@tomlebreux.com' --passwordfd=0 --localsieve emailarray --upload
triggers:
- action: email
condition: always
to: Tom Lebreux <me@tomlebreux.com>
Without going into too much details, I have three tasks that
- installs
sieve-connect
using its PKGBUILD, - validates the
emailarray
sieve script, - upload it to the server.
You should now be able to build your own sieve scripts and understand how you could deploy using the ManageSieve protocol. There are a lot more functionality possible because sieve is extensible. Happy filtering!
-
I went with PolarisMail as it was recommended by a colleague and meets necessary criterias listed on Drew Devault’s recommended email providers. ↩︎
-
Some email service providers might not even provide this feature. In that case, either use sieve filters on the email client, or switch to a provider that supports open standards. ↩︎
Contribute to the discussion in my public inbox by sending an email to ~tomleb/public-inbox@lists.sr.ht [mailing list etiquette]