Skip to content

Examples

Most filters follow a handful of patterns, and a working script is easier to adapt than a blank page. The examples on this page are starting points for common filters, grouped by who installs them.

The first group are system scripts. An administrator installs each one as a SieveSystemScript and selects it from an SMTP stage, as described in Trusted interpreter. The second group are user scripts. A user uploads them over ManageSieve or JMAP, and they run at delivery to that user’s mailbox.

Examples that run in Sievepad are followed by a link that opens them with a matching test message, so each one can be tried before it is installed.

System scripts enforce policy while the message is still being received, so most of them end in a reject. A system script refuses a command or a message with reject. The reject text becomes the SMTP reply only when it starts with a three-digit reply code followed by a space (Replies and actions). Every example below therefore starts the reject text with a full reply code.

Reject clients with a blocklisted EHLO domain

Section titled “Reject clients with a blocklisted EHLO domain”

A client that announces a known bad name in its EHLO command can be turned away before it sends anything. The following script rejects a client whose EHLO domain is listed in the blocked-domains lookup list. Install it as a system script named block-helo, and select it from the script field of MtaStageEhlo:

require ["variables", "extlists", "reject"];
if string :list "${env.helo_domain}" "blocked-domains" {
reject "550 5.7.1 Your domain '${env.helo_domain}' has been blocklisted.";
}

Try this script in Sievepad

The script field then names the script:

{
"script": {"else": "'block-helo'"}
}

The list is a set of MemoryLookupKey entries with the namespace blocked-domains, one per domain. Lists held in an SQL store work differently. They cannot be tested with :list, so a script that needs them calls the query function.

Require the From header to match the sender of authenticated mail

Section titled “Require the From header to match the sender of authenticated mail”

An authenticated user could otherwise send mail whose visible sender is someone else. The mustMatchSender check compares the envelope sender of an authenticated submission with the addresses of the account. It does not look at the From header, which is what recipients see.

The following script closes that gap at the DATA stage. It requires the From header of authenticated submissions to carry the envelope sender, which mustMatchSender has already verified. Install it as a system script named from-header, and select it from the script field of MtaStageData:

require ["variables", "envelope", "reject"];
if not string :is "${env.authenticated_as}" "" {
if not address :all :comparator "i;unicode-casemap" :is "from" "${envelope.from}" {
reject "550 5.7.1 The From header must match the sender address.";
}
}

Try this script in Sievepad

The script tests env.authenticated_as first, so unauthenticated messages are not affected. Messages that list several addresses in the From header pass when one of them is the envelope sender. Addresses, aliases and catch-all describes the addresses an account may send from.

Reject messages with an executable attachment

Section titled “Reject messages with an executable attachment”

The spam filter treats dangerous attachments as evidence: it adds them to the score of a message. Some sites prefer a hard rule instead. The following script, run at the DATA stage, rejects messages that carry an attachment whose name ends in .exe, .scr or .js, which refuses the message outright whatever its score:

require ["mime", "foreverypart", "reject"];
foreverypart {
if header :mime :param "filename" :matches "Content-Disposition" ["*.exe", "*.scr", "*.js"] {
reject "550 5.7.1 Executable attachments are not accepted.";
stop;
}
}

Try this script in Sievepad

Two common tasks need no example here. Adding a disclaimer, or otherwise changing the content of outgoing messages, is described in Content rewriting. Greylisting needs no script, because it is built into the SMTP server, as described in Greylisting.

User scripts organise mail rather than refuse it. They run once for each message delivered to the user, after the spam filter has scored it. A script that ends without filing the message keeps it in the inbox, or in the Junk folder when the spam filter classified it as spam.

An out-of-office reply is the most common user script. The following script answers each sender at most once every seven days while the user is away:

require ["vacation"];
vacation :days 7 :subject "Out of office"
"I am out of the office until 3 November and will reply when I return.";

Try this script in Sievepad

The reply is sent with an empty envelope sender. It is not sent to mailing lists, or in answer to messages that are themselves automatic replies, which prevents loops between two absent users. JMAP clients usually configure the same reply through the vacation response, described in Vacation responses.

Mailing list traffic is easier to read when each list has its own folder. The following script files messages from mailing lists into a folder named after the list, creating the folder when it does not exist:

require ["fileinto", "mailbox", "variables"];
if header :matches "List-Id" "*<*.*>*" {
fileinto :create "Lists/${2}";
}

Try this script in Sievepad

For the header List-Id: Release announcements <releases.lists.example.com>, the message is filed into Lists/releases.

Some users prefer to review likely spam separately from the Junk folder. The following script files messages that the spam filter considers likely spam into a Spam review folder:

require ["fileinto", "mailbox", "spamtest", "relational", "comparator-i;ascii-numeric"];
if spamtest :value "ge" :comparator "i;ascii-numeric" "5" {
fileinto :create "Spam review";
}

Try this script in Sievepad

The spamtest value is a number from 1 (not spam) to 10 (spam), and 5 corresponds to the spam threshold of the spam filter. A message filed by the script stays where the script put it, and is not moved to the Junk folder.

Forwarding sends mail on to another mailbox, for example a personal address. The following script forwards a copy of every message to another address, and keeps the original in the mailbox:

require ["copy"];
redirect :copy "jane.doe@example.net";

Try this script in Sievepad

The :copy argument makes the difference between forwarding a copy and moving the message. Without it, the message is forwarded and not stored. The number of forwards per message is limited by maxRedirects, 1 by default.

Forwarding has a known weakness with sender authentication. Stalwart sends the forwarded copy with the address of the account running the script as its envelope sender, and by default signs it with DKIM for that account’s domain (dkimSignDomain). The From header stays unchanged. The receiving server therefore checks SPF against the account’s domain, and delivery status notifications return to the account, not to the original sender. DMARC needs SPF or DKIM to align with the From domain, which neither of these does when the original sender is on another domain, so the copy passes DMARC only through the original sender’s DKIM signature. When the sender’s domain enforces DMARC, and the message carries no valid aligned DKIM signature, the receiving server may reject the copy.

Many users give out addresses with a sub-address, such as jane+shop@example.org, to see who uses which address and to sort the replies. The following script files messages sent to such an address into a folder named after the sub-address:

require ["envelope", "envelope-dsn", "fileinto", "mailbox", "variables", "subaddress"];
if envelope :detail :matches "orcpt" "*" {
if not string :is "${1}" "" {
fileinto :create "${1}";
}
}

The script does not test the to envelope part, because Stalwart removes the sub-address from the recipient when it accepts the message, as described in Addresses, aliases and catch-all. Stalwart keeps the address as the sender wrote it, in lower case, as the original recipient of the delivery, and a user script reads it through the orcpt envelope part, which requires the envelope-dsn extension. The value carries an address type prefix (rfc822;jane+shop@example.org), and the :detail argument extracts the sub-address after that prefix. When a message reaches the user through a mailing list, the original recipient is the list address, so the script files it by the sub-address the sender added to the list address.

Testing the envelope rather than the To and Cc headers matters when the address does not appear in the headers at all, as with a blind copy (Bcc) or a message that reaches the user through a mailing list. When the recipient address was accepted unchanged, the delivery has no original recipient (unless the sending server supplied one), and the test does not match.