Rules
A rule tags, flags, marks read or moves a new message that meets its conditions. This page explains how to write rules, how they combine and how they treat folders.
A first rule
Section titled “A first rule”Declare rules in init.lua with rules.on: a name, the conditions a message has to meet, and the actions to take on it.
rules.on("newsletter", { from = "news@example.com", subject = "test 123", header = { ["List-Unsubscribe"] = true } }, { tag = "vista", mark_read = true, move = "Newsletter" })
rules.list() -- { "newsletter" }rules.off("newsletter") -- removes it; an unknown name is ignoredTry it on the messages already in a folder with a dry run, which changes nothing:
tmail rules run personal INBOX --dry-runWhere rules run
Section titled “Where rules run”The Lua side only records the definitions; the rule service in the library matches them and acts on the messages.
tmail watchcalls the rule service for every new message, after thenew_mailhandlers have run. It does not rely onIRuleService.OnNewMailAsync, which is there for a host that wants to subscribe the rules to the new-mail watcher on its own.tmail rules runapplies the rules to the newest messages of a folder.
Conditions
Section titled “Conditions”Every condition given must hold: they are combined with AND. A rule needs at least one.
| Key | Tests | Accepts |
|---|---|---|
from |
the From header as tmail reads it (display name and address) |
a string or a list of strings |
to |
the To header only; a Cc header does not count |
a string or a list of strings |
subject |
the subject | a string or a list of strings |
header |
the named headers | a table { [name] = value }, where value is a string, a list of strings, or true |
How they match:
- A string matches when the field contains it, ignoring case. It is a substring test:
subject = "test"matches “Latest TEST results”. - A list matches when any of its elements does.
headerrequires every named header to be present. With a string or a list, the header’s value must also contain one of the texts;trueonly requires that the header exists. Header names are case-insensitive.- When a message has the same header more than once, only the first value is matched.
- The body is not matched.
toandheaderneed the message headers.tmail rules runfetches them only when a rule uses one of these keys, by downloading the whole message;fromandsubjectare read from the message list.
Actions
Section titled “Actions”| Key | Does | Accepts |
|---|---|---|
tag |
adds IMAP keywords (tags) to the message | a string or a list of strings |
mark_read |
sets the Seen flag |
a boolean |
flag |
sets the Flagged flag |
a boolean |
move |
moves the message to a folder of its account, created when missing | a folder name |
stop |
ends the walk over the rules after this one | a boolean |
- A tag is an IMAP keyword: 1 to 64 ASCII letters, digits or
_ - . $, starting with a letter,_or$. The system keywordsSeen,Flagged,Answered,Deleted,DraftandRecentare refused in any case. - Tags are stored on the server as keywords of the message, so the folder must accept custom keywords. When it does not, the tag step fails with
The server does not allow custom keywords in <folder>. - tmail does not read tags back yet: the message summaries it holds do not show them.
- On a Maildir account every
tagstep fails withKeywords are not supported on Maildir accounts, because tmail keeps no keywords there. It is a step error like any other, so the other steps still run.
How several rules combine
Section titled “How several rules combine”A new message is checked against every rule, in the order the rules were defined:
- Every matching rule contributes its actions. A rule that does not match contributes nothing, its
stopincluded. - A matching rule with
stop = trueends the walk after its own actions. The rules after it are not checked. - Tags are the union of the tags of the matching rules, in order of first appearance, with case-insensitive duplicates removed.
mark_readandflagapply when any matching rule asks for them.moveis themoveof the first matching rule that has one; later ones are ignored.
The actions then run in this order, each one a separate call to the server: one keyword per tag, mark read, flag, folder creation when needed, and the move.
Folders
Section titled “Folders”- Creation. A
moveto a folder that is not in the account’s folder list creates the folder first. Created folders are not subscribed. - Names. Names are compared exactly, case included. On a server that treats names case-insensitively, the existing folder is used, and the
create folderline of a dry run may be shown although nothing is created. - INBOX.
INBOXis the exception: it is matched ignoring case (move = "Inbox"isINBOX) and is never created. - Prefixes. On a server that keeps every folder under a prefix such as
INBOX., write the prefix (INBOX.Newsletter). A name without it is refused before anything is created. - Nested names are created one level at a time, split on the hierarchy delimiter the server reports for the personal namespace. With
/as the delimiter,Lists/DevcreatesListswhen missing, thenLists/Dev. With.as the delimiter, the same name is one folder whose name contains a/. On a server without a hierarchy the whole name is one folder. A name with an empty part, such asLists//Dev, is an error. - Maildir. On a Maildir account, a
movecreates the missing Maildir folder with itscur,newandtmpdirectories. - Once per run. A folder created or found during a run is remembered for the rest of that run, so it is created once. A run reads the folder list at most once, and only when a move needs it.
- Already there. A message already in the target folder is not moved, and no step is recorded for it.
INBOXcounts as the same folder in any case.
Errors and dry runs
Section titled “Errors and dry runs”- Each step is independent. A failing step is logged and reported with its message, and the next step still runs: a tag the server refuses does not stop the move.
- The exception is the folder creation and the move: if the folder cannot be created, the move is not attempted and the message stays where it is.
- A dry run (
tmail rules run --dry-run) lists the steps and makes no change: no tag, flag, folder or move is sent. The folder list is still read, socreate folderappears only for a folder that does not exist yet.
Validation
Section titled “Validation”rules.on checks a rule before it registers it:
- A rule needs at least one condition and at least one action. A
falseboolean is not an action, so{ mark_read = false }alone is refused.{ stop = true }is an action, so a rule whose only action isstop = trueis allowed. - An unknown key in either table is an error that lists the valid ones, for example
rule 'n': unknown condition 'subjct'; valid conditions: from, to, subject, header. - Wrong types, empty or blank strings (
from = "",from = " "), empty lists (from = {}), non-string list elements, an empty or blankmoveand an invalid tag are errors too. Each error names the rule. - A refused rule is not registered. The
rules.oncall raises an error;tmail rules runprints it and exits with 1.
Redefining and reloading
Section titled “Redefining and reloading”rules.onwith the name of an existing rule replaces that rule and keeps its place in the order. Names are case-sensitive and must not be blank.- A reload of the scripts forgets every rule;
init.luadefines them again.