Trusted interpreter
Some mail policies cannot be expressed with the settings of the SMTP server: rejecting a sender based on a custom lookup, adding a header that downstream systems rely on, or rewriting part of a message. System scripts fill that gap. Administrators install them to act on messages while the SMTP server receives them, and the trusted interpreter runs them.
Because only administrators write system scripts, the trusted interpreter gives them wide powers. System scripts can call every function of the function library, including DNS, SQL, HTTP and AI model calls. They cannot file messages into mailboxes or send automatic replies, which remain the job of user scripts. Two interpreters compares the two interpreters.
Installing a system script
Section titled “Installing a system script”Each system script is a SieveSystemScript, found in the WebUI under Settings › Sieve › System Scripts. The object has a name, an optional description, the script text in contents, and an isActive flag.
Only active scripts are loaded. isActive is false by default, so a new script does nothing until it is activated, which allows a script to be prepared before it takes effect. Script names are case-insensitive and must be unique among the active scripts.
An active script still runs only when an SMTP stage selects it. The stage objects are MtaStageConnect, MtaStageEhlo, MtaStageMail, MtaStageRcpt and MtaStageData. Each has a script field, an expression that returns the name of the script to run for the current session, or false to run none.
Because the field is an expression, a stage can run different scripts depending on the listener, the client or the sender. Scripts can also load other active system scripts with the include command, so common logic can be kept in one place.
The following object defines a script that rejects clients whose EHLO domain is listed in the blocked-domains lookup list:
{ "name": "block-helo", "description": "Reject blocklisted HELO domains", "isActive": true, "contents": "require [\"variables\", \"extlists\", \"reject\"];\n\nif string :list \"${env.helo_domain}\" \"blocked-domains\" {\n reject \"550 5.7.1 Your domain '${env.helo_domain}' has been blocklisted.\";\n}\n"}The EHLO stage then selects it with the following script value on MtaStageEhlo:
{ "script": {"else": "'block-helo'"}}Each script is compiled when it is saved. A script that does not compile is refused with an invalidProperties error, Failed to compile system Sieve script: <error>.
A script that compiled once can still stop compiling later, for example after a change to the interpreter settings. A stored script that no longer compiles when the settings are loaded is reported as a configuration error with the same text, and is not loaded.
Scripts cannot be loaded from files, so contents always holds the full text of the script.
Replies and actions
Section titled “Replies and actions”A system script runs in the middle of an SMTP transaction, so its outcome is an SMTP reply. It decides what happens to the command or message being processed:
reject "<reply>"refuses the current command, or the message at the DATA stage. The text is sent as the SMTP reply when it starts with a three-digit code followed by a space, for examplereject "550 5.7.1 Not accepted.";. Any other text is sent as503 5.5.3 <text>, a “bad sequence of commands” reply that does not describe a policy rejection, so always start the text with a reply code. A reject at the connect stage closes the connection after the reply, while at the other stages the session continues.discardat the DATA stage accepts the message with250 2.0.0 Message queued for delivery.and drops it. At the other stages it has no effect.- Ending the script without one of these actions accepts the command. At the DATA stage, changes made by
addheader,deleteheader,replaceand the other editing extensions are applied to the queued message. redirectand the notification and reply actions send new messages through the queue, up tomaxOutMessagesper run.
A runtime error, such as exceeding a limit, ends the script. A reject issued before the error still applies. Without one, the command is accepted, so a failing script does not block mail.
Settings
Section titled “Settings”The trusted interpreter is configured on the SieveSystemInterpreter singleton, found in the WebUI under Settings › Sieve › System Interpreter.
System scripts can send messages of their own, for example with redirect or notify. These messages need a sender and, to be trusted by receivers, a DKIM signature. The following settings decide how they are addressed and signed. Each is an expression, and the first three are evaluated with the RCPT TO variables:
| Field | Default | Use |
|---|---|---|
defaultFromName | 'Automated Message' | Display name in the From header of generated messages. |
defaultFromAddress | 'MAILER-DAEMON@' + system('domain') | Address in the From header of generated messages. |
defaultReturnPath | Not set | Envelope sender of generated messages. While it is unset, they are sent with an empty envelope sender (<>), so no bounce is generated for them. |
dkimSignDomain | system('domain') | Domain whose DKIM keys sign generated messages; every active key of that domain is used. |
Two further settings complete the picture. messageIdHostname overrides the host name used in the Message-ID header of generated messages, which is the server host name when the field is not set. noCapabilityCheck allows system scripts to use extensions without declaring them in a require statement. It is enabled by default.
Limits
Section titled “Limits”System scripts are trusted, but they can still contain mistakes, and a mistake in a script that runs for every message affects the whole server. The limits protect the server from scripts that loop or produce too much output:
| Field | Default | Limit |
|---|---|---|
maxCpuCycles | 1048576 | Instructions a script may execute in one run. |
maxVarSize | 52428800 | Size of a variable, in bytes. |
maxNestedIncludes | 5 | Depth of nested include commands. |
maxRedirects | 3 | redirect actions per run. |
maxOutMessages | 5 | Messages a script may send per run. |
maxReceivedHeaders | 50 | Received headers a message may carry before a script treats it as looping and sends nothing for it. |
The following object body lowers the instruction limit and the number of messages a script may send:
{ "maxCpuCycles": 100000, "maxOutMessages": 2}The trusted interpreter has no expiry setting for the duplicate extension, because the extension is not available to system scripts. The untrusted interpreter has one.
Troubleshooting system scripts
Section titled “Troubleshooting system scripts”A system script runs unseen, inside SMTP sessions, so a script that seems to have no effect gives no direct sign of why. The cause is usually one of the following situations, each of which is recorded in the server log:
- The script is not active. Inactive scripts are not loaded, and a stage that names one logs
sieve.script-not-found. - The name returned by the stage’s
scriptexpression does not match the script’s name, or the expression is not set on the stage where the script is expected to run. The stage logssieve.script-not-foundwith the name it looked for. - The script was created or changed after the settings were last reloaded. System scripts are loaded with the server settings, so a new or edited script takes effect only after a
ReloadSettingsaction, as described in Applying changes. - The script no longer compiles with the current interpreter settings, and the configuration error
Failed to compile system Sieve scriptwas reported when the settings were loaded. - The script uses an extension the trusted interpreter does not provide, such as
fileintoorvacation. The script fails at itsrequirestatement or, without one, at the action, which is refused with the eventsieve.not-supported. - The script ran but failed, for example by exceeding
maxCpuCyclesor calling a lookup list that does not exist. The eventssieve.runtime-errorandsieve.list-not-foundrecord the cause, andsieve.runtime-erroris logged at thedebuglevel.
When the script does run, its decisions are logged too. The actions a script takes are logged as sieve.action-accept, sieve.action-reject and sieve.action-discard at the debug level.