Overview
Expressions are small conditional programs evaluated at runtime, controlling aspects such as whether a connection is accepted, which authentication mechanisms are offered, or how a message is routed. Several JMAP objects expose fields whose value is an expression; examples include saslMechanisms on MtaStageAuth, route on MtaRoute, and allowedEndpoints on Http. The reference page for each object marks which of its fields are expression-typed.
Expressions evaluate context variables, call functions, and combine values using operators. A result can be a boolean, a string, a number, or an array. Stalwart compiles expressions into bytecode at configuration time and evaluates them at runtime.
Shape of an expression
Section titled “Shape of an expression”An expression is stored as a JSON object with two fields. The match array lists zero or more {if, then} pairs that are evaluated in order; the first matching condition determines the result. The else string is the fallback when no condition matches:
{ "match": [ {"if": "<condition>", "then": "<value>"}, {"if": "<condition>", "then": "<value>"} ], "else": "<default value>"}All three fields (if, then, else) hold string values. Numbers, booleans, and arrays are written as their string representations: "true", "false", "3", "[plain, login]". When an expression always evaluates to the same result, the match array is omitted and only else is set:
{"else": "value"}Core concepts
Section titled “Core concepts”An expression is composed of the following elements:
- Variables carry contextual data supplied by the component evaluating the expression. For instance,
remote_ip,url_path,rcpt, orretry_nummay be available depending on the context. - Functions manipulate variables and values: string operations (
starts_with,contains), regular-expression matching (matches), DNS lookups (dns_query), directory queries (is_local_domain), and more. - Values can be booleans (
true,false), strings (for example'local','fallback'), numbers (for example25,1.26), or arrays that combine the above. - Operators combine values using arithmetic, logical, and comparison operations.
- Conditional logic chains
if/then/elseclauses to select a result.
Examples
Section titled “Examples”Each example below shows a partial JSON view of the parent object; only the expression-typed field is included.
A conditional expression offering PLAIN and LOGIN mechanisms only on TLS-protected submission ports, and disabling authentication elsewhere:
{ "saslMechanisms": { "match": [{"if": "local_port != 25 && is_tls", "then": "[plain, login]"}], "else": "false" }}An expression that rewrites non-SMTP recipients by extracting the local part and the top-level domain using regex capture groups:
{ "rewrite": { "match": [ {"if": "listener != 'smtp' && matches('^([^.]+)@([^.]+)\\.(.+)$', rcpt)", "then": "$1 + '@' + $3"} ], "else": "false" }}A multi-condition routing expression that returns different queue targets depending on whether the recipient is local and how many retries have elapsed:
{ "route": { "match": [ {"if": "is_local_domain(rcpt_domain)", "then": "'local'"}, {"if": "retry_num > 1", "then": "'fallback'"} ], "else": "'mx'" }}A DNSBL check that consults Spamhaus Zen for the remote address and returns the matched list category, combining ip_reverse_name, string concatenation, and dns_query:
{ "tag": { "match": [ {"if": "contains(dns_query(ip_reverse_name(remote_ip) + '.zen.spamhaus.org', 'ipv4'), '127.0.0.2')", "then": "'SBL'"}, {"if": "contains(dns_query(ip_reverse_name(remote_ip) + '.zen.spamhaus.org', 'ipv4'), '127.0.0.3')", "then": "'CSS'"}, {"if": "contains(dns_query(ip_reverse_name(remote_ip) + '.zen.spamhaus.org', 'ipv4'), '127.0.0.4')", "then": "'XBL'"} ], "else": "false" }}A per-sender rate limit that increments an in-memory counter and returns false (reject) once the threshold is exceeded. The counter key incorporates the authenticated account, so accounts do not share a budget:
{ "accept": {"else": "counter_incr('', 'rcpt-' + authenticated_as, count(recipients)) <= 500"}}A longer ladder that classifies MailSpike DNSBL responses by the fourth octet of the returned IP address:
{ "tag": { "match": [ {"if": "octets[0] != 127", "then": "false"}, {"if": "octets[3] == 10", "then": "'RBL_MAILSPIKE_WORST'"}, {"if": "octets[3] == 11", "then": "'RBL_MAILSPIKE_VERYBAD'"}, {"if": "octets[3] == 12", "then": "'RBL_MAILSPIKE_BAD'"}, {"if": "octets[3] >= 13 && octets[3] <= 16", "then": "'RWL_MAILSPIKE_NEUTRAL'"}, {"if": "octets[3] == 17", "then": "'RWL_MAILSPIKE_POSSIBLE'"}, {"if": "octets[3] == 18", "then": "'RWL_MAILSPIKE_GOOD'"}, {"if": "octets[3] == 19", "then": "'RWL_MAILSPIKE_VERYGOOD'"}, {"if": "octets[3] == 20", "then": "'RWL_MAILSPIKE_EXCELLENT'"} ], "else": "false" }}A simple fallback-only expression, with no match array:
{ "chunking": {"else": "remote_ip == '192.0.2.1'"}}