GNU Mailutils Sieve - filtering email using the Sieve standard

Filtering E-mail

Many users want to filter their email, whether that's to remove spam or to organise it into different folders. Most people will use their email client's filtering feature. Unfortunately, many command line email clients (including my favourites NeoMutt and Mutt) don't have filtering built-in. However, there is an standard called Sieve for filtering email which can fulfil the requirement. This page covers how to use Sieve with a local email set-up: in this set-up there's a local email folder (maildir or mbox) which new email comes into, and Sieve is called from the command line to filter the email.

If you've never heard of Sieve, it's a set of RFC's for filtering email. Sieve (RFC 3028) is a standard for users to be able to filter their own email. It's a bit strange it's not more popular, because as the RFC notes:

"Users will make use of filtering if it is offered and easy to use, this language has been made simple enough to allow many users to make use of it, but rich enough that it can be used productively

Most implementations are done on the server side, where the user uploads their rules and the server filters email into different IMAP folders. However, it's a general capability that can also be implemented in a way that runs on the command line: GNU Mailutils - which is widely available - contains such an implementation of Sieve.

The basics of a Sieve script is that it consists of a number of tests which are applied to the mail files in a mailbox. Each email is tested against all the tests in the Sieve script. If an email matches a test, then the actions for the test are carried out. Sieve scripts can do things like:

  • move emails into other folders
  • delete mails
  • send an automated reply
  • label and remove spam

⚠️ Backup all email and practise on a test mailbox before unleashing on your main email! ⚠️

Approaches to filtering email

There's quite a few options for dealing with the need to organise email, whether using filters or tags. These are the major ones I'm aware of:

  • Filter at the mail provider end
    Most email providers have some form of filtering capability, often accessed through a Web GUI editor. This is a popular option, but it's not portable between providers. So those well-tuned rules you've spent ages on will have to be implemented again if you move mail providers!
  • Filter in the email client
    Most modern email clients have email filters, for example Thunderbird calls this "Message Filters". Unfortunately, NeoMutt doesn't have them.
  • Filter the remote IMAP server
    IMAP-filter filters on the IMAP server side. I already use mbsync to synchronise my email to a local folder, so switching or using IMAP-filter didn't seem like a good option. See the NeoMutt resources page for details on using mbsync.
  • Use a specialist message delivery agent
    The most well-known is procmail but it is no longer maintained and should not be used. There are options like fdm or maildrop. Maildrop is the most promising of these. This method doesn't quite work for me as I'm using mbsync to directly deliver to my maildir, though it probably could be made to work by chaining things together.
  • Don't filter, use tagged search
    Tools like Mu, Lieer and Notmuch broadly aim to keep email in a big folder (think of Gmail's 'All Mail') and use tagged searches to find them. Mu and Notmuch have limited moving ability - Notmuch has afew. I think this would have worked for me, but it felt like too much complexity for my relatively straightforward needs.
  • Use Sieve
    As Sieve is a specialist language and standard it should be more popular. The main reason it's not is that the common scenario is to run it on the server side: if your email service provider doesn't offer it, then you can't use it. The well-known implementation is part of Dovecot IMAP server. Running a full IMAP server to just filter some emails would be mad! Well GNU Mailutils has a Sieve implementation that we can run over a local maildir directory.

GNU Mailutils

GNU mailutils is a collection of mail utilities, the home-page says it's "a Swiss army knife of electronic mail handling". It's available in pretty much every distribution so should be easy to use! There's a local mail delivery agent (called mda), and some mail servers (IMAP and POP3. Additional utilities include:

  • from: print out a list of addresses
  • mail: receive and send email
  • messages: count the number of messages in a mailbox
  • movemail: move mail from one location to another
  • readmsg: extract messages from a folder
  • decodemail: decode a MIME part
  • sieve: filter email using the sieve standard
  • guimb: mailbox scanning and processing
  • putmail: read a message from stdin and deliver into a mailbox
  • mimeview: detect a MIME type and invoke an appropriate file viewer
  • Message Handling (MH) tools: interface between Mailutils and MH-E

For testing and generally messing around with maildir folders I also used mblaze - it's a set of tools for dealing with maildir folders, worth checking out.

A key Mailutils capability is that it uses a standard URL structure for dealing with mailboxes and conversion between mailbox types:

# display messages in a mailbox, specifying the mailbox type
$ from --file=mbox:///home/<user>/.mail/example-messages

$ from --file=maildir:///home/<user>/.mail/test-account/INBOX/

In the first example it will display messages that are in a mbox file, and in the second it shows the messages in a maildir directory. The key is the URL syntax of maildir://<location>. It's particularly useful when moving email around between different mailbox types:

# move mail between main mailbox (mbox) to a test account (maildir)
movemail --verbose mbox:///home/<user>/.mail/main.mbox maildir:///home/<user>/.mail/test-account/INBOX

This lets us move mail between mbox and maildir set-ups, there's a --max-messages=N. This is useful for creating a temporary mailbox and then testing that a Sieve script does what's expected!

Sieve introduction

The Sieve RFC's describes a syntax for defining rules that take action on emails. It was designed to run on a email providers server by users uploading their rules, so it's consciously limited to try and avoid security or resource allocation issues. We don't get a full language, it's a set of tests and actions.

To use Sieve we create a sieve script and run the script over a mailbox. The sieve script examines each email in the mailbox. For each email it runs through the tests in the test script, when a test passes it processes the email according to the actions. In some cases the actions will tell Sieve to stop further steps, but the default is that Sieve will continue to check the email against each test.

The basic structure of a test is:

if <test>
{
   # some comment
   <action_1>;
   [action_2];
}

For example, to check for email from a particular email address:

if address "from" "bsmith@example.com"
{
    keep;
    stop;
}

In this example, the if is a Command, which runs the test. The Test is to look at the address in the email: it's told to specifically check the from header. If it is "bsmith@example.com" then the test is true so the Action block runs. Notice that the action block is within brackets. The first action is the keep the email (a Noop) and notice that it ends with a semicolon. The second action is that Sieve is told to stop, so this terminates it from checking further rules - if this wasn't there then even if a rule matched Sieve would check all other rules.

It's also worth noting that all email headers and content are case insensitive - "from", "From", "FrOM" are all the same.

Mailutil's sieve is designed to be extensible, so you have to tell it which tests and actions to load. At the top of the test file we have:

# load a single extension
require "fileinto";

# load multiple modules in a list - square brackets with a command between each module
require ["fileinto", "variables", "pipe", "test-pipe", "test-timestamp"];

The require line loads the specified module, and it ends with a semi-colon. The second example is loading multiple modules. It does this by using the generic list which is between square brackets and with commas between items. Lists can also be used in tests (so to test for multiple different email addresses for example). The fileinto, variables and pipe are actions, while test-pipe and test-timestamp are tests.

The Sieve language consists of tests, logic and flow control and actions. This page covers the most important (but not all) parts as follows:

Sieve tests:

  • True & False: automatic true or false
  • Exists: whether a header exists
  • Address: any structured address header
  • Header: any field in the header
  • Envelope: the smtp envelope
  • Size: test the size of an email
  • Timestamp: test a date
  • Pipe: the output of an external command
  • Spamd: the result from running the spamd command
  • List: test more than one field

Sieve test logic and flow control:

  • allof - all tests must pass
  • anyof - one test must pass
  • not - reverse the meaning of the test
  • if / elsif /else - test flow control
  • stop - don't check this email against further tests

Sieve actions:

  • stop: don't process this email through any further rules (see earlier)
  • keep: keep the email that's being processed (the implicit action)
  • fileinto: file the email into a specified folder
  • discard: delete the email
  • reject: reject the email and send back a message to the sender
  • redirect: redirect (bounce) the email to another address
  • pipe: run a command or script
  • variables: use variables within the sieve script
  • vacation: send an email to the sender informing them that we're not reading email

Running sieve

The NeoMutt resource page covers the details of my e-mail set-up. But, the summary is that I download my email using Mbsync into a local Maildir folder. This means I'm not running sieve as part of my local mail delivery agent, since my email is directly placed into my INBOX. Instead I "post process" it afterwards by executing it on the command line. I run it like this:

$ guix shell mailutils
[env]$ sieve --mbox-url=maildir:///home/<user>/.mail/INBOX --dry-run --verbose ~/.bin/sieve-script1.sv

This specifies the --mbox-url (also -f) to run the sieve script over: it uses the standard URL definition we discussed earlier. The --dry-run (also -n) option means no action is taken, it outputs what's happening in the terminal window. Check the man page for additional debugging options.

In many instances there's no need for a configuration file. I found that in order to use some of the Sieve actions like redirect a ~/.sieve configuration file is required. To make sure that configuration is correct add --config-verbose. The output will look something like this:

sieve: opening configuration file /etc/mailutils.conf
sieve: configuration file /etc/mailutils.conf doesn't exist
sieve: opening configuration file /home/user/.sieve
sieve: parsing file `/home/user/.sieve'
sieve: finished parsing file `/home/user/.sieve'
sieve: /home/user/mail-test/sieve-test.sieve:65.5-36: FILEINTO on msg uid 21: delivering into maildir:///home/user/.mail/test-account/to-sales
sieve: /home/user/mail-test/sieve-test.sieve:66.5-8: STOP on msg uid 21
[ ... lots more output ...]

We can see that it's parsed the sieve file and the configuration passes. It then runs the sieve script (/home/user/mail-test/sieve-test.sieve) and the first email is uid 21. It tells us that the test on row 65 of that file has a FILEINTO action for that message, and that it's delivered the email into a maildir (/home/user/.mail/test-account/to-sales). The second test tells us that on row 66 of the sieve-test.sieve file there's a STOP action which ends processing of the email message.

Sieve Filter examples

To show the specific capabilities of Sieve here are some examples of common filters. I took these from the ProtonMail and Fastmail subreddits where lots of people ask about using Sieve.

Test for a specific To email address

A common need is to filter depending on who the email's recipient is (the "To:" address). For example, if we wanted to filter out all email to shop@mydomain.com.

The best option for filtering on a specific address is the address test which examines certain structured parts of the header. The easiest form is:

require ["fileinto"];

if address "to" "shop@mydomain.com"
{
    fileinto "shop-folder";
    stop;
}

This will use the address test, look at the "To" field specifically and check if that field matches "shop@mydomain.com". Note that the "to" is case-insensitive. If an email test's true, then the action block is run.

The first action is the fileinto action: this will file the email into the folder that's specified in a string. The action ends with a semi-colon. The second action is to stop which means that Sieve will not check any other tests for this email: if this isn't specified then Sieve will check the each test in the sieve script.

Lets say that we have multiple shops each on their own domain (shop@mydomain.com and shop@myothershop.com). We've got a couple of options:

require ["fileinto"];

if address :localpart "to" "shop"
{
    fileinto "shop-folder";
    stop;
 }

The address test can examine a part of the email, specifying :localpart means it will only look at the part in front of the at symbol. To be more specific we could specify both full addresses that we want matched by putting them into a list.

require ["fileinto"];

if address "to" ["shop@mydomain.com", "shop@myothershop.com"]
{
    fileinto "shop-folder";
    stop;
 }

This uses the list ability (within square brackets and each item is separated with a comma) to specify the addresses. It's better than the previous one as it will fully match the email addresses that we want.

File an email into a folder

The fileinto action is the most used as it lets us move or copy an email into a folder.

Lets say we have multiple temporary addresses which we want to filter out bob+tvshop bob+supermarket bob+videotapeshop

The best way, as it's a complete match, is to use address:

if address :is "to" ["bob+tvshop@mydomain.com", "bob+supermarket@mydomain.com", "bob+videotapeshop@mydomain.com"]
{
    fileinto "read-later-folder";
    stop;
}

This will look at the To line of the email and look for an exact match to any of the three emails.

The variables extension is useful if doing a lot of filing as it can be used to create a paths:

require ["fileinto" "variables"]
set "folder" "maildir:///home/user/.mail/later"

if address :is "to" ["bob+tvshop@mydomain.com", "bob+supermarket@mydomain.com", "bob+videotapeshop@mydomain.com"]
{
    fileinto "${folder}/read-later-folder";
    stop;
}

The require lines loads the variable extension. The set creates a variable called folder which points at the right folder. Then in the fileinto the ${folder} expands the final path to /home/user/.mail/later/read-later-folder.

Forward all emails

Although filing email into a folder is the most important action there are others, like redirect.

require ["redirect"];

if address is "to" "bob@example.com"
{
    redirect "bob@newplace.com";
    stop;
}

Normally, if you forward an email to someone it's clear that you did so as the address of the email will be yours, and the forwarded email is an attachment. The redirect action is designed to "bounce" the email to the new address, which is not the same. When "bob@newplace.com" opens their email client it will look like any redirected email was sent to them directly.

In order for this to work sieve needs to be able to call an SMTP server to send the email. This can either be a local Sendmail compatible program, or a remote SMTP server. To configure it create a ~/.sieve file. If using MSMTP it would be:

{
    sendmail:///home/user/bin/msmtpq;
}

For a remote SMTP server it is:

{
    smtps://user:password@mymailprovider.com
}

If not specified Sieve will create the From: line of the SMTP envelope by using the hostname of the machine (this probably isn't what you want). To specify a valid line add the following to the configuration file:

sieve
{
    email me@mydomain.com
}

Testing for subdomains

The address test looks for an exact match by default. But, it's sometimes useful to have more flexibility, so there's other match options (:contains, :matches and :regex). The :contains match type will match a substring match.

For example, lets say that we need to monitor email from some servers named "web1.example.com" and "web2.example.com, as well as "monitoring.example.com" we could do a substring match like this:

if address :domain :contains ["web" ,"automated.example.com"]
{
    keep;
    stop;
}

This will checks the domain part of the email for either "web" or the full "automated.example.com". To make the search more precise it could be tightened to using one of the fields that address looks at - "From, To, Cc, Bcc, Sender, Resent-From and Resent-To".

Testing multiple email addresses

The next option is to use :matches which matches the entire string using a wildcard expression. In this example, we want to find email to a couple of different email addresses (andy-smith@example.com and andrew-smith@example.com) and put them into a folder.

if address :matches "to" "a*-smith@example.com"
{
    fileinto "smiths-email";
    stop;
}

The :matches match-type supports using "*" to match zero or more characters, and question mark to match one character. In this example email to "andy-smith@example.com" and "andrew-smith@example.com" will also match.

There's also a :regex search match, see the documentation for the details.

Testing multiple fields

To ensure that a match is as close as possible it's often useful to match more than one field: that way the match can be more precise. Sieve has the ability to add logic to tests with allof or anyof.

For example, in this example we want to sort email from our boss that has "URGENT" in the subject line:

if allof ( address :is "from" "myboss@example.com",
           header :contains "subject" "URGENT" )
{
    fileinto urgent-mail;
    stop;
}

To group tests they are places inside brackets and separated by a comma. The test logic allof checks whether both tests are true, if they are it executes the actions.

Filtering mailing lists

The address test is the best option as it looks at the specific address headers: If not specified it looks at "From, To, Cc, Bcc, Sender, Resent-From and Resent-To". For more complex filtering the headers test can check any header within the email.

There are quite a few mailing lists and alerts that can go to a different mailbox. Imagine you're subscribed to the Guix Codeberg account and want to filter out emails about pull requests. The characteristics of the emails are that the headers contain:

This can be translated into a rule:

require ["fileinto", "variables"];
set "folder" "maildir:///home/user/.mail/test-account/";

if allof ( header :contains "X-Gitea-Repository-Link" "https://codeberg.org/guix/guix",
            header :contains "X-Forgejo-Reason" "pull" )
{
    fileinto "${folder}/guix/guix-prs/";
    stop;
}

The first line loads the required extensions, the fileinto and variables actions. Then the folder variable is set so that it's easy to file email into the right location. The test uses alloff for test logic - testing both that the header contains the right link and that the reason is a pull request. If both tests pass then the email is filed into a specific email box.

Move old email to an archive folder

This example uses the timestamp test to check whether a mailbox contains email that is over 30 days old, and if so to file them into an old-mail folder. The timestamp test is a bit tricky as it didn't seem to support all the options that the manual says it does, but this works:

require ["fileinto", "variables", "test-timestamp"];
set "folder" "maildir:///home/user/.mail/account";

if timestamp :before "Date" "30 days ago"
{
    fileinto "${folder}/old-mail/";
    stop;
}

The test support a :before part so that we're checking if the email is before a date. The format XX days before (can be days/months/years) works - some of the other ones didn't work (I probably misunderstood the manual).

Tests

The GNU Mailutils Sieve manual covers all the different sorts of tests that Sieve has. These are:

  • True & False: automatic true or false
  • Exists: whether a header exists
  • Address: any structured address header
  • Header: any field in the header
  • Envelope: the smtp envelope
  • Size: test the size of an email
  • Timestamp: test a date
  • Pipe: the output of an external command
  • Spamd: the result from running the spamd command
  • List: test more than one field

Note that this isn't all tests, just the ones I found interesting - the manual.

Test: True & False

These two tests evaluate to true and false. I couldn't come up with a good use-case for them, other than for testing or doing something that happens on every run (maybe run a script).

if true exists "To"
{
    keep;
}

Test: Exists

Tests true if a headers exist. This can be useful, as in some cases there's no real need to check the contents of the header.

if exists "X-GNU-PR-Package"
{
    fileinto "gnu-stuff";
    stop;
}

In this example the tests whether the "X-GNU-PR-Package" header exists, if it does that's sufficient to file it into a folder.

Test: Address

The test address looks at email addresses in structured headers. It's probably the most useful test. It's a defined object and has the following elements:

  • :localpart - the part in front of the at symbol
  • :domain - the part after the at symbol
  • :all - the whole address, this is the default.

This test has a lot of flexibility:

address [address-part] [comparator] [match-type] header-names key-list

if address :is "from" "Bob@example.com"

if address :is ["to" "cc" "bcc"] "Bob@example.com"

The first example would look at the "from" address only, the second would look at the headers specifically listed. If not specified this test looks at "From, To, Cc, Bcc, Sender, Resent-From and Resent-To".

A good use of the :localpart is to find virtual addresses:

if address :localpart "to" "bob+shop1"
{
    fileinto "shopping";
    stop;
}

An example of using the :domain part is to move work email into a folder:

if address :domain "from" "mywork.com"
{
    fileinto "work";
    stop;
}

The address test only looks at the structured headers, to access all headers (including the SMTP envelope) use the headers test.

Test: Header

The header test can be against any header in the email. To specifically focus on an email address use the address test, and there's also the envelope test to look at the SMTP envelope. To be as precise as possible it's often useful to use test logic to match more than one header.

The header test is always a case-insensitive test (from RFC 3028 String Comparison) - this is true of all tests.

Generally, this test should be used when not matching an actual email address (use address for that).

header [comparator] [match-type] [:mime] header-names(string-list) key-list(string-list)

# example testing multiple headers
require ["fileinto", "variables"];
set "folder" "maildir:///home/user/.mail/test-account/";

if allof ( header :contains "X-Gitea-Repository-Link" "https://codeberg.org/guix/guix",
            header :contains "X-Forgejo-Reason" "pull" )
{
    fileinto "${folder}/guix/guix-prs/";
    stop;
}

Test: envelope

The envelope test is applied to the SMTP envelope. Since you can pretty much set anything you want in an SMTP envelope this isn't a safe test: for example, anyone can telnet to an SMTP port and set the From of the SMTP envelope to be anything they want. Generally, using test header is going to be better.

Proton mail seems to have a custom implementation that seemed to be used, at least in their implementation using return-path and envelope-to were examples.

Test: size

Test the size of an email. I don't have a need for this, so this example is totally made up:

if size :over 2M
{
    reject text:-EOT
      Hi,
        Thanks for your email, but it's too large, please send with a link for download
      EOT
      ; # must have the semi-colon on a separate line
      stop;
}

If the email size is over 2 MB (it can match on M or K) then send then reject the email. The reject action can use the here-doc concept.

Test timestamp

The timestamp test is used to check the date of a structured header against a given date. It's a GNU Sieve extension that is using the various inputs that the date command understands: the manual has an extensive appendix covering the various inputs which was a bit impenetrable! The main need is to look for a date in the past, using "90 days ago" works, but I couldn't get the manuals example to work ('now - 90 days'), this seems to show an operation but unclear.

There's a Date and Index (RFC520) but GNU mailutils doesn't implement it, looks like Sievelib has partial support. I guess for more complex needs it would be possible to use sievelib via the pipe test.

Test: timestamp [:before | :after] header(string) date(string)


 require ["fileinto", "variables", "test-timestamp"];
 set "folder" "maildir:///home/user/.mail/account";


 if timestamp :before "date" "30 days ago"
 {

     fileinto "~/.mail/old-email";
     stop
 }

This works if you specify "days ago".

There's a lot of variety in the data input formats, but what operations you can do is a bit unclear:

Date: Sun, 01 Jun 2025 12:08:59 +0200

2025-05-31      # ISO-8601
20:00 GMT BST   # time with timezone and daylight saving
1 month ago     # previous to the current month that 'now' is within
1 year ago      # didn't test, but presumably works
#-1 month       # works in `date` but can't get it to work here
30 days ago     # works
after Sunday    # unclear

Test: pipe

There's a test pipe and an action pipe. Both provide the ability to call an external command (script). The test one passes or fails depending on whether the script returns 0 or something else.

Test: pipe [:envelope] [:header] [:body] [:exit code(number)] [:signal code(number)] command(string)

The test is run for each individual email that's in the mail folder.

Test: spamd

Connects to the spamd daemon and checks whether an email is spam or not. Super useful, but haven't tested as my email Provider does this for me.

Test: spamd [:host tcp-host(string)] [:port tcp-port(number)] [:socket unix-socket(string)] [:user name(string)] [:over | :under limit(string)]

Test list

This test checks for headers from a set of keys. The example in the manual is about dealing with spam where the email has an X-spamd-keywords header and the test checks for keywords. I haven't had to use this test, but it seems useful!

Test: list [comparator] [match-type] [ :delim delimiters(string) ] headers(string-list) keys(string-list)

Test logic and flow control

For the best results we need to make tests as specific as possible. It's often the case that to be specific the best approach is to test multiple fields: maybe there's a wide-ranging match on the 'To' field, but the test can also check if a particular header is there as well.

  • allof - all tests must pass
  • anyof - one test must pass
  • not - reverse the meaning of the test
  • if / elsif /else - test flow control
  • stop - don't check this email against further tests

Allof

The allof test logic adds the ability to check multiple tests. If all tests pass, then the action is run:

if allof ( address :is "From" "bsmith@example.com",
            header  :contains "Subject" "URGENT" )
{
    fileinto "urgent-mail";
    stop;
}

This example shows two tests, an address and header one, they are grouped using the brackets around them. The allof means that both tests have to pass for the action to be run: email has to come both from bsmith@example.com and contain the word "URGENT" somewhere in the Subject.

Anyof

The anyof test logic checks that any of the specified tests pass. If one test in the group passes, then the action is run.

I haven't actually needed this one, so I don't have an example of it.

Test: not

The test not takes a test as an argument and returns the opposite:

# discard email that is NOT to Bob
if not header :is "To" "bob@example.com"
{
    discard;
}

In this example the test looks for the specified email address in the header. Normally if it founds it the test would be true and it would execute the action - in this case not reverses that.

I haven't actually found a situation (yet) where I've needed this capability.

If / elseif / else

A Sieve script has a series of tests within it, and the sieve command runs each tests against a particular email, in order. Consequently, there's only one flow-control which is if. If the rule is complex then elsif can be used to create different branches in the logic: I actually don't have any examples where this happened. Finally if all if's and elseif's don't return true then a final else can be specified.

if <test1: test> <block1: block>

elsif <test1: test>

else <block>

I actually haven't run into any circumstances where I really needed this flow-control. There are a few tests (for mailing lists) where it's nice to 'cluster' them together as they're all somewhat associated, it could be a series of if tests, but I'm using elsif to group them.

Action: stop

The stop action ends all processing on an email, this means that this email will not be checked with the other tests within the Sieve script. It's really useful because normally an email is checked against every test in the Sieve script:

  • test 1: -> action
  • test 2: -> action

This means multiple actions could be applied to the same email: a good example of this is that an email could be both filed and redirected to someone else.

However, in a lot of cases (particularly when filing emails) if a rule has tested true then the action is the only one we want to apply. Using stop is the wasy to deal with this.

Match type

Tests generally specify a field and a target that's supposed to be found in the field. The match type is how that target will be found. The options are:

  • :is - exact match
  • :contains - substring match
  • :matches - substring match with wildcard expression
  • :regex - use POSIX extended regular expression to match

Match type: is

The value must match the specified test value exactly. This is the most precise option and should be used if possible

if address :is "from" "bsmith@example.com"
{
  keep;
  stop;
}

Here the from field must precisely match bsmith@example.com.

Match type: contains

The :contains match type means the value must be a substring match:

if address :domain :contains ["mydomain1", "sales.otherdomain"]
{
    keep;
    stop;
}

This test checks whether the email has an address domain (the part after the @ symbol) of either "mydomain1" or "sales.otherdomain". As it's using a substring match it will catch a variety of domains, for example "mydomain.co.uk", "mydomain.com", "sales.mydomain" would all pass this test. To be more precise the address field could be tightened to a specific header, but this illustrates substring matching.

Match type: matches

The :matches match type is a bit looser (or flexible) as it matches the entire string using a wildcard expression. The rules are:

  • using * matches zero or more characters
  • using ? matches one character
  • the * and ? must be escaped using backslash

For example, lets say that we send email as "andy-smith@example.com", but also "andrew-smith@example.com" and we want a test for that:

if address :localpart :matches "a*-smith"
{
    fileinto "smiths";
    stop;
}

In this case it will match "andy-smith" and "andrew-smith" because the star matches zero or more characters. It will also match "a-smith", or anything before the hyphen. It's essentially a constrained regular expression.

Match type: regex

Uses a POSIX extended regular expression to match:

if address :localpart :regex "^bsmith$|^bob[by]*smith$"
{
    fileinto "priority";
    stop;

}

This will match email in the To header using the local part of the email (the part in front of the at symbol). The regex looks for "bsmith", "bobsmith" and "bobbysmith" and in all cases put them into a priority folder. This type of rule is too wide for most usage, because most email will be sent "To" the person, but it's an example of what can be done.

There's a numaddr extension for mailutils that can be used to tighten up the rule further by checking if the email address is solely to the individual user.

Actions

If a test returns true then the actions for the test are run. These are grouped together with parenthesis (squiggly brackets!). Each action is ended with a semi-colon. Recall that the email will be tested against all tests in the sieve script unless the stop action (see earlier) is specified.

GNU Mailutil's sieve has a lot of actions and new ones can be created. The main ones are:

  • stop: don't process this email through any further rules (see earlier)
  • keep: keep the email that's being processed (the implicit action)
  • fileinto: file the email into a specified folder
  • discard: delete the email
  • reject: reject the email and send back a message to the sender
  • redirect: redirect (bounce) the email to another address
  • pipe: run a command or script
  • variables: use variables within the sieve script
  • vacation: send an email to the sender informing them that we're not reading email

Action: keep

The keep action is the default implicit action, so if no sieve test alters an email (e.g. filesinto or disards) then it will be left in the mailbox that sieve is being run against. This makes sense as whatever is left over should be left in the mailbox.

RFC 3894 says that using keep in a test will impact all rules thereafter, so a later discard won't work. I've tested this and it's not correct for Mailutils' Sieve. Consequently, we can do:

if exists "to"
{
    fileinto "maildir://home/user/.mail/backupemail/";
    keep;
}

if address :is "from" "sam@annoying.com"
{
    discard;
    stop;
}

In this case the first test has keep so that the email is copied rather than fileinto's normal behaviour which is to move the mail into the folder. The second test then looks at email and finds any that are from a particular person and discards them.

Action: fileinto

Files an email into a folder by moving it. The folder can be any of the supported mailbox types (e.g. maildir):

fileinto [:permissions mode] folder

 fileinto :permissions "g=rw,o=r" "~/shared"

 # example: file codeberg alerts into a folder
 require ["fileinto", "variables"]
 set "folder" "maildir:///home/steve/.mail/test-account";

 if header :contains "X-Gitea-Repository-Link" "https://codeberg.org/guix/guix"
 {
     fileinto "${folder}/guix/guix-issues/;
     stop;
 }

The folder is a string. Permissions specifies the permissions to use if the folder is created: the default is 0600 for file, and 0700 for directories.

There are a couple of caveats to be aware of:

  • the directories to the folder must exist

    For example if the ~/.mail directory exists but ~/.mail/projects doesn't then sieve will crash when told to fileinto "maildir://~/.mail/projects/project1.

  • copying rather than moving is tricky

    A fileinto action moves the email, it removes the implicit keep from the mail.

    The obvious solution is to add a keep to the rule. According to RFC 3894 this will cause problems if there's a later rule with a discard action as the explicit keep that's been set will prevent the discard working. I've tested this and it's not correct for mailutils:

if exists "to"
{
    fileinto "maildir://~/.mail/backup-location/";
    keep;
}

RFC 3894 specifies a :copy tag to fileinto but this isn't available in Mailutil's sieve. It looks like it's available in the Python sievelib

Action: discard

The discard action deletes the messages from the mailbox without any notification to the sender:

if address :is "from" ["bob@example.com"]
{
  discard;
  stop;
}

Action: reject

Whereas the discard action simply deletes an email, the reject action sends back a specified email message to the sender informing them of the rejection. Note that Mailutils must be able to send outbound email for a rejection email to be sent: see the redirect action for the details of how to configure Mailutils forth is.

reject reason

 if size :over 1M
 {
     reject "Sorry, I don't accept 1MB emails"
 }

The rejection reason is some text which is used in the email back to the user. It can be some text specified in double quotes, it can also be a heredoc or the #include directive can be used.

Action: redirect

The redirect action is used to send the message to another address. Sieve will process the message by talking directly to a local or remote SMTP server. The way it redirects it will make the message look like it was sent directly to the new address. As sieve is talking to an SMTP server this also needs to be configured in a ~/.sieve file.

[require "redirect"];

if header :is "To" "bob@example.com"
{
    redirect "bob@newplace.com";
    stop;
}

If using a local MTA like msmtp the sieve file has the following:

mailer
{
  sendmail:///home/steve/bin/msmtpq;
}

Whereas, if outgoing email is done through and SMTP server do this:

mailer
{
  smtps://user:pass@host
}

If not specified sieve will create the from line in the SMTP envelope using the hostname of the machine (this probably isn't what you want). To specify a valid from line in the configuration file do:

sieve
{
    e-mail myname@mydomain.com;
}

Rather than using ~/.sieve a specific configuration file can be provided with --config-file <file>, and to debug what's happening use --config-verbose.

Action: pipe

Just like the pipe test the pipe action runs a command (script). The tags control how much of the message is passed into the command:

pipe [:envelope] [:header] [:body] command(string)


# run a pipe if a specific address exists
require ["fileinto", "variables", "pipe"];

if address :is "To" "postmaster@example.com"
{

    pipe "/home/user/bin/pipe-action-sieve.sh";
    stop;
}

This works to execute the script, a simple example could be for an alerter!

Action: vacation

The vacation action will inform a sender that the recipient is not reading email - because they're on vacation! Or whatever reason you want to give.

It's a sophisticated action, so the manual page is worth reading as potentially it could lead to a reply being sent to every email.

require [vacation];

# send the email every 2 days
vacation :days 2 :file "on-vacation.txt"

Sieve extensions

GNU Mailutil's sieve has some extensions to standard sieve. The manual provides a lot of detail, but a couple were interesting.

Extension: variables

The variables extension allows the user to have variables in a sieve script. There are two types of variables, user-defined and match.

The user-defined variables are useful when dealing with complex sieve scripts. For example:

require ["variables", "fileinto"];

set "sender" "root"

if envelope :matches "${sender}"
{
    keep;
    stop;
}

A match refers to the most recently evaluated part of a match (from :matches or :regexp):

require ["fileinto", "variables"];
set "folder" "maildir:///home/user/.mail/test-account/";


if allof ( address :localpart :matches "to" "bob+*",
           exists ["X-list", "List-Unsubscribe] )
{
      fileinto "${folder}/mailing-lists/${0}-list";
      stop;

}

This example uses allof to match that a mailing list header exists, and that the localpart of the email is "bob+<something>". Lets says that this user has subscribed to a bunch of mailing lists using this sort of structure. When the match happens the variable extensions puts the whole match into "${0}", then "${1}" is used for the substring that is the first occurrence of the wildcard. In this case it uses that variable in the fileinto action, to place each one into the right virtual box. If the user subscribed to "bob+football", then it would be placed in a "football-list" mailbox.

Sieve Resources

Useful Sieve resources are:

Sieve Summary

I hope this is an accessible summary of the most important parts of the Sieve standard and specifically the GNU Mailutils implementation. Sieve's actually pretty straightforward and has more than enough capabilities for the average filtering needs. The main thing I haven't covered on this page is how to use it for spam control. It's interesting that both ProtonMail and Fastmail provide it, so perhaps it will become popular over time!

There don't seem to be that many Free Software Sieve implementations. The GNU Mailutils one covers all the main parts (and has some unique extensions). The most important element for me is that it's usable from the commandline, which is a big difference over Dovecot's Pidgeonhole. The only part that's really missing for me is the imap4flags extension providing the ability to set flags or labels on email.