Sieve filter (advanced custom filters)

ProtonMail offers users various ways to automatically filter emails by assigning tags or sorting them into folders. In general, there are three methods:

  1. Blacklisting/whitelisting senders such that they are always or never put into the spam folder;
  2. Creating a custom filter using ProtonMail’s interactive interface;
  3. [Most advanced] Creating a custom filter in Sieve.

Out of these methods, creating a custom filter in Sieve offers the greatest versatility, but it is also more complicated to use. For this reason, we consider Sieve an advanced feature for users with some technical experience. For most users, the interactive interface is suitable to build the custom filters you need.

Table of contents

What is Sieve?

Sieve is a programming language used to filter emails. You can create filters in Sieve by writing simple rules. For instance, “Give all messages from Kyle the green label”. Combining these rules can create a sophisticated filtering system.

You can write Sieve rules from scratch, borrow them from examples like the ones in this article, or use software that makes it easier.

In fact, we already offer you a way to create Sieve filters using the interactive interface, which uses your input to generate Sieve filters. Another good way to learn Sieve is by creating filters using the interactive interface and editing the filters in Sieve, as the interactive interface itself uses a subset of Sieve.

For instance, for the following filter, compare the interactive filters input and the Sieve code, when you view the same filter using EDIT SIEVE:

This article will introduce you to Sieve and explain how to write your own Sieve filters. If you need more information about using Sieve, there are numerous other tutorials online that can help. If you have questions about Sieve in ProtonMail that aren’t answered on this page, you can always reach out Support team here.  

Getting started

To start creating Sieve filters, go to Settings while logged in to your account at mail.protonmail.com, and then select the Filters tab on the left. On the Filters page, click on ADD SIEVE FILTER.

A Sieve script is made up of a list of commands. In most scripts it starts with a require command. The require command loads an extension that provides a certain functionality. For instance, to assign a label or put a message into a folder, we need the fileinto command.

To load this command we simply write:

require "fileinto";

To load multiple extensions you can use a list:

require ["fileinto", "imap4flags"];

In this case, imap4flags loads an extension that allows you to flag mail as read. After the require command, you will often perform some tests on the incoming message. This is done by combining if and another command such as address or header.

Lastly, if the tests succeed you can apply an action to a message. For example, suppose we want to put all emails from a specific sender into the same folder and flag the incoming mail as read. This could be written as:

require ["fileinto", "imap4flags"];

# I don't really like Spott
if address :is "from" "Spott.Tenerman@northpark.example.com"
{ 
    addflag "\\Seen";
    fileinto "enemies";
}

Note that the # sign indicates a comment and is not interpreted as part of the Sieve script. Also, the folder or the label you set (enemies here) has to exist in your environment: you can learn to create folders and labels here. After entering this filter, hit the SAVE button. Your filter will now run for every incoming email.

Of course, you might want to put emails from multiple senders into the same folder. In this case, you can pass a list of strings to the address command (a string is a series of characters, in this case an email address):

require ["fileinto", "imap4flags"];

# I don't really like Spott and Kyyyhel
if address :is "from" ["Spott.Tenerman@northpark.example.com", "Kyhel.Broski@northpark.example.com"]
{ 
    addflag "\\Seen";
    fileinto "enemies";
}

Note that whenever you can pass a string to a test condition, it’s usually also possible to pass a list. It will then try to find a match by trying any of the strings in the list. This doesn’t apply to commands like fileinto, addflag, etc.

But Sieve is more powerful than just reordering your mailbox. It also allows you to reject emails with a response message:

require "reject";

# Reject mails that spell my name wrong
if header :contains "subject" "Kyhel"
{
    reject "My name is not Kyhel";
}

Using tests

Combining tests in Sieve

As we showed you in the Getting Started section, you can perform tests using the if command on incoming messages to determine whether an email should be affected by a require command.

In addition to the if command, there are other test commands that allow you to structure your script in a simple way.

Else

The else command allows you to do something when the if command didn’t execute its actions. An example of this is:

require ["fileinto"];

# If the subject contains something incomprehensible, then put the mail into the kenny folder
if header :contains "subject" "mmph mmph"
{
    fileinto "Kenny";
} else { 
    fileinto "Understandable";
}

Here, the email will be moved in the folder Kenny if the subject is exactly mmph mmph. In the other case (so when the subject is not exactly mmph mmph) the email will be moved in the folder Understandable.

Elsif

The elsif is the contraction of else if. Like the else command, it will be executed if the if condition is wrong but only if the elseif condition is correct. If the elseif is not correct, then the next elsif or else block will be executed.

require ["fileinto", "imap4flags"];

# If the subject contains something incomprehensible, then put the mail into the kenny folder
if header :contains "subject" "mmph mmph"
{
    fileinto "Kenny";
# Kyhel sends me only speeches
} elsif address :is "from" "Kyhel.Broski@northpark.example.com" { 
    fileinto "Speeches";
} else { 
# otherwise the mail is important, so add a star.
    addflag "\\Flagged";
}

Note that only one of the if, elseif, or else blocks will be executed in a single run.

Anyof

Sometimes, you need to execute a command when one of several tests succeed. For this, you can use the anyof command. This is done by writing anyof followed by a parenthesis ‘(’, the statements you want to test separated by commas, and a closing parenthesis ‘)’.

require ["fileinto", "imap4flags"];

# Kenny either sends from kenny@northpark.example.com or puts "mmph mmph" in the subject.
   if anyof(address :is "from" "kenny@northpark.example.com", header :contains "subject" "mmph mmph")
{
    fileinto "Kenny";
}

This script will put messages that come from kenny@northpark.example.com or contain mmph mmph in the subject line into the folder Kenny.

Allof

The counterpart to anyof also exists: allof. This allows you to execute a command only when all given conditions match:

require ["fileinto", "imap4flags"];

# Kenny always sends me mails with mmph mmph
if allof(address :is "from" "Kenny@northpark.example.com", header :contains "subject" "mmph mmph")
{
    fileinto "Kenny";
}


This script will put messages that come from kenny@northpark.example.com and contains mmph mmph in the subject line into the folder Kenny.

Not

Lastly, sometimes you need to apply a command if something doesn’t match. Adding not in front of the statement will make sure that happens:

require ["fileinto", "imap4flags"];

# If a subject line does not contain real guitar put it into the young people folder
if not header :contains "subject" "real guitar"
{
    fileinto "Young people";
}

Which is equal to:

require ["fileinto", "imap4flags"];

# The else part will be evaluated if the condition is not true
if header :contains "subject" "real guitar"
{
    # do nothing
} else {
    fileinto "Young people";
}

This puts all emails that do not contain real guitar in the subject line in the Young people folder.

It is of course possible to combine all these tests together. For example, you may want to star an email if it is both not from Kenny and does not contain mmph mmph in the subject:

require ["fileinto", "imap4flags"];

if not anyof (    header :contains "subject" "mmph mmph", 
    address :is "from" "Kyhel.Broski@northpark.example.com" ) { 
    addflag "\\Flagged";
}

Performing tests on headers

Using the address command you can perform tests on address headers, such as the from, to, and sender header. You can extract different parts of the email address by using one of the following flags:

  • :localpart — the part before the at symbol (@)
  • :domain — the part after the at symbol (@)
  • :all — the whole address

The following snippet explains the use of this command:

require ["fileinto", "imap4flags"];

# Northpark people are made of paper, springfield are mostly yellow
if address :domain "from" "northpark.example.com"
{
    fileinto "PaperPeople";
} elsif address :domain "from" "springfield.example.com"{
    fileinto "YellowPeople";
}

if address :localpart "from" "chef"
{
    addflag "\\Flagged";
}

In summary, this script will put everything sent from the domain northpark.example.com in PaperPeople and springfield.example.com in YellowPeople.

Moreover, no matter what domain an email is sent from, if the part before @ is equal to chef (e.g. chef@example.com), then the message will be flagged. Another interesting usage is to automatically store everything sent to one address in a folder:

require ["fileinto", "imap4flags"];

# Put support mail in a separate organized folder
if address :localpart "to" "support"
{
    fileinto "Support";
}

Advanced

The envelope command allows you to perform more tests. In general, the address test only retrieves the value out of the header. However, in the SMTP session one can specify a different from address than the one in the header.

In the ProtonMail header interface, the envelope-from is equal to the return-path header and envelope-to is equal to the x-original-to header.

Using the envelope command you can retrieve the actual to: and from: addresses from the envelope. Note that no other fields than those two exist, so sender does not exist in this command. Please note that envelope is in an extension and thus requires you to require this extension first.

require ["fileinto", "imap4flags", "envelope"];

# Northpark people are made of paper, springfield are mostly purple
if envelope :domain "from" "northpark.example.com"
{
    fileinto "PaperPeople";
} elsif envelope :domain "from" "springfield.example.com"{
    fileinto "PurplePeople";
}

if envelope :localpart "from" "chef"
{
    addflag "\\Flagged";
}

Using comparators to evaluate two values

When a test evaluates two values, it is possible to specify how this comparison is done using different flags called comparators. Beforehand, we already used two comparators: :is and :contains, which checks that the given string is exactly equal to the specific value, and that the specific value contains the given string, as in the following test.

require ["fileinto", "imap4flags"];

if not anyof (
    header :contains "subject" "mmph mmph", # the subject contains mmph mmph 
    address :is "from" "Kyhel.Broski@northpark.example.com" # the recipient is exactly Kyhel.Broski@northpark.example.com
) { 
    addflag "\\Flagged";
}

The comparator :matches can also be used to define a more specific format. It will compare both values from the beginning to the end, just like the comparator :is. However, in the case of :matches, the value that you defined can contain the values ? and *. The question mark will match one character, and the star (called wildcard) will match zero or more characters.

With this format, you can create this test:

require ["fileinto", "imap4flags"];

if header :matches "subject" "mmph*" { 
    addflag "\\Flagged";
}

In this example, the test will succeed if the subject of the message starts with mmph and then contains any character or characters after it. In other words, the email will be flagged when the subject begins with ‘mmph’. ‘mmph mmph’ and ‘mmph mmph Hello’ will both match; ‘Hello mmph’ and ‘Springfield sales’ will not.

The star can also be used in the middle of the value and several times, as follows:

require ["fileinto", "imap4flags"];

if header :matches "subject" "mmph *mmph *mmph" { 
    addflag "\\Flagged";
}

This test will match if the subject begins with ‘mmph ’ (with a space) contains another ‘mmph ’ (with a space) and ends with ‘mmph’ (without a space). Matching subjects are ‘mmph mmph mmph’, ‘mmph mmph mmphmmph’ or ‘mmph is mmph and mmph’.

This :matches comparator can be very useful with address comparison. As you may know, ProtonMail has several domains: protonmail.com and protonmail.ch. If you want to check a message is coming from a ProtonMail user, you can use this script:

require ["fileinto", "imap4flags"];

# Put support mail in a separate organized folder
if address :domain :matches "from" "protonmail.*"
{
    fileinto "Internal";
}

If for some reason you need to match exactly the character * or ?, then you can quote them by adding \\ before: \\* will match a star and \\? will match a question mark.

Note that we also support the regex extension, which also defines the comparator :regex. This comparator is a more precise version of :matches, but is very complex. For more information, please read the official documentation.

In some cases (as you will see in the following of this article), you may want to compare numerical values. The package relational is made for this usage. This package defines the comparator :value which is followed by the comparison type.

It allows you to check if a value is greater than ( the comparison type being “gt”), greater than or equal to (“ge”), equal to (“eq”), less than or equal to (“le”) or less than (“lt”) the given value.

When comparing numerical values, the package comparator-i;ascii-numeric is also very useful. It tells to the script interpreter that the content of the string is a number, and not a  regular string. It can be used by adding to the test :comparator “i;ascii-numeric”.

With all of this information, we can build the following script:

require ["fileinto", "relational", "comparator-i;ascii-numeric"];  

if header :value "ge" :comparator "i;ascii-numeric" "subject" "2"   
{    
 fileinto "Dummy example";
}

This test will move the message to the folder Dummy example if the subject is greater or equal to 2. If the subject is not a number, but a text, then the test will fail. As such, this example looks very dumb, but it is intended to be used in combination with other extensions as date or spamtest, that we will define later in this article.

Comparison using context in Sieve

You can also access information related to your account and ProtonMail context in general using extensions. For example, you can check if the sender address is in your contact list, or you can adjust the spam threshold.

Accessing your contact list

You can access your contact list using the extlist extension. Combined with the header tests, you can then check if a contact is in your contact list.

require ["fileinto", "extlists"];  
# Checks that the sender is in your personal address book
if header :list "from" ":addrbook:personal?label=Family"   
{    
 fileinto "Known"; 
}

This test will flag a message with the label Known if the sender is contained in the list :addrbook:personal?label=Family. The list can be divided in two parts. First, :addrbook:personal means that the address is in your personal address book. Second, label=Family further narrows the list, specifying that on top of being in your address book, the contact must also belong to the contact group Family. If you need, you can change this label to another contact group. You could use :addrbook:personal?label=Work if you want, in which case the test would succeed only if the sender is in your address book and in the contact group Work.

More generally, a list respects the Tag URI Scheme, and you can add extra parameters to narrow your filters. Then you can use this list as follows:

require ["fileinto", "extlists"];  
# replace :your:list:here by the list you want to use
if header :list "from" ":your:list:here"
{    
 # some actions... 
}

We provided four different lists:

  • :addrbook:personal¹ checks if an address is in your contact list. The list accepts the parameter label that matches a specific contact group. This parameter can be declined in four subversions:
    • :addrbook:personal?label=something will match a contact that is in the contact group “something”;
    • :addrbook:personal?label.starts-with=something will match a contact that belongs to at least one group beginning with “something”;
    • :addrbook:personal?label.ends-with=something will match a contact that belongs to at least one group ending with “something”;
    • :addrbook:personal?label.contains=something will match a contact that belongs to at least one group containing “something”.

You can also access cryptographic information regarding the matching email:

  • :addrbook:personal?keypinning=true will match a contact that has a trusted key. Changing true to false will match a contact that does not have a trusted key;
  • :addrbook:personal?encryption=true will match a contact for whom encryption is enabled. Changing true to false will match a contact for whom encryption is not set up or is disabled;
  • :addrbook:personal?signing=true will match a contact for whom signing is enabled. Changing true to false will match a contact for whom signing is disabled.
  • :addrbook:myself¹ matches every address that is owned by yourself;
  • :addrbook:organization matches every address that is owned by someone in the organization you are member of;
  • :incomingdefaults:inbox checks if the address is in your whitelist;
  • :incomingdefaults:spam checks if the address is in your blacklist.

Combined with the variable extension (described in the next paragraph), the matching variables will be altered. The matching variable ${0} will always contain the last email address contained in the specified list. If the list was marked with the note 1, the matching variable ${1} will contain the display name.

For example, with the lists below, you can build a filter that will delete any emails received from anyone other than your family or someone on your whitelist:

require "extlists";  

# checks that the sender is not in the contact group Family, whilelisted or yourself
if not anyof(
    header :list "from" ":addrbook:personal?label=Family", 
    header :list "from" ":incomingdefaults:inbox",
    header :list "from" ":addrbook:myself"
) {    
  discard; # permanently delete the email
}

If that condition is fulfilled, the discard action will be executed. This action deletes the email immediately and permanently . You could also simply move it to the trash folder using a fileinto action instead of a discard: fileinto “trash”;.

Creating variables

Another useful tool when managing context is the variable definition. A variable is a temporary storage location in which you can put text and tag it with a name. Later on, you will be able to reuse this content by calling it by its name. For instance you could have the following script:

require ["reject", "variables"];

# First check who is the sender
if allof(
    address :is "from" "Kenny@northpark.example.com", 
    header :contains "subject" "mmph mmph"
) {
    # It's from Kenny!
    # Create the variable message containing 'mmph mmph'
    set "message" "mmph mmph";
} else {
    # Create the variable message containing 'Sorry, I don't want emails today!'
    set "message" "Sorry, I don't want emails today!";
}
# Then, reject the message
reject "${message}";

The goal of this script is to reject an email with a customized message. If the original email is from kenny and its subject is ‘mmph mmph’, the variable ‘message’ is created, containing mmph mmph. In the other case, the message will be filled out with ‘Sorry, I don’t want emails today!’. Then, on the last step we reuse this variable in the reject command.

Sieve does not natively define variables, so they are defined in the extension variables, which has to be required at the beginning of your script. There are two ways to define a variable.

  • Explicit creation of a variable:

The action set is provided to create a variable.

require "variables";

# Create a variable called "labelname" and containing the string "Work".
set "labelname" "Work";

The first string is the name of your variable and the second is the value of your variable. So the previous example creates a variable called labelname and containing Work.

Once a variable is defined, you can call it by adding the following format in a string: ${name}, where name is the name of your variable. When executed, it will be replaced either by the variable value. (If you have not defined the variable, it will be replaced by an empty string.) Thus, in the following script:

require "variables";
require "fileinto";
# Create a variable called "labelname" and containing the string "Work".
set "labelname" "Work";
# Move the email in "${foldername}/${labelname}" which becomes after variable resolution "/Work";
fileinto "${foldername}/${labelname}";

Your email will be tagged with the label /Work. Because we have already defined the variable labelname, it is thus replaced by Work. However, foldername is not defined, so it is simply removed.

  • Implicit assignation of a variable:

The most interesting use case of variables is when using a :matches test. Let’s take the following example, where the email sender is test@protonmail.com:

require "variables";
require "fileinto";

# do a matches test 
if header :matches "from" "*@*" {
    # The first * matches "test", the second "protonmail".  
    # Thus, the first matching variable contains "test"
    fileinto "${1}";
}

When used with the variables extension, the result of a match operation will be stored in the variables. Several variables will be set with a numeric name. First, the variable 0 will contain the full match (in our example that will be the full address: test@protonmail.com). Then, the first matching group will be assigned to the variable 1 (in our example that will be test), the second matching group will be assigned to the second variable (protonmail.com), and so on until the last matching group.

In our example, the command fileinto is executed. The location is defined to ${1} which is recognized as a variable and thus replaced by its value, as computed above: test.

Note that it is also possible to assign variables in a :regex test, using regular expression matching groups.

Transforming variables

One nice possibility is the transformation of variables. For that, the set keyword can be used with a combination of flags that can be used to change the value before assigning it to the variable.

  • :lower will change a variable value to lower case;
  • :upper will change a variable value to upper case;
  • :lowerfirst will change the first letter of the variable value to lowercase;
  • :upperfirst will change the first letter of the variable value to uppercase;
  • :quotewildcard will quote any wildcard contained in the string, so that it can be used literally in a match statement;
  • :length will return the length of the value.

Note that you can also reuse a defined variable in another set action. Let’s improve our previous examples:

require "variables";
require "fileinto";

# Set labelname to WORK, and modify it to lowercase with the first letter in upper case 
set :lower :upperfirst "labelname" "WORK";
set :lower :upperfirst "foldername" "geneva";

# Create a variable that is a combination of foldername and labelname
set "location" "${foldername}/${labelname}";

fileinto "${location}";

Step by step, the filter will create three variables. The first command set will create the variable labelname. The content of the variable is Work. Indeed, the original value WORK is transformed using both :lower and :upperfirst flags, changing the case work and then Work.

The second set command will create a second variable called foldername, and containing the string Geneva. Indeed, the flags :lower and :upperfirst were used.

Finally, the variable location is created. The value “${foldername}/${labelname}” contains two variables: foldername and labelname. So the sieve interpreter will replace these variables by their values: Work and Geneva.

Finally, the variable location is used as argument for the command fileinto. Thus, the email will be moved to the folder whom name is the value of the variable location: Work/Geneva.

The comparators can also be applied to matching variables:

require "variables";
require "fileinto";

if address :all :matches "from" "*@*" {
    set :lower :upperfirst "fileintovar" "${1}";
    fileinto "${fileintovar}";
}

Here, if the email’s sender is test@protonmail.com, it will be flagged with the label Test.

Another way to change the value of a variable is to use the vnd.proton.eval extension. This extension defines the new flag :eval, which will allow you to do some simple computations:

require "variables";
require "fileinto";
require "vnd.proton.eval";

# do a match test on the sender address
if header :matches "from" "*" {
    # create a variable called length, containing the length of the first     
    # matching variable
    set :length "length" "${1}"; 
    # Create a variable called fileintovar containing the result of the expression written below
    set :eval "fileintovar" "${length} * 25 - 1 / 8+3";
    fileinto "${fileintovar}";
}

In this example, and still considering the email is arriving from test@protonmail.com, the email will be flagged with the value 478. Indeed, the length of the first matching variable is 19, and the result of 19 * 25 – 1 / 8 + 3 is rounded up to 478.

This may appear useless at first, but this extension makes more sense combined with other operations, such as setting spam conditions.

Checking if an email is spam in Sieve

You can check if a message is spam with the help of the spamtest extension. This extension provides the test called spamtest. To evaluate the spam level of a message, a spam score is assigned to it. If the score is high, the message is probably a spam. If it’s low, it’s probably not a spam. Through the extension, you can access the spam score of the message and manipulate it. The score has the following meaning.

Spam score  Interpretation
0message was not tested for spam, or Sieve could not determine whether any test was done
1message was tested and is clear of spam
2 – 9message was tested and may contain spam; a higher number indicates a greater likelihood of spam
10message was tested and definitely contains spam

With this in mind, we can build the following script:

require ["spamtest", "i;ascii-numeric", "fileinto"];
# checks that the spam score of the current message is under or equal to 5
if spamtest :value "ge" :comparator "i;ascii-numeric" "5" {
    fileinto "trash";
}

This will move to the trash any message whose spam score is greater than 5.

Advanced: X-Spam-Status

If you look carefully in the headers of a message, you will see a line beginning with X-Spam-Status. This header is also a spam score, but does only take into account the current email, ignoring anything about your personal settings (contacts, whitelist, and blacklist). Therefore, both score are not always the same.

However, you can retrieve the value of X-Spam-Score with a header test, and use it to adjust the spam settings. For example, you can apply a label to a message if its original spam score is higher than the threshold you choose:

require ["environment", "variables", "relational", "comparator-i;ascii-numeric", "vnd.proton.eval"];
if environment :matches "vnd.proton.spam-threshold" "*" {
      set "threshold" "${1}";
   if header :matches :comparator "i;ascii-casemap" "x-spam-status" "*score=* *" {
 set :eval "doublethreshold" "${threshold} * 2";
       set "score" "${2}";
    
 if string :value "le" :comparator "i;ascii-numeric" "${doublethreshold}" "${score}" {
   fileinto "FULL SPAM";  
 } elsif string :value "le" :comparator "i;ascii-numeric" "${threshold}" "${score}" {
      fileinto "spam trap";  
 }
   }  
} 

Adjusting the native spam threshold

If you want to know whether ProtonMail will mark a message as spam, you can retrieve our internal threshold using the environment test (provided by the environment extension).

The environment test can be used to compare variables defined by ProtonMail against another value. Among others, the global variable vnd.proton.spam-threshold contains the spam threshold used by default in ProtonMail.

require ["environment", "variables"];

if environment :matches "vnd.proton.spam-threshold" "*" {...

Since the variable extension is used, the variable ${1} will now contain the spam threshold.

You can now compare it against the spam score:

require ["environment", "variables", "i;ascii-numeric"];
require "spamtest";
# retrieving the threshold in a variable
if environment :matches "vnd.proton.spam-threshold" "*" { 
    # comparing the spam score against the threshold
    if spamtest :value "ge" :comparator "i;ascii-numeric" "${1}" {
        discard;
    }
}

Now, what if you want to make the spam test stricter or looser? In that case, you can use the eval flag:

require ["environment", "variables", "fileinto", "i;ascii-numeric"];
require "spamtest";

# Retrieve the threshold in a variable
if environment :matches "vnd.proton.spam-threshold" "*" { 
    # Creates a variable called threshold containing the threshold minus 1
    set :eval "threshold" "${1} - 1";

    # Do the spam test 
    if spamtest :value "ge" :comparator "i;ascii-numeric" "${threshold}" {
        fileinto "spam";
    }
}

This test will reduce the threshold by one, which will result in more emails being marked as spam.

Comparison against other fields in Sieve

Matching on specific headers is also possible. For instance, to put all messages sent to lists (often they are marketing messages or newsletters), you can do:

require "fileinto";

# Filter all lists into the same folder
if exists "list-unsubscribe"
{
    fileinto "advertisements";
}

To sort social media emails into their own folder, you could write:

require "fileinto";

# Filter all lists into the same folder
if anyof(exists "x-facebook", exists "x-linkedin-id") {
    fileinto "social";
} elsif exists "list-unsubscribe"
{
    fileinto "advertisements";
}

Note that we use:

anyof(exists "x-facebook", exists "x-linkedin-id")

instead of:

exists ["x-facebook", "x-linkedin-id"]

As the latter checks if both x-facebook and x-linkedin-id has been set.

To actually look what value a header contains, you can use the header test:

require "fileinto";

# Put all mails that have been sent without TLS/SSL into the same folder
if header :is "x-pm-transfer-encryption" "none" {
    fileinto "unencrypted";
}

Apart from the :is operation, which does an exact match, the header command also supports :matches, which matches using wildcards (e.g. “*@*.com” will match any email address that ends in dot com), and the :contains command, which checks if the header contains a given string.

To design Sieve filters, it can be useful to retrieve the headers of an email. To do this in the ProtonMail web interface, you can click on the More menu (the arrow down button) and then View headers. A new window will open containing all the headers of the email.

Filter using message size in Sieve

It is possible to treat large emails differently than small emails. For instance, you might want to flag large emails, so that you can delete them to save mailbox space. This can be accomplished in the following way:

require ["imap4flags"];

# Flag emails that probably have large attachments (> 2 MiB)
if size :over 2M # you can also use 2097152 if you want, they are synonymous
{
    addflag "\\Flagged";
}

# Automatically mark as read really small messages. They can't have much content anyway...
if size :under 1000
{
    addflag "\\Seen";
}

As you can see, units are available if you want to specify big sizes, by adding the letter corresponding to the expected unit just after the number. Three unit are available: K for a kibioctet (or 1024 bytes), M for a mebioctet (or 1 048 576 bytes) and G for a gibioctet (or 1 073 741 824 bytes).

Please note that “over” in this case means greater than, and “under” means less than. This means that if a message has a size of exactly 1000 bytes, then neither

size :under 1000

nor

size :over 1000

will match.

Note that Sieve filters don’t have access to the actual content and only show the encrypted size.

Performing advanced actions on messages

You can execute different reactions to a message in Sieve. We already presented the actions fileinto, addflag, discard, and reject in the previous sections. Here we will present more advanced actions.

Vacation messages and date tests

You can replicate the auto-reply feature in Sieve using the vacation command. In fact, the auto-reply feature in your Settings relies on Sieve to do its job. But by using the vacation options inside a script, you have many more possibilities to customize your auto-replies. For instance, you can tailor specific messages depending on conditions. Note: As with the auto-reply feature in the Settings, the vacation action is only available in paid plans.

The vacation command sends a vacation response to anyone who tries to contact you. It is often coupled with the tests currentdate or date to send responses in a specific timespan.

Suppose I am going on holiday from July 14, 2017, until August 14, 2017, in Colorado time. I can use the following Sieve code to set an autoresponder:

require ["date", "vacation", "relational"];

if allof(currentdate :zone "US/Mountain" :value "ge" "date" "2017-07-14",
 currentdate :zone "US/Mountain" :value "le" "date" "2017-08-14")
{
    vacation "Queue you guys, I'm going on vacation.";
}

One of the optional arguments for a vacation command are the arguments :handle and :days. By default, Sieve will not reply multiple times to the same sender within a specific number of days called timeout. This prevents you from inadvertently sending numerous automated emails.

To control the timeout, you can use the parameter :days. The :days argument is used to specify the period in which addresses are kept and are not responded to, and is always specified in days. Sometimes, you have multiple vacation commands in your Sieve script, and you need to make sure that each one of them sends a reply at least once (if the rule matches). In that case the :handle option can be used. The argument to :handle is a string that identifies the type of response being sent.

To see how this works, suppose you are a teacher. Teachers often receive homework assignments by email. It’s useful to sort them in their own folder. Furthermore, you can reject any hand-ins that come after the deadline.

In such a case you might want to use handle to make sure replies are sent when needed:

require ["date", "vacation", "reject", "fileinto", "relational"];

if header :contains "subject" "Homework assignment 1"
{
    # remind people not to forget the attachments
    if size :under 5000
    {
        vacation :handle "Homework assignment 1 - missing attachment" "Your message size is really low. Please make sure you didn't forget to add the homework as an attachment.";
    }
   
    # check if the student made the deadline
    if  currentdate :zone "US/Mountain" :value "le" "date" "2017-06-12"
    {
        fileinto "Homework Assignment 1";
    } else {
        reject "Too late, you missed the deadline.";
    }
}

if header :contains "subject" "Homework assignment 2"
{
    # remind people not to forget the attachments
    if size :under 5000
    {
        vacation :handle "Homework assignment 2 - missing attachment" "Your message size is really low. Please make sure you didn't forget to add the homework as an attachment.";
    }
   
    # check if the student made the deadline
    if  currentdate :zone "US/Mountain" :value "le" "date" "2017-06-12"
    {
        fileinto "Homework Assignment 2";
    } else {
        reject "Too late, you missed the deadline.";
    }
}

Here are all the arguments allowed for the vacation response, listed in the order the arguments should be passed:

  • :days is the number of days the autoresponder should refrain from sending a response to the same sender after sending a vacation message
  • :subject is a prefix (by default auto) that the autoresponder should use to reply to the sender. For instance, “Homework Assignment 2” will receive a reply with “late: Homework Assignment 2” if :subject “late” is passed.
  • :mime indicates that the first line of response is using a specific format. This allows the sender to respond with html messages. For instance, to write an html response, pass the :mime argument and write in the first line of the response Content-Type : text/html.
  • :handle is a string that identifies the type of response being sent. It will be used as subject unless you use the :subject flag.

Please note the :zone parameter in currentdate is an optional parameter and will use the local time of the server (Geneva, Switzerland, in the case of ProtonMail) if it is not set.

The zone parameter accepts timezone offsets, which are strings in the form “+0100” meaning UTC+1, and actual timezones in the ICANN database (see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). The last option is often more useful (while being non-standard Sieve) as it also encodes the daylight savings time of each timezone.

You can compare dates by using the normal :is/ :contains/ :matches parameter. So for instance, the following Sieve command matches any date in July 2017:

currentdate :zone "US/Mountain" :matches "date" "2017-07-??"

But in most cases the :value parameter is much more useful.

The last parameter you need to pass is the format. The format also encodes which part of the date you want to compare. In the given example, we have used the date format. All supported formats are:

  • year, the year encoded in the format “0000” to “9999”
  • month, the month encoded as “01” to “12”
  • day, the day encoded as “01” to “31”
  • date, encoded as yyyy-mm-dd
  • hour, the hour encoded as “00” to “23”
  • minute, the minute encoded as “00” to “59”
  • second, the second encoded as “00” to “60” (60 is a leap second, only occurs at 23:59:60 whenever scientists deem it necessary)
  • time, the time as hh:mm:ss
  • iso8601, the date and time according to the ISO8601 standard, for example: 2005-08-15T15:52:01+00:00
  • std11, the date and time according to the RFC2822 standard, for example: Mon, 15 Aug 2005 15:52:01 +0000
  • zone, the timezone offset in the format +/-zzzz, for instance +0000 or -1200
  • julian, The number of days since November 17, 1858 UTC
  • weekday, the day of the week starting from Sunday as 0 until Saturday as 6

Lastly, it is also possible to retrieve the date from a header instead of using the currentdate. This date command works in the same way as currentdate, except that it requires an extra string before the format string: the header name. For instance:

date :zone "US/Mountain" :matches "date" "received" "2017-07-??"

Please note that the headers are not necessarily accurate: a sender can change them at will and are therefore not reliable.

Considerations:

  1. The autoresponder does not respond to automatically generated messages such as mailing lists, emails sent by another autoresponder, or messages sent by a noreply address.
  2. The autoresponder will not send messages multiple times to the same email address, except when the vacation command has a different :handle.

Managing expiration

One unique feature of ProtonMail is the ability to set an expiration time on sent messages, at which point the message will be deleted from the recipient’s mailbox.

You can also use this feature to manage your incoming messages by adding an expiration time:

require "vnd.proton.expire"; 
# permanently delete all incoming and outgoing emails after 10 days
expire "day" "10";

The script above will delete any received email after 10 days. This one is a bit extreme, since it applies to all emails. Instead, you should probably use a condition to apply the script to only a specific set of emails. For example, your could expire any message from someone who is not in your contacts:

require ["extlists", "vnd.proton.expire"];

# permanently delete after 10 days any email not from me or from someone in my address book.
if not anyof(
    header :list "from" ":addrbook:personal",
    header :list "from" ":addrbook:myself"
) {
 expire "day" "10";
}

List of supported actions and tests

Packages

Sieve supports the following extensions. You can refer to the official documentation for more information.

Date

  • Usage: date
  • Description: Provides a way to check date information.
  • Documentation: https://tools.ietf.org/html/rfc5260
  • Implementation: The default comparator is i;ascii-numeric and not i;ascii-casemap. Indeed, date is not useful with the casemap comparator.
  • See: Vacation messages and Date tests

Envelope

Environment

FileInto

  • Usage: fileinto
  • Description: Applies a label to a message or moves it to a folder.
  • Documentation: https://tools.ietf.org/html/rfc5228#section-4.1
  • Implementation: Slashes indicate the folders full path and they must be escaped if a label name contains a slash. The following options are supported:
    • Work“: this is the label or folder called ‘Work’
    • Work/Project1“: this is the sub-folder ‘Project1’ that is in the folder ‘Work’
    • Work/Project1/Docs“: this is the sub-folder ‘Docs’ that is in the sub-folder ‘Project1’ that is in the folder ‘Work’
    • Work/Misc\\/Others“: this is the sub-folder ‘Misc/Others’ that is in the folder ‘Work’
  • See: Getting started

Imap4flags

Reject

Spamtest

Spamtestplus

Vacation

Variables

Relational

  • Usage: relational
  • Description: Provides relational march operators.
  • Documentation: https://tools.ietf.org/html/rfc5231

Regex

  • Usage: regex
  • Description: Provides the regex match operators.
  • Documentation: https://tools.ietf.org/id/draft-ietf-sieve-regex-01.html

Ascii numeric comparator

Externally Stored Lists

  • Usage: extlists
  • Description: Provides access to contact lists.
  • Documentation: https://tools.ietf.org/html/rfc6134
  • Implementation: The following lists are supported
    • addrbook:personal : Personal contact list. The following queries are supported:
      • label[.starts-with / .ends-with / .contains]=<group: string>: operation against a contact group;
      • keypinning=<value: true / false> contact’s public key definition;
      • encryption=<value: true / false> contact’s default encryption;
      • signing=<value: true / false>  contact’s default signature.
    • :addrbook:myself Addresses belonging to the current user;
    • :addrbook:organization Addresses belonging to the members of the current organization;
    • :incomingdefaults:inbox Whitelist
    • :incomingdefaults:spam Blacklist
  • See: Accessing your contact list

Eval

  • Usage: vnd.proton.eval
  • Description: Evaluates a simple arithmetical function given in a string.
  • Documentation: Transforming variables

Include

Expiration

  • Usage: vnd.proton.expire
  • Description: Manages message expiration.
  • Documentation: Managing expiration

Tests

Currentdate

  • Usage:
currentdate [":zone" <time-zone: string>] [COMPARATOR] [MATCH-TYPE] <date-part: string> <key-list: string-list>

Date

  • Usage:
date [":zone" <time-zone: string> / ":originalzone"] [MATCH-TYPE] <header-name: string> <date-part: string> <key-list: string-list

HasFlag

  • Usage:
hasflag [MATCH-TYPE] [COMPARATOR] <list-of-flags: string-list>
  • Description: Tests if a certain message has a certain flag.
  • Package: imap4flags
  • See: Getting Started

Envelope

  • Usage:
envelope [COMPARATOR] [ADDRESS-PART] [MATCH-TYPE] <envelope-part: string-list> <key-list: string-list>

Address·

  • Usage:
address [COMPARATOR] [ADDRESS-PART] [MATCH-TYPE] <header-list: string-list> <key-list: string-list>

Header

  • Usage:
header [COMPARATOR] [MATCH-TYPE] <header-names: string-list> <key-list: string-list>

HasExpiration

  • Usage:
hasexpiration
  • Description: Tests if a message has an expiration time set.
  • Package: vnd.proton.expire

Exists

  • Usage:
exists <header-names: string-list>

Expiration

  • Usage:
expiration :comparator "i;ascii-numeric" [MATCH-TYPE] <unit: "day" / "minute" / "second"> <key-list: string-list>
  • Description: Compares the message expiration time with the given key(s). The test will fail if it is run against a non-expiring message.
  • Package: vnd.proton.expire

Environment

  • Usage:
environment [COMPARATOR] [MATCH-TYPE] <name: string> <key-list: string-list>

Size

  • Usage:
environment [COMPARATOR] [MATCH-TYPE] <name: string> <key-list: string-list>

SpamTest

  • Usage:
spamtest [":percent"] [COMPARATOR] [MATCH-TYPE] <value: string>
  • Description: Compares the message spam score with the given value. Since both spam score and value should be numbers, the match type should be i;ascii-numeric.
  • Package: spamtest, or spamtestplus when :percent is used.
  • See: Checking if an email is a spam in Sieve

String

  • Usage:
string [MATCH-TYPE] [COMPARATOR] <source: string-list> <key-list: string-list>
  • Description: Evaluates if any of the source strings matches any key.
  • Package: variables

Anyof

  • Usage:
anyof <tests: test-list>
  • Description: Performs a logical OR on the tests supplied to it.
  • Package: <default>
  • See: Getting started

Allof

  • Usage:
allof <tests: test-list>
  • Description: Performs a logical AND on the tests supplied to it.
  • Package: <default>
  • See: Getting started

Not

  • Usage:
not <test: test>
  • Description: Inverts the outcome of the given test.
  • Package: <default>
  • See: Getting started

True

  • Usage:
true
  • Description: Always matches.
  • Package: <default>

False

  • Usage:
false
  • Description: Never matches.
  • Package: <default>

Actions

Require

  • Usage:
require <packages: string-list>
  • Description: Loads a specified extension such that its methods or modifications can be used.
  • Package: <default>
  • See: Getting started

FileInto

  • Usage:
fileinto <folder: string>
  • Description: Moves the message that is being processed to a certain folder.
  • Package: fileinto
  • See: Getting started

Addflag

  • Usage:
addflag <list-of-flags: string-list>
  • Description: Adds the specified flag to the message that is being processed.
  • Package: imap4flags
  • See: Getting started

Removeflag

  • Usage:
removeflag <list-of-flags: string-list>
  • Description: Removes the specified flags from the message that is being processed.
  • Package: imap4flags

Setflag

  • Usage:
setflag <list-of-flags: string-list>
  • Description: Removes all flags and sets the specified flags on the message that is being processed.
  • Package: imap4flags

Stop

  • Usage:
stop
  • Description: Stops processing all Sieve filters. Subsequent Sieve filters will not run.
  • Package: <default>

Return

  • Usage:
return
  • Description: Stops processing the current Sieve filter. Subsequent Sieve filters will still run.
  • Package: include

Set

  • Usage:
set [MODIFIER] <name: string> <value: string>
  • Description: Creates new variables associating the given name and the given value.
  • Package: variables
  • See: Creating variables

Discard

  • Usage:
discard
  • Description: Discards the message at the end of this Sieve filter. After discarding, no other sieve filters will be run.
  • Package: <default>

Keep

  • Usage:
keep
  • Description: Reverts the last discard command. Will only have effect if it is in the same Sieve filter. If no discard has been executed then this will do nothing.
  • Package: <default>

Reject

  • Usage:
reject <reason: string>

Expire

  • Usage:
expire <unit: "day" / "minute" / "second"> <value: string>
  • Description: Expires a message after the given time.
  • Package: vnd.proton.expire
  • See: Managing expiration

Unexpire

  • Usage:
unexpire
  • Description: Removes the expiration time of a message.
  • Package: vnd.proton.expire

Vacation

  • Usage:
vacation [":days" number] [":subject" string] [":mime"] [":handle" string] <reason: string>
  • Description: Sends an auto-response to the SMTP address of the sender with the specified reason as the body.
  • Package: vacation (requires a paid account)
  • See: Vacation messages and Date tests

Valid External List

  • Usage:
valid_ext_list <ext-list-names: string-list>
  • Description: Tests whether all the given external lists are supported and valid.
  • Package: extlists