Mail Filter deprecated
Overview
OX App Suite filters incoming mail server-side via Sieve (RFC 5228). The middleware does not take part in mail delivery itself: it writes and reads the user's Sieve script through the ManageSieve protocol (RFC 5804), while the script is executed by the Sieve server of the mail backend (typically Dovecot with Pigeonhole) on every delivery.
Clients use the HTTP API mailfilter/v2 (the older mailfilter v1 interface still exists, but new functionality appears in v2 only). Every rule created through the API is stored as one block in the user's Sieve script, annotated with a comment carrying the rule name, a unique id and flags. The rules remain ordinary Sieve: an administrator can inspect the script at any time via ManageSieve or action=getscript.
Connection settings, timeouts, TLS and authentication against the Sieve server are configured via the com.openexchange.mail.filter.* properties (see the property documentation); this article covers the functional properties of the rules, not the connection configuration.
Filter rules
A rule consists of a condition (tests on headers, sender, recipient, size, date and so on, combinable with allof/anyof/not) and a list of actions (file into a folder, redirect, set flags, discard, vacation notice, ...). Rules execute in script order; the API supports reordering, activating/deactivating (a deactivated rule stays in the script, commented out) and applying a rule to an existing folder after the fact (apply; individual actions can be blocked for that via com.openexchange.mail.filter.options.apply.blockedActions, default: redirect).
Redirect targets can be limited host-side via com.openexchange.mail.filter.redirectWhitelist. Which tests and actions a client may offer is announced dynamically from the capabilities of the Sieve server (the API's config action): what the server cannot do is not offered, and rules requiring capabilities the server does not announce are rejected.
Individual test values can be blocked host-side; see Mail Filter Blacklisting.
Hand-written scripts
Sieve scripts may be maintained outside of App Suite (ManageSieve). The middleware reads foreign constructs tolerantly: what it does not understand is presented as an inert, non-editable rule and preserved verbatim on write-back. As of 8.54, if/elsif/else chains also parse into ONE multi-branch rule; the API shows such rules but rejects updates to them (error MAIL_FILTER-0041 or 0042, see the API behavior section below) — the API can only represent the leading branch, so saving would silently damage the other branches. The v2 list action announces this up front: such rules are reported with an errormsg naming the refusal, while the leading branch's condition and actions stay visible, so clients can render them read-only instead of failing at save time. Authors of hand-written scripts should also know: when the middleware rewrites a multi-branch rule it manages itself (such as the internal/external vacation notice below), it regenerates the branches in canonical formatting — custom comments and formatting inside them are lost.
Vacation notice
The vacation notice is a vacation rule per RFC 5230: the Sieve server automatically answers incoming mail with a configured text, optionally limited to a date range. The server deduplicates the replies per sender — standard interval :days, optionally seconds-based:
com.openexchange.mail.filter.options.vacation.minimumInterval.seconds(default-1= off): enables seconds-based intervals (:seconds, RFC 6131) and at the same time sets the minimum the middleware enforces on write. Only takes effect if the Sieve server announces thevacation-secondscapability.- The subject of the reply is optional: without a subject, the Sieve server generates the default per RFC 5230 (on Dovecot
Auto: <original subject>). - The sender address (
from) of the notice is validated against the addresses of the rule's owner (primary address and aliases); foreign senders are rejected. - Subject fields must not contain line breaks (error MAIL_FILTER-0044): a break would otherwise end up verbatim in the
Subject:header of the reply — a header-injection vector.
Two host-side restrictions apply to every vacation notice:
com.openexchange.mail.filter.vacationDomains(allowlist): when set, the notice only answers senders from the listed domains. The middleware injects the test into the rule on write and strips it again on read — invisible to clients. The value shapes newly written rules only; existing rules adopt a changed list on their next change.com.openexchange.mail.filter.vacationRestrictToAddresses(defaulttrue): the notice only answers mail that was delivered to one of the addresses it has been enabled for. The middleware wraps thevacationaction into anenvelopetest for that (local part and domain compared separately, so sub-addresses likeuser+detail@example.comstill match). Without the restriction, the:addressesargument alone decides — per RFC 5230 that argument only ADDS addresses, so the notice would also answer mail delivered to addresses the user did not select. Requires the server capabilityenvelope; without thesubaddresscapability the whole address is compared instead (sub-addresses then do not match, which is logged at warning level).
Both tests are invisible on the API read path, but visible in the raw script (action=getscript).
Internal/external vacation notice (as of 8.54)
As of 8.54 a vacation notice can answer internal senders differently from external ones — with a separate text and subject for external senders, or not at all for external senders. Three modes, selected explicitly by the vacationMode field of the vacation action command of the v2 API:
| Mode | Meaning | vacationMode value |
|---|---|---|
| 1 | One reply for everybody (classic) | all |
| 2 | Internal and external replies differ | split (+ textExt, opt. subjectExt) |
| 3 | Only internal senders receive a reply | internalOnly |
split without textExt is invalid, as are all/internalOnly combined with the ext fields, an unknown mode value (an empty value and an explicit JSON null included — the server never defaults the mode), and ext fields without vacationMode (error MAIL_FILTER-0044 with a cause-specific message). An empty subjectExt counts as not set: external senders then receive the server-generated default subject per RFC 5230 (on Dovecot Auto: <original subject>, see "subject" above).
Classification: the stamp header
Whether a sender is internal or external is decided not by the rule, but by the mail platform at delivery time: it stamps every delivered message with a classifier header whose name the hoster configures:
com.openexchange.mail.filter.options.vacation.internal.externalTagHeader— the header name, e.g.Vacation-External. The values are fixed convention:true= external,false= internal. The stamping logic must also delete inbound instances of the header before stamping — the complete contract including a recipe is in the SECURITY section below.
The generated rules only ask "is this message stamped internal or external?". The decisive benefit over domain lists baked into rules: the actual policy — domain lists, LDAP groups, subsidiaries, newly acquired domains — lives centrally in the operator's stamping logic. When the operator changes it, the change takes effect immediately for all existing vacation notices, without any user having to touch a rule or the middleware having to rewrite a script. Baked-in lists would instead freeze the policy of the day each rule was last saved.
A third sender class as a feature: messages deliberately NOT stamped at all (neither true nor false) receive no reply from any rule. This suppresses vacation notices for whole sender classes centrally — newsletters, monitoring, no-reply senders — again without touching any user's rules. Important: the delete step of the SECURITY section remains mandatory for unstamped messages too ("selective stamping is safe, selective deleting is not").
Availability
com.openexchange.mail.filter.options.vacation.internal.enabled(defaultfalse): the feature toggle. While off, requests introducing the new fields are rejected (MAIL_FILTER-0043) and the feature is announced as unavailable.- Additional prerequisite: the configured classifier header (above). Without it the feature stays unavailable even with the toggle switched on. Invalid names (anything but letters, digits and hyphens) count as unset and are logged.
- Clients learn the combined availability via the config action (
vacationInternalAvailable). Careful — an operator's promise: what is announced is CONFIGURATION, not verified end-to-end function. The middleware cannot check whether the platform really stamps — the toggle is the operator's commitment that it does (verification: see the runbook below).
Switching the toggle off never orphans existing extended rules: they keep working, keep being reported with their fields, and stay editable — their extended modes stay accepted on rules that already carry the structure. A rule degrades to a classic notice only through an explicit vacationMode: "all" save (or an update replacing its actions without any vacation command). Clients supporting the fields send the mode on every vacation write, also while the feature is unavailable — but for an existing extended rule they echo the mode they read, and offer the downgrade to "all" only as an explicit user choice with a warning: availability can also flip temporarily (a misconfigured classifier header, a cascade override), and a routine edit — a date change, a text tweak — must not sunset the rule for good. A client without the fields cannot degrade the rule through an ordinary edit: its saves preserve the extended structure.
Interplay with the restrictions above
vacationDomains and vacationRestrictToAddresses cover the extended rules as well, on every branch (internal and external alike): a branch whose test fails does not suppress the notice but hands the message to the sibling branch — a restriction sitting on the first branch alone would not restrict, it would reroute to the wrong reply variant.
The generated structure (for orientation, not an API contract)
if allof (<the user's condition>, [<vacationDomains test>,]
allof (header :is "Vacation-External" "false",
not header :is "Vacation-External" "true"))
{ vacation ... "<internal text>"; }
elsif allof (<the user's condition>, [<vacationDomains test>,]
allof (header :is "Vacation-External" "true",
not header :is "Vacation-External" "false"))
{ vacation ... "<external text>"; }
Each branch requires its own value AND the absence of the opposite value. In mode 3 the second branch is an empty else { }. The shape is an internal wire format of the middleware — clients work exclusively with the API fields.
SECURITY: the stamping contract
The trust boundary of the feature lies in the stamping logic of the mail platform. The contract:
- Every delivered message passes through the stamping logic, typically a Dovecot
sieve_beforescript. The delivery step is deliberately the right place:sieve_beforeruns per recipient copy — a message sent to several recipients is classified for each recipient individually. A stamp applied earlier (MTA) would apply once for all recipients and could not express recipient-dependent policy (multi-tenant: whether a sender is "internal" can depend on the recipient). - Delete first: every inbound instance of the header is removed — RFC 5293
deleteheader "<name>"removes all occurrences. The delete step covers every message without exception, including those that deliberately stay unstamped: an untouched message could carry a smuggled<name>: falsethat wins the internal reply precisely because no genuine stamp contradicts it. Selective stamping is safe, selective deleting is not. - Then stamp:
addheader "<name>" "true"(external) or"false"(internal) — or deliberately nothing (the third class above).
Example (sieve_before; the classification here is by sender domain merely as an illustration):
require ["editheader", "envelope"];
deleteheader "Vacation-External";
if address :is :domain "from" ["example.com", "example.de"] {
addheader "Vacation-External" "false";
} else {
addheader "Vacation-External" "true";
}
The header must not be listed in Dovecot's sieve_editheader_forbid_add / sieve_editheader_forbid_delete settings.
The classification logic itself is free — the example above is deliberately minimal. Since the script runs per recipient, it can also decide differently per user: Dovecot can, for instance, load attributes for the recipient from LDAP via userdb and expose them to the script (the Pigeonhole variables/environment machinery). The list of "internal" domains — or the whole classification rule — can this way be maintained per user or tenant in LDAP; changes there take effect immediately, again without touching any middleware configuration or user rule.
Fail-closed as the safety net: a message carrying neither of the two values — or both (a smuggled instance next to the genuine stamp) — matches no branch and no notice goes out. Broken or missing stamping therefore never leaks the internal text to external senders; it mutes the notice. This hardening is defense in depth, not a replacement for the delete step: on a delivery path that does not stamp at all, a smuggled <name>: false would be indistinguishable from a genuine one.
Operations (runbook)
Enablement rule: activate the feature only once (a) every node in the cluster runs 8.54 or later — the property is reloadable and would otherwise take effect on older nodes, which edit extended rules with the hazards described below — and (b) a rollback below 8.54 is no longer expected.
Pre-flight verification (before switching the toggle on): from an external sender, inject a message with a forged header (<name>: false) and verify it arrives at the recipient with <name>: true — that proves deleting AND stamping on this delivery path. Repeat the test for every delivery path (MX, internal delivery, forwards, mailing list expansion).
Symptom signature for support: "an active vacation notice answers nobody" = stamping broken, a delivery path without stamping logic, or the header was renamed (next item). There is no error and no middleware log entry for this — fail-closed is silent.
The header name is effectively immutable once extended rules exist: existing rules test the name they were written with, until their next change. After a rename nothing stamps the old name any more — those rules answer nobody, without any error being reported. Keep stamping the old name alongside the new one during a transition period, or never rename.
Downgrade behavior (middleware < 8.54): an older middleware reads an extended rule as an ordinary vacation rule (the internal branch; its classifier condition shows as a plain condition) plus a separate, non-editable "unsupported" rule preserving the remaining branches verbatim. Delivery keeps working unchanged throughout, including the internal/external distinction — the branches stay in place in the script, and the Sieve server ignores the metadata comment described next. Any script write from the old node — saving, reordering or deleting ANY rule, not only vacation notices — stamps a metadata line of its own above the separated branch. That line splits the pair in the middleware's rule model, not in the script's delivery semantics; an 8.54 middleware skips such metadata lines when it reattaches the branches, so the pair heals itself on the next read after the upgrade. The hazard is editing the vacation notice under the old version: it rewrites only the internal branch, while the external branch survives verbatim behind it and chains to the rewritten condition (the external reply can silently stop or start firing); reordering filter rules can separate the pair further, which the Sieve server then rejects as an invalid script. If a downgrade is unavoidable: keep the window short and avoid editing vacation notices during it; to remove the exposure beforehand, have affected users disable the internal/external option of their notice (saving it with vacationMode: "all" degrades the rule) or strip the branches via ManageSieve.
Support signals of the mail filter v2 interface: a save refused with MAIL_FILTER-0042 for a multi-branch rule that carries a vacation action logs a WARN from com.openexchange.mail.filter.json.v2.json.RuleParser (visible at default log levels) — the signature of a vacation notice whose branches no longer match the middleware-written shape, e.g. after hand-editing via ManageSieve. The same rule already carries an errormsg in the list response — the client-side trace of the same situation. A reported loss of the internal/external distinction can only result from a client's own save — either an explicit vacationMode: "all", or an update replacing the rule's actions without any vacation command (a client without the fields cannot strip the structure through an ordinary edit any more — the server preserves it); check the client in question rather than the middleware.
Enumerating affected scripts (e.g. before a header rename or a planned downgrade): extended rules are recognizable in the Sieve storage by their classifier header. Search the mail backend's Sieve storage for the configured header name, e.g. grep -rl 'header :is "Vacation-External"' <sieve directory>, or per user via doveadm sieve get -u <user> <script>. The hits are the scripts whose rules need consideration for the task at hand.
Requirements towards the Sieve server
The middleware verifies the announced Sieve capabilities (vacation, date, relational, vacation-seconds, envelope, subaddress, ...) and only offers what the server can do. What it cannot verify is that the server implements the RFC semantics correctly. The features of this article rely in particular on the server handling the implicit keep in conformance with RFC 5228 (the empty else branch of an internal-only notice must not defeat it) and on generating the default subject per RFC 5230. The stamping logic of the SECURITY section additionally requires editheader (RFC 5293) on the delivery path.
HTTP API behavior (for client developers and support)
- Mode signals on read: every vacation rule reports its
vacationModeexplicitly —allon classic notices too, old rules included. Sole exception: multi-branch rules the middleware does not recognize as extended vacation rules (hand-written; updates are refused withMAIL_FILTER-0042) report their vacation action without a mode and carry anerrormsginstead, announcing that they cannot be edited through this interface — the rule itself, with the leading branch's condition and actions, stays listed. The_Extfields appear exactly when they are set; empty strings are normalized to "not set" on write and never round-trip. This differs from the legacy fieldssubject/text, which persist and return empty values (textis Sieve's mandatory reason argument,subjectis written on mere JSON presence) — clients must not treat the new fields the same way. - Updates replace the actions completely, the mode transition is explicit: clients supporting the fields MUST send
vacationModeon every vacation write — also while the feature is unavailable."all"is always accepted, and on an existing extended rule the read mode stays accepted too: clients echo the mode they read and offer the downgrade to"all"only as an explicit user choice with a warning — availability can flip on a configuration mistake, and a routine edit must not downgrade the rule for good. An update whose vacation action carries NOvacationModecomes from a field-unaware client and preserves an existing extended structure instead of replacing it. An update without anyactioncmdsleaves the extended fields in force either way. - Error codes:
MAIL_FILTER-0041— multi-branch rule without a vacation action, not changeable through this interface (hand-written; "it can only be changed where it was created").MAIL_FILTER-0042— vacation notice not changeable through this interface. From v1 for every extended vacation notice (they stay fully editable through v2); from v2 for a multi-branch rule carrying a vacation action that the middleware does not recognize as its own shape (hand-edited, or split by a pre-8.54 node). The display messages differ: v1 points the user to an up-to-date client (v2 can edit those rules); v2 says the notice was changed outside the application and can only be deleted here.MAIL_FILTER-0043— feature not available for this account (toggle off or header not configured; the distinction is in the server log, not in the user-facing message).MAIL_FILTER-0044— invalid vacation fields (cause-specific technical messages:all/internalOnlycombined with the ext fields,splitwithouttextExt, an unknownvacationModevalue, ext fields withoutvacationMode, line break in a subject).MAIL_FILTER-0045— more than onevacationaction command in a single rule (a rule holds at most one vacation notice).
Further references
- Properties in detail: the
com.openexchange.mail.filter.*entries of the middleware configuration documentation - Blocking test values: Mail Filter Blacklisting
- Release communication: the 8.54 entries in Detailed Software Changes