Skip to main content

Sieve Conditions in Zimbra: What a Filter Can Test

A Sieve rule only acts when its condition matches. Zimbra gives you the standard tests from RFC 5228 - headers, addresses, size - and then adds around twenty of its own that reach into things no RFC covers: whether the message has an attachment, whether the sender is in your contacts, whether it arrived outside working hours. Most of them are invisible in the web client and only available if you write the script yourself.

For what a rule does once it matches, see Sieve Actions in Zimbra. For complete scripts you can copy, see 10 Zimbra Sieve Filter Examples.

Match types

Almost every test takes a match type that says how the comparison is made. Getting this right matters more than the choice of test.

Match typeMeaning
:isThe value is exactly this
:containsThe value contains this as a substring
:matchesWildcard match - * for any run of characters, ? for one
:value, :countNumeric comparison, needs the relational extension

:contains is the safe default for subjects and :is for headers with a fixed value like X-Spam-Flag. Reach for :matches when you need a wildcard, and remember that it anchors to the whole value, so matching a domain needs a leading *.

Any test can be given a list instead of a single string, and it matches when any entry in the list matches:

if header :contains "subject" ["invoice", "receipt", "statement"] {
fileinto "Invoices";
}

Combining tests

allof is AND, anyof is OR, and not negates. They nest, so there is no condition you cannot express, only ones that get hard to read.

require ["fileinto"];

if allof (address :domain :is "from" "example.com",
header :contains "subject" "invoice") {
fileinto "Invoices";
}

if anyof (header :contains "subject" "urgent",
header :is "X-Priority" "1") {
fileinto "Urgent";
}

if not address :domain :is "from" "example.com" {
fileinto "External";
}

true and false are tests in their own right, and both are useful while you are building a script - if false parks a rule without deleting it, and if true proves the actions work before you write the real condition.

The standard tests

The workhorse. It reads a message header, and it accepts several header names at once.

if header :is "X-Spam-Flag" "YES" {
fileinto "Junk";
}

if header :matches ["to", "cc"] "*@partner.example.com" {
fileinto "Partners";
}

address

Like header, but it parses the header as an email address first, so display names and angle brackets do not get in the way. :all is the whole address and the default, :localpart is the part before the @ and :domain the part after it.

if address :domain :is "from" "example.com" {
fileinto "Example";
}

if address :localpart :is "from" "noreply" {
fileinto "Notifications";
}

Prefer address over header whenever you are matching a sender or recipient. A header :contains "from" "example.com" also matches a display name that merely mentions it, which is exactly the hole a spoofed sender walks through.

envelope

Tests the SMTP envelope - the MAIL FROM and RCPT TO of the actual delivery - rather than the headers, which the sender writes and can therefore forge.

require ["envelope", "fileinto"];

if envelope :domain :is "from" "example.com" {
fileinto "Example";
}

if envelope :is "to" "sales@example.com" {
fileinto "Sales";
}

The envelope recipient is also the only reliable way to tell why a message reached you when it was sent to a distribution list, since the To header will name the list, not you.

exists and size

if exists "List-Unsubscribe" {
fileinto "Newsletters";
}

if not exists ["From", "Date"] {
fileinto "Junk";
}

if size :over 10M {
fileinto "Large";
}

exists is true only when every header named is present. size takes :over or :under with a K, M or G suffix.

body

require ["body", "fileinto"];

if body :contains "unsubscribe" {
fileinto "Newsletters";
}

Zimbra supports only :contains here. The :matches, :is, :raw and :text variants in RFC 5173 are not implemented, so anything more precise has to be done on the headers.

relational

:value compares numbers and :count counts how many values a header has. Both need the relational extension and the i;ascii-numeric comparator, and the operators are the two-letter forms gt, ge, lt, le, eq and ne.

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

if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-Score" "5" {
fileinto "Junk";
}

if address :count "ge" :comparator "i;ascii-numeric" ["to", "cc"] "20" {
fileinto "Mass mail";
}

This is how you read a spam score rather than just a yes/no spam flag, and it is worth the verbosity.

date

Zimbra's date is not the RFC 5260 test of the same name. It compares the Date header against a date in yyyyMMdd form, and supports only :before and :after.

if date :before "20260101" {
fileinto "Old";
}

string

Compares a variable rather than a header, which is what makes variables useful.

require ["variables", "fileinto"];

if header :matches "subject" "*" {
set "subject" "${1}";
}

if string :contains "${subject}" "invoice" {
fileinto "Invoices";
}

Zimbra's own tests

These have no RFC behind them. They are the reason a hand-written Zimbra script can do things the web client cannot express.

TestTrue when
attachmentThe message has an attachment
mime_headerA header of any MIME part matches - unlike header, which only sees the top level
meA named header contains one of the account's own addresses
addressbookThe address in a header is in the user's contacts
contact_rankingThe sender is someone the user has written to before
listThe message has a List-Id or X-Zimbra-DL header
bulkBulk mail, detected from List-Unsubscribe, Precedence and similar
importanceThe Importance or X-Priority header is high, normal or low
flaggedThe message carries a flag, including one set earlier in the same script
conversationThe thread was started by, or replied to by, the user
inviteThe message is a calendar invitation, optionally of a given :method
current_timeDelivery time, as HHmm in the user's time zone, :before or :after
current_day_of_weekDay of delivery, 0 Sunday to 6 Saturday, :is only

A few that earn their place in everyday scripts:

require ["fileinto"];

# Anything with an attachment
if attachment {
fileinto "Attachments";
}

# Mail you are not actually addressed on - you were Bcc'd or on a list
if not me :in "to,cc" {
fileinto "Bcc";
}

# Newsletters and announcements, without naming a single sender
if bulk {
fileinto "Bulk";
}

# A sender you have never heard of
if not addressbook :in "from" {
fileinto "Unknown senders";
}

mime_header is the one to know about for attachments, because it sees the headers of every part and so can match a content type:

if mime_header :contains "Content-Type" "application/pdf" {
fileinto "PDF";
}

And the two time tests, which are how you separate work from everything else:

require ["tag"];

if anyof (current_time :before "0800",
current_time :after "1800",
current_day_of_week :is ["0", "6"]) {
tag "After hours";
}

Zimbra also carries a set of legacy social-network tests - facebook, linkedin, twitter, socialcast and three community_* tests. They match on hardcoded sender domains and exist for products that no longer do, so treat them as historical.

Choosing the right test

Most filters that misbehave do so because of the test, not the action. Three rules of thumb cover most of it:

  • Match senders with address, not header, so a display name cannot fool the rule.
  • Match on the envelope when the decision has to survive a forged header.
  • Prefer a structural test such as list, bulk or attachment over a keyword search - it keeps working when the wording changes.

Next

For what happens once a condition matches, read Sieve Actions in Zimbra. For where Zimbra stores these scripts and how to apply one to a whole domain, read Zimbra Sieve Filters: An Introduction.