Overview
Expressions in Stalwart Mail Server provide a powerful, flexible, and dynamic way to configure and control various aspects of the mail server based on context. These expressions enable administrators to define conditional logic that evaluates variables, manipulates data, and returns results in the form of booleans, strings, numbers, or arrays. Think of expressions as small programs that evaluate conditions and produce results. These results can control various aspects of the mail server's behavior, from deciding whether to accept connections to determining how to route messages.
Stalwart Mail Server compiles these expressions into optimized bytecode, ensuring that they are executed with high performance and minimal memory usage. Expressions are used extensively across the server’s configuration, enabling precise customization for routing, authentication, spam filtering, and more.
Core Concepts
An expression consists of the following components:
- Variables: Variables are contextual data points provided by the mail server component evaluating the expression. For instance, variables like
remote_ip
,url_path
,rcpt
, orretry_num
might be available in different contexts, depending on where the expression is used. - Functions: A rich library of functions is available to manipulate variables and perform logic. These functions include string operations (
starts_with
,contains
), numerical comparisons, regex matching (matches
), and more. - Values: Expressions manipulate values that can be booleans (
true
orfalse
), strings (e.g.,'local'
,'fallback'
), numbers (e.g.,25
,1.26
) or arrays (combinations of the mentioned types). - Operators: Mathematical and logical operations that combine values.
- Conditional Logic: Multiple expressions can be combined using
if
andelse
clauses to define conditional behavior. Theif
clause specifies the condition to evaluate, while thethen
clause provides the result if the condition is true. Anelse
clause specifies the result if no conditions are met.
Expressions can return several types of values:
- Boolean: true/false values for conditional decisions.
- String: Text values for configuration settings or transformations.
- Number: Numerical values for thresholds or calculations.
- Arrays: Lists of values when multiple items are needed.
Writing Expressions
Basic Syntax
Expressions use a familiar programming-style syntax. They can be as simple as a single comparison or as complex as a multi-step evaluation. They are defined in configuration files, and their syntax follows a structured approach:
[component.section]
key = [
{ if = "condition", then = "value_if_true" },
{ if = "condition_2", then = "value_if_true" },
{ else = "value_if_false" }
]
Expressions can chain multiple conditions to handle different scenarios and each condition is evaluated sequentially until one matches. Expressions usually consist of one or more conditional blocks, each with an if
clause that evaluates a condition and a then
clause that specifies the result if the condition is true. If no conditions match, the else
clause is executed.
For example, the expression bellow allows access to HTTP endpoints if the IP starts with 192.180.1.
or the path contains specific filenames:
[server.http]
allowed-endpoint = [
{ if = "starts_with(remote_ip, '192.180.1.') || contains(['robots.txt', '.well-known'], split(url_path, '/')[1])",
then = "200" },
{ else = "400" }
]
For simple expressions, the if
/then
and else
blocks can be omitted, and a single value can be assigned directly, as shown below:
[session.data]
chunking = "remote_ip == '192.0.2.1'"
In this example, the chunking
setting is assigned based on the condition that the remote_ip
is equal to 192.0.2.1
.
Variables
Variables represent contextual information available to the expression. The exact variables available depend on where the expression is being evaluated. Common examples include:
remote_ip
: The IP address of the connecting clientlocal_port
: The port number the server is listening onurl_path
: The path component of a URL in HTTP contextssubject
: The email subject in message processing contexts
Functions
Functions provide powerful capabilities for manipulating values. Some common functions include:
starts_with(string, prefix)
: Checks if a string starts with a given prefixcontains(array, value)
: Checks if an array contains a specific valuematches(pattern, string)
: Performs regular expression matchingsplit(string, separator)
: Splits a string into an array
Operators
Operators are used to combine values and perform logical or mathematical operations. Common operators include:
- Arithmetic:
+
,-
,*
,/
- Logical:
&&
(AND),||
(OR),!
(NOT), '^' (XOR) - Comparison:
>
,<
,==
,!=
,>=
,<=
Control Flow
Expressions support conditional logic through if/then/else constructs. These are typically configured as arrays of conditions:
[some.configuration]
setting = [
{ if = "condition1", then = "result1" },
{ if = "condition2", then = "result2" },
{ else = "default_result" }
]
The use of expressions in the configuration file is optional though, static values can also be assigned to settings, for example:
setting = true
Examples
The following examples demonstrate how expressions can be used in different contexts to control the behavior of the mail server:
[session.auth]
mechanisms = [ { if = "local_port != 25 && is_tls", then = "[plain, login]"},
{ else = false } ]
[session.mail]
rewrite = [ { if = "listener != 'smtp' & matches('^([^.]+)@([^.]+)\.(.+)$', rcpt)", then = "$1 + '@' + $3" },
{ else = false } ]
[queue.outbound]
next-hop = [ { if = "is_local_domain('', rcpt_domain)", then = "'local'" },
{ if = "retry_num > 1", then = "'fallback'" },
{ else = false } ]
[queue.outbound.tls]
allow-invalid-certs = [ { if = "retry_num > 0 && last_error == 'tls'", then = true},
{ else = false } ]
starttls = [ { if = "retry_num > 1 && last_error == 'tls'", then = "disable"},
{ else = "require" } ]
[spam-filter.dnsbl.server.STWT_RBL_MAILSPIKE_IP]
enable = true
zone = [ { if = "location == 'tcp'", then = "ip_reverse + '.rep.mailspike.net'" },
{ else = false } ]
tag = [ { 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 } ]
scope = "ip"
[spam-filter.rule.STWT_FAKE_REPLY]
enable = true
scope = "any"
priority = 24
condition = [ { if = "contains_ignore_case(['re', 'aw', 'antw', 'sv'], split_once(subject, ':')[0]) && !$X_HDR_IN_REPLY_TO && !$X_HDR_REFERENCES", then = "'FAKE_REPLY'" },
{ else = false } ]
[spam-filter.rule.STWT_FREE_OR_DISP]
enable = true
scope = "email"
priority = 63
condition = [ { if = "!contains(['env_from', 'from', 'reply_to', 'to', 'cc', 'bcc', 'dnt'], location) || is_empty(sld)", then = "false" },
{ if = "key_exists('freemail-providers', sld)", then = "'FREEMAIL_' + to_uppercase(location)" },
{ if = "key_exists('disposable-providers', sld)", then = "'DISPOSABLE_' + to_uppercase(location)" },
{ else = false } ]