Untrusted interpreter
Users want to organise their own mail without asking an administrator: file newsletters into a folder, flag messages from their manager, forward copies elsewhere, or answer automatically while they are away. User scripts give them that control, and the untrusted interpreter runs them.
A user script runs when a message is delivered to the user. It can file the message into folders, set flags, forward it, discard it or send an automatic reply. Users upload and activate their scripts over ManageSieve or JMAP.
Every user can upload a script, and the server cannot vet each one. The interpreter therefore enforces limits on what a script may consume, and lets the administrator disable extensions.
For the same reason, user scripts can call only a subset of the function library. Functions that reach external systems, such as DNS or SQL lookups, are not available. The one exception is llm_prompt, which calls an AI model for accounts that hold the interactAi permission. Two interpreters compares the two interpreters.
The interpreter is configured on the SieveUserInterpreter singleton, found in the WebUI under Settings › Sieve › User Interpreter.
Resource limits
Section titled “Resource limits”A script can strain the server in two ways: by its size and structure, which matter when it is compiled, and by its behaviour, which matters every time it runs. The limits follow that division and fall into three groups. The first group bounds a script when it is uploaded, and a script that exceeds one of these limits is refused with a compile error:
| Field | Default | Limit |
|---|---|---|
maxScriptSize | 100 KiB | Size of a script. |
maxScriptNameLength | 512 | Length of a script name. |
maxStringLength | 4096 | Length of a string constant. |
maxNestedBlocks | 15 | Depth of nested if, elsif and else blocks. |
maxNestedTests | 15 | Depth of nested tests. |
maxNestedForEvery | 3 | Depth of nested foreverypart loops. |
maxMatchVars | 30 | Match and regular expression variables a script may capture. |
maxLocalVars | 128 | Variables in scope at any point. |
maxVarNameLength | 32 | Length of a variable name. |
maxHeaderSize | 1024 | Length of a header value written by a script. |
maxIncludes | 3 | include commands in a script. |
The second group bounds a script while it runs, which it does for every message the user receives:
| Field | Default | Limit |
|---|---|---|
maxCpuCycles | 5000 | Instructions executed in one run, including those of included scripts. |
maxNestedIncludes | 3 | Depth of nested include commands. |
maxVarSize | 4096 | Size of a variable in bytes; longer values are truncated. |
maxRedirects | 1 | redirect actions per run. |
maxOutMessages | 3 | Messages a script may send per run, counting vacation replies, notifications and redirects. |
maxReceivedHeaders | 50 | Received headers a message may carry before the script treats it as looping and sends no message for it. |
When a script fails at run time, for example by exceeding one of these limits, it stops. Actions it has already taken stay in effect. A message that the script had not yet filed, redirected or discarded is delivered to the inbox, so a failing script does not lose the message.
The headers a script adds to a message are also subject to limits outside the interpreter: the size and number of header fields allowed by maxHeaderSize and maxHeaderCount on the Email singleton. Once an added header would push the message over either limit, that header and every header added after it are dropped.
The third group bounds storage. maxScripts is the number of scripts an account may store, 100 by default. A maxSieveScripts entry in the account’s quotas overrides it for that account.
The following object body tightens the runtime limits for a server with many users:
{ "maxCpuCycles": 2000, "maxRedirects": 1, "maxOutMessages": 2}Extensions
Section titled “Extensions”Every extension listed in Supported extensions is available to user scripts unless it is named in disableCapabilities.
The extensions an administrator typically considers disabling are the ones with effects beyond the user’s own mailbox. enotify sends notifications to other addresses. editheader, replace and enclose change the content of the message, which can remove information that the administrator or other filters rely on. Forwarding with redirect is part of the base language and cannot be disabled, but it is bounded by maxRedirects.
Disabling an extension affects scripts in different ways, depending on when they were stored. Clients learn which extensions are available from the capability list of ManageSieve and JMAP, which omits disabled extensions. A new script that requires a disabled extension is refused at upload, with a compile error such as Capability 'enotify' is not supported at line <line>, column <column>..
Existing scripts are a different matter. Scripts that users uploaded before the extension was disabled are not checked again, so they fail at delivery when they reach it. A global script that requires a disabled extension is skipped when the configuration is loaded, with the build warning Failed to compile user global Sieve script, skipping it: <error>.
The following object body disables content modification and notifications:
{ "disableCapabilities": {"editheader": true, "replace": true, "enclose": true, "enotify": true}}The accepted names are listed in the SieveCapability enumeration.
Two settings narrow extensions without disabling them. The URI schemes that notify may use are the keys of allowedNotifyUris, which by default allows only mailto.
The header fields that editheader may neither add nor delete are the keys of protectedHeaders. The default protects Auto-Submitted, Original-From, Original-Subject and Received, so that a script cannot hide the path a message took or remove the marker of an automatic reply.
Vacation replies and generated messages
Section titled “Vacation replies and generated messages”The vacation extension sends an automatic reply to each sender. Replying to every message would flood correspondents who write often, so a sender receives a reply at most once within a period. When the script does not specify the period, the same sender receives another reply only after defaultExpiryVacation, 30 days by default.
The interpreter also fills in a subject when the script gives none. The reply then carries the original subject prefixed with defaultSubjectPrefix, Auto: by default. When the original subject is not known, it carries defaultSubject, Automated reply by default.
The duplicate extension lets a script recognise a message it has already seen, for example a copy that arrives through two mailing lists. It records the identifiers of the messages it tests. When the script does not specify an expiry, the identifiers expire after defaultExpiryDuplicate, 7 days by default.
Messages that a user script sends must pass the receivers’ authentication checks like any other mail. Vacation replies and the other messages a user script sends are therefore DKIM-signed, according to dkimSignDomain.
For these messages, the sender and sender_domain variables of the expression refer to the address of the account whose script sends the message. The envelope sender of the reply is empty (<>), as RFC 5230 section 5.1 recommends for vacation replies. The default expression signs with the account’s domain when it is a domain hosted on the server, and does not sign otherwise. The signature therefore aligns with the domain in the From header of the reply, which is the condition receivers check under DMARC.
Interaction with the spam filter
Section titled “Interaction with the spam filter”A user script runs after the spam filter has scored the message, so it can take the verdict into account. The script can test the result with the spamtest extension, or with the X-Spam-Score header.
The script also takes precedence over the Junk folder. When the script ends without filing the message, a message classified as spam is moved to the Junk folder as usual. When the script files the message with fileinto, including fileinto "INBOX", the message stays in the folder the script chose. It is not moved to Junk, and its X-Spam-Status header reads No.
Global scripts
Section titled “Global scripts”Global scripts let the administrator publish ready-made user scripts that every user can adopt.
Each global script is a SieveUserScript, found in the WebUI under Settings › Sieve › User Scripts. It has a name, an optional description, the script text in contents, and an isActive flag. The flag is false by default, so a new global script is not available until it is activated.
A global script is offered, not imposed. It runs only when a user’s script includes it with include :global "<name>", and it then runs with the user’s permissions in the untrusted interpreter.
The following object publishes a global script that files messages from mailing lists into a Lists folder:
{ "name": "file-lists", "description": "File mailing list messages", "isActive": true, "contents": "require [\"fileinto\", \"mailbox\"];\n\nif exists \"List-Id\" {\n fileinto :create \"Lists\";\n}\n"}A user enables it by including it from the active script:
require ["include"];
include :global "file-lists";