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 type | Meaning |
|---|---|
:is | The value is exactly this |
:contains | The value contains this as a substring |
:matches | Wildcard match - * for any run of characters, ? for one |
:value, :count | Numeric 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
header
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.
| Test | True when |
|---|---|
attachment | The message has an attachment |
mime_header | A header of any MIME part matches - unlike header, which only sees the top level |
me | A named header contains one of the account's own addresses |
addressbook | The address in a header is in the user's contacts |
contact_ranking | The sender is someone the user has written to before |
list | The message has a List-Id or X-Zimbra-DL header |
bulk | Bulk mail, detected from List-Unsubscribe, Precedence and similar |
importance | The Importance or X-Priority header is high, normal or low |
flagged | The message carries a flag, including one set earlier in the same script |
conversation | The thread was started by, or replied to by, the user |
invite | The message is a calendar invitation, optionally of a given :method |
current_time | Delivery time, as HHmm in the user's time zone, :before or :after |
current_day_of_week | Day 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, notheader, 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,bulkorattachmentover 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.