Documentation
Mail rules reference
rules.toml lives at the root of the mail store and decides what happens to
every message that passes through Everlock — inbound or outbound. Each rule
names the messages it selects under match and the action that handles them.
Default rules
Built in
A message that matches no rule is stored in the recipient's mailbox, in the
inbox folder. This is built in and needs no rule.
Recommended for every setup
Spam filters mark the mail they judge as spam with a header: web.de and GMX
set X-Spam-Flag: YES, SpamAssassin sets X-Spam-Flag and
X-Spam-Status: Yes, score=…, rspamd sets X-Spam: yes. Mail that reaches
Everlock through such a filter — forwarded from a web.de or GMX account, or
relayed by a filtering gateway — carries that verdict. This rule files it
into junk:
[[rule]]
id = "rule-spam"
priority = 10
type = "put_in_folder"
folder = "junk"
match.any = [
{ header = "X-Spam-Flag", is = "yes" },
{ header = "X-Spam", is = "yes" },
{ header = "X-Spam-Status", matches = "^yes," },
]
Trusting these headers is safe in one direction: a sender can mark its own
mail as spam, never as wanted, so the rule can only move mail into junk.
The same rule from the admin shell:
/mail rules create rule-spam direction=inbound priority=10 type=put_in_folder folder=junk match={ any = [{ header = "X-Spam-Flag", is = "yes" }, { header = "X-Spam", is = "yes" }, { header = "X-Spam-Status", matches = "^yes," }] }
Every message stored into junk carries the IMAP keyword $Junk, whichever
action put it there, so mail clients show it as spam.
Exceptions
Rules run in ascending priority, and the first rule that matches decides.
An exception is a rule with a lower priority number than the rule it
overrides. Here mail from test@example.com stays in the inbox even when a
filter marked it as spam, and mail from prod@example.com always goes to
junk:
[[rule]]
id = "rule-test-sender-inbox"
priority = 1
type = "store"
match = { envelope = "sender", is = "test@example.com" }
[[rule]]
id = "rule-prod-sender-junk"
priority = 5
type = "put_in_folder"
folder = "junk"
match = { envelope = "sender", is = "prod@example.com" }
How rules run
Rules are evaluated in ascending priority order. The first rule that matches
wins.
- lower priority numbers run first
- omitted
prioritydefaults to10 - equal priorities keep file order
priority = 0is valid
Rules have a direction — inbound or outbound. Inbound rules apply to mail
arriving over SMTP. Outbound rules apply to mail sent through the submission
listener. A rule runs for its own direction only.
Rule shape
Each rule is one [[rule]] TOML block. Its identity, order, direction and
action are plain keys; every condition sits under match:
[[rule]]
id = "rule-alerts"
name = "alerts to their own folder"
priority = 5
direction = "inbound"
type = "put_in_folder"
folder = "alerts"
match = { envelope = "receiver", is = "alerts@example.com" }
| Field | Default | Description |
|---|---|---|
id | — | Required. Unique rule identifier, starting with rule-. |
name | — | Human-readable label for your own reference. |
priority | 10 | Evaluation order. Lower numbers run first. |
direction | inbound | inbound or outbound. |
type | — | Required. The action, with its own fields. |
match | every message | The conditions a message meets for the rule to apply. |
A rule without match applies to every message of its direction.
The match language
match holds one node. A node is either a combinator — any, all, not —
or a condition. Conditions follow the header, address and envelope tests of
Sieve (RFC 5228), each written with one key.
Conditions
A condition has exactly one source, an optional part, and exactly one test:
{ header = "From", part = "domain", is = "example.com" }
# source part test
Sources
| Source | What it tests |
|---|---|
header = "<Name>" | Any header field. The name compares without regard to case. The value is unfolded and its RFC 2047 encoded words are decoded, so =?utf-8?Q?Gr=C3=BC=C3=9Fe?= is tested as Grüße. A field that occurs several times matches when any occurrence matches. |
envelope = "sender" | The SMTP MAIL FROM address. It is empty for bounces. |
envelope = "receiver" | The RCPT TO address of this delivery. |
auth = "spf", "dkim", "dmarc" | The inbound verdict keyword: pass, fail, softfail, none, neutral, temperror or permerror. |
message = "size" | The raw message size in bytes. |
Parts
part splits addresses and tests each one; the condition matches when any
address matches.
| Part | Tests |
|---|---|
address | the whole address, local@domain |
localpart | the part before the last @ |
domain | the part after the last @ |
name | the display name, decoded and unquoted |
part applies to the address headers — From, To, Cc, Bcc,
Reply-To, Sender, Resent-From, Resent-To, Resent-Cc,
Resent-Sender — and to envelope, where address, localpart and
domain apply. Without part, a header is tested as text.
part keeps the display name apart from the address. A message with
From: "service@paypal.com" <evil@attacker.example> has the domain
attacker.example, so this condition leaves it alone:
{ header = "From", part = "domain", is = "paypal.com" }
Tests
| Test | Sources | Matches when |
|---|---|---|
is | header, envelope, auth | the value equals the given text |
contains | header, envelope, auth | the value contains the given text |
matches | header, envelope, auth | the regular expression finds a match in the value |
exists = true / false | header, envelope, auth | the header is present / the envelope sender is set / a verdict is present — or, with false, the opposite |
gt | message = "size" | the size in bytes is greater than the number |
lt | message = "size" | the size in bytes is less than the number |
is, contains and matches take a string or a list of strings; the test
matches when any of them does:
{ header = "Subject", contains = ["invoice", "Rechnung"] }
All comparisons ignore case.
matches uses the Rust regex syntax
and searches anywhere in the value, like Sieve's :regex; anchor with ^
and $ to test the whole value. A TOML literal string ('…') keeps
backslashes as written: matches = '^\[build\]'. Patterns compile when the
rules load.
Combining conditions
| Node | Matches when |
|---|---|
{ any = [node, …] } | at least one node matches |
{ all = [node, …] } | every node matches |
{ not = node } | the node does not match |
Nodes nest:
[[rule]]
id = "rule-unsigned-invoices"
priority = 3
type = "put_in_folder"
folder = "junk"
match.all = [
{ auth = "dmarc", is = "fail" },
{ header = "Subject", contains = "invoice" },
{ not = { header = "From", part = "domain", is = "example.com" } },
]
TOML forms
These three spellings are the same rule; use whichever reads best:
match.any = [
{ header = "X-Spam-Flag", is = "yes" },
{ header = "X-Spam", is = "yes" },
]
[rule.match]
any = [
{ header = "X-Spam-Flag", is = "yes" },
{ header = "X-Spam", is = "yes" },
]
match = { any = [{ header = "X-Spam-Flag", is = "yes" }, { header = "X-Spam", is = "yes" }] }
More conditions
{ header = "From", contains = "ADAC" }
{ envelope = "sender", is = "prod@example.com" }
{ envelope = "receiver", part = "domain", is = "example.com" }
{ auth = "dmarc", is = "fail" }
{ message = "size", gt = 26214400 }
{ header = "X-Mailer", exists = true }
{ not = { header = "Subject", contains = "release" } }
Rules that are skipped
Each rule loads on its own. A rule whose keys or match do not validate is
skipped, the log names it with the reason, and the rest of the file applies.
/mail rules list and /mail rules show report it as skipped with the same
reason.
Conditions live under match only. A rule that carries a condition field
next to its action — sender, receiver, domain, local_part,
subject_contains, message_id, header_from_contains,
header_to_contains, size_gt, size_lt, has_header, spf_result,
dkim_result, dmarc_result — is skipped as a whole, for example
skipped: uses subject_contains, write it under match. With the condition
written under match, the rule applies.
When rules.toml as a whole fails to parse as TOML, inbound delivery answers
with a temporary failure (4xx) until the file parses again; senders retry.
Actions
The action is written inline using the type key plus any action-specific
fields.
allow
Permit the message and stop evaluating further rules. The message is handled
as with no rule — stored in the recipient's inbox for inbound, sent normally
for outbound. Useful as a priority-1 allowlist entry before a catch-all
reject with a higher priority number.
type = "allow"
reject
Reject the message with an SMTP error. The sender receives a permanent failure.
type = "reject"
drop
Accept the message and discard it silently. The sender sees a success reply.
type = "drop"
store
Store in the recipient mailbox. folder defaults to inbox.
type = "store"
folder = "inbox"
store_as
Store in a different mailbox than the envelope recipient. Useful for aliases and
shared inboxes. folder defaults to inbox.
type = "store_as"
mailbox = "admin@example.com"
folder = "support"
put_in_folder
Store in the recipient mailbox, in a specific folder. Folder names ignore
case: folder = "Junk" and folder = "junk" both file into junk, the
mailbox's Junk folder, and every message filed there carries the $Junk
keyword.
type = "put_in_folder"
folder = "alerts"
set_flags
Store in the recipient's inbox and set IMAP flags on the message. Each flag is
optional and defaults to false.
type = "set_flags"
seen = true
flagged = true
answered = false
deleted = false
forward
Forward the message to another address and keep no local copy. The raw message is forwarded as-is, delivered by MX lookup.
type = "forward"
address = "ops@example.net"
store_and_forward
Store locally and forward to another address. folder defaults to inbox.
type = "store_and_forward"
address = "ops@example.net"
folder = "sales"
Outbound rules
Outbound rules apply to mail sent through the submission listener and use the
same match: header reads the headers of the submitted message,
envelope = "sender" its MAIL FROM, envelope = "receiver" the recipient
being delivered to. Submitted mail carries no authentication verdicts.
Four actions apply to outbound rules:
allowrejectdropforward
An outbound rule with any other action is skipped.
Reject outbound mail over 10 MB:
[[rule]]
id = "rule-outbound-size-limit"
direction = "outbound"
type = "reject"
match = { message = "size", gt = 10485760 }
Redirect outbound mail from one domain through a relay address:
[[rule]]
id = "rule-outbound-relay"
direction = "outbound"
type = "forward"
address = "relay@outbound.example.net"
match = { envelope = "sender", part = "domain", is = "example.com" }
Managing rules from the admin shell
| Command | Does |
|---|---|
/mail rules list | One row per rule in evaluation order: priority, direction, id, action, number of conditions, and status (active or skipped). Skipped rules are listed with their reason below the table. |
/mail rules show <id> | The rule's priority, direction, action with its folder, mailbox or address, and status, followed by the rule as it stands in rules.toml — a TOML block you can copy into the file. |
/mail rules create <id> key=value … match=<inline TOML> | Adds a rule. match= is the last argument and takes the rest of the line as an inline TOML value, spaces and quotes included. The rule is validated as rules.toml validates it, regular expressions included. |
/mail rules delete <id> | Removes the rule, skipped or active. |
/mail rules create rule-ci direction=inbound priority=3 type=put_in_folder folder=ci match={ header = "Subject", contains = "[build] finished" }
/mail rules show rule-ci
/mail rules show rule-spam prints:
id: rule-spam
priority: 10
direction: inbound
action: put_in_folder
folder: junk
conditions: 3
status: active
[[rule]]
id = "rule-spam"
priority = 10
direction = "inbound"
type = "put_in_folder"
folder = "junk"
match.any = [
{ header = "X-Spam-Flag", is = "yes" },
{ header = "X-Spam", is = "yes" },
{ header = "X-Spam-Status", matches = "^yes," },
]
The structured outlets (HTTP API, MCP) take match as a JSON object with the
same keys.
Example rules file
# Spam marked by an upstream filter goes to junk
[[rule]]
id = "rule-spam"
priority = 10
type = "put_in_folder"
folder = "junk"
match.any = [
{ header = "X-Spam-Flag", is = "yes" },
{ header = "X-Spam", is = "yes" },
{ header = "X-Spam-Status", matches = "^yes," },
]
# Mailing lists to their own folder
[[rule]]
id = "rule-lists"
priority = 20
type = "put_in_folder"
folder = "lists"
match = { header = "List-Id", exists = true }
# Alerts to a dedicated folder
[[rule]]
id = "rule-alerts"
priority = 5
type = "put_in_folder"
folder = "alerts"
match = { envelope = "receiver", is = "alerts@example.com" }
# Support tickets to the shared admin mailbox
[[rule]]
id = "rule-support"
priority = 5
type = "store_as"
mailbox = "admin@example.com"
folder = "support"
match = { envelope = "receiver", is = "support@example.com" }
# Oversized outbound messages are refused
[[rule]]
id = "rule-outbound-no-large"
direction = "outbound"
type = "reject"
match = { message = "size", gt = 10485760 }