Documentation

Last updated: 2026-11-12

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.

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 priority defaults to 10
  • equal priorities keep file order
  • priority = 0 is 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" }
FieldDefaultDescription
id—Required. Unique rule identifier, starting with rule-.
name—Human-readable label for your own reference.
priority10Evaluation order. Lower numbers run first.
directioninboundinbound or outbound.
type—Required. The action, with its own fields.
matchevery messageThe 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

SourceWhat 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.

PartTests
addressthe whole address, local@domain
localpartthe part before the last @
domainthe part after the last @
namethe 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

TestSourcesMatches when
isheader, envelope, auththe value equals the given text
containsheader, envelope, auththe value contains the given text
matchesheader, envelope, auththe regular expression finds a match in the value
exists = true / falseheader, envelope, auththe header is present / the envelope sender is set / a verdict is present — or, with false, the opposite
gtmessage = "size"the size in bytes is greater than the number
ltmessage = "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

NodeMatches 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:

  • allow
  • reject
  • drop
  • forward

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

CommandDoes
/mail rules listOne 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 }
mail rules smtp