Function reference
This page lists the built-in functions that Stalwart adds to Sieve, grouped by what they work on. Each entry gives the arguments, what the function returns and, for most functions, an example. The functions are called from expressions, inside eval, let and while instructions. They operate on Sieve values (strings, integers, floats and arrays) and on the message being processed: its headers, MIME parts, envelope and environment.
Not every script may call every function. Some functions are available only to the trusted interpreter, which runs scripts installed by the administrator at an SMTP stage. The untrusted interpreter runs user-created scripts, and exposes a smaller subset. It excludes functions that touch external systems (DNS, HTTP, SQL, key-value stores and external programs), and functions that inspect parts of the message that are not relevant to per-user filtering. Each entry, or the heading of its group, notes when a function is restricted to trusted scripts.
Before any of these functions can be used, a script must declare the vnd.stalwart.expressions extension in a require statement.
Text functions
Section titled “Text functions”Removes whitespace from both ends of a string.
- Arguments: 1 (String or Array)
- Example:
let "name" "trim(header.subject)"removes leading and trailing whitespace from the subject line.
trim_start, trim_end
Section titled “trim_start, trim_end”Removes whitespace from the start or the end of a string.
- Arguments: 1 (String or Array)
- Example:
let "clean" "trim_end(header.subject)".
Returns the length in bytes for strings, or the number of elements for arrays.
- Arguments: 1 (String or Array)
- Example:
if eval "len(header.subject) > 200" { ... }detects unusually long subject lines.
to_lowercase, to_uppercase
Section titled “to_lowercase, to_uppercase”Converts a string to lowercase or uppercase.
- Arguments: 1 (String or Array)
- Example:
let "domain" "to_lowercase(email_part(envelope.from, 'domain'))".
is_lowercase, is_uppercase
Section titled “is_lowercase, is_uppercase”Returns true when every alphabetic character in the string is lowercase or uppercase. Non-alphabetic characters are ignored.
- Arguments: 1 (String or Array)
- Example:
if eval "is_uppercase(header.subject)" { addflag "$screaming"; }.
is_ascii
Section titled “is_ascii”Returns true when the value is a pure-ASCII string, integer, or float. For arrays, returns true when every string element is ASCII.
- Arguments: 1 (String, Number, or Array)
- Example:
if eval "!is_ascii(header.subject)" { ... }fires for a subject containing non-ASCII text.
has_digits
Section titled “has_digits”Returns true when the string contains at least one ASCII digit.
- Arguments: 1 (String or Array)
- Example:
if eval "has_digits(envelope.from)" { ... }.
count_chars
Section titled “count_chars”Counts the number of Unicode characters in a string.
- Arguments: 1 (String)
- Example:
count_chars('héllo')returns5.
count_spaces, count_uppercase, count_lowercase
Section titled “count_spaces, count_uppercase, count_lowercase”Counts the whitespace, uppercase alphabetic or lowercase alphabetic characters in a string.
- Arguments: 1 (String)
- Example:
if eval "count_uppercase(header.subject) > 20" { addflag "$screaming"; }.
contains
Section titled “contains”Returns true when the first argument contains the second. For arrays, element equality is used instead of substring search.
- Arguments: 2 (String or Array, Substring or Element)
- Example:
if eval "contains(header.subject, 'INVOICE')" { ... }.
contains_ignore_case
Section titled “contains_ignore_case”Like contains, but compares case-insensitively.
- Arguments: 2 (String or Array, Substring or Element)
eq_ignore_case
Section titled “eq_ignore_case”Compares two strings for equality, ignoring ASCII case.
- Arguments: 2 (String, String)
- Example:
if eval "eq_ignore_case(env.helo_domain, 'mail.example.org')" { ... }.
starts_with, ends_with
Section titled “starts_with, ends_with”Returns true when the first string starts or ends with the second.
- Arguments: 2 (String, Prefix or Suffix)
- Example:
if eval "ends_with(email_part(envelope.from, 'domain'), '.example.org')" { ... }.
strip_prefix, strip_suffix
Section titled “strip_prefix, strip_suffix”Removes the given prefix or suffix from a string. Returns the empty string when the affix is not present.
- Arguments: 2 (String, Affix)
- Example:
let "tenant" "strip_suffix(email_part(envelope.to, 'domain'), '.example.org')".
substring
Section titled “substring”Extracts a substring by character position. The second argument is the zero-based start index, and the third is the number of characters to take.
- Arguments: 3 (String, Start, Count)
- Example:
let "prefix" "substring(header.subject, 0, 10)".
Splits a string into an array on newline boundaries.
- Arguments: 1 (String)
split, rsplit
Section titled “split, rsplit”Splits a string on every occurrence of a delimiter. rsplit returns the array ordered from right to left.
- Arguments: 2 (String, Delimiter)
- Example:
let "labels" "rsplit(email_part(envelope.to, 'domain'), '.')"yields['org', 'example', 'mx1']formx1.example.org.
split_once, rsplit_once
Section titled “split_once, rsplit_once”Splits a string at the first or the last occurrence of the delimiter and returns the two parts as a two-element array. Returns the empty string when the delimiter is not found.
- Arguments: 2 (String, Delimiter)
split_n
Section titled “split_n”Splits a string at most n times. The final element contains any remaining input, including further occurrences of the delimiter.
- Arguments: 3 (String, Delimiter, Max Splits)
- Example:
split_n('a,b,c,d', ',', 2)returns['a', 'b', 'c,d'].
thread_name
Section titled “thread_name”Returns the thread name of the subject by stripping reply/forward prefixes (Re:, Fwd:, language variants) and normalising whitespace, so that related messages share the same value.
- Arguments: 1 (String)
- Example:
let "subject" "thread_name(header.subject)".
html_to_text
Section titled “html_to_text”Converts an HTML string to its plain-text equivalent.
- Arguments: 1 (String)
- Example:
let "body" "html_to_text(body.to_html)".
detect_language (trusted only)
Section titled “detect_language (trusted only)”Returns the two-letter ISO 639-1 language code detected for a string, or "unknown" if no language could be determined.
- Arguments: 1 (String)
- Example:
if eval "detect_language(body.to_text) == 'ru'" { fileinto "russian"; }.
levenshtein_distance (trusted only)
Section titled “levenshtein_distance (trusted only)”Returns the Levenshtein edit distance between two strings.
- Arguments: 2 (String, String)
- Example:
if eval "levenshtein_distance(to_lowercase(envelope.to), '[email protected]') <= 2" { ... }catches recipient typosquatting.
cosine_similarity (trusted only)
Section titled “cosine_similarity (trusted only)”Returns a float in [0, 1] representing the cosine similarity of two values. Arrays are compared as bags of elements and strings as bags of characters.
- Arguments: 2 (Array or String, Array or String)
jaccard_similarity (trusted only)
Section titled “jaccard_similarity (trusted only)”Returns a float in [0, 1] representing the Jaccard similarity (intersection over union) of two values. Arrays are compared as sets of elements and strings as sets of characters.
- Arguments: 2 (Array or String, Array or String)
Array functions
Section titled “Array functions”Returns the number of elements in an array. For non-array values, returns 1 when the value is non-empty and 0 otherwise.
- Arguments: 1 (Array)
- Example:
if eval "count(recipients) > 100" { ... }.
Returns a sorted copy of the input array. The second argument is true for ascending order and false for descending.
- Arguments: 2 (Array, Ascending)
- Example:
sort(['z', 'a', 'b'], true)returns['a', 'b', 'z'].
Returns a copy of the array with duplicate elements removed, preserving the original order.
- Arguments: 1 (Array)
winnow
Section titled “winnow”Returns a copy of the array with empty elements removed.
- Arguments: 1 (Array)
is_intersect
Section titled “is_intersect”Returns true when the two arrays share at least one element. When one of the arguments is a scalar, checks whether the value is contained in the other array.
- Arguments: 2 (Array, Array)
- Example:
if eval "is_intersect(header.to[*], ['[email protected]', '[email protected]'])" { ... }.
Email functions
Section titled “Email functions”is_email
Section titled “is_email”Returns true when the input string is a syntactically valid email address.
- Arguments: 1 (String)
email_part
Section titled “email_part”Extracts either the local part or the domain of an email address. The second argument must be the literal string 'local' or 'domain', and any other value returns the empty string.
- Arguments: 2 (Email, Part)
- Example:
let "from_domain" "email_part(envelope.from, 'domain')".
domain_part (trusted only)
Section titled “domain_part (trusted only)”Extracts a part of a domain name. The second argument selects which part to return:
-
sld: the second-level domain as recognised by the public suffix list. Formail.example.co.uk, returnsexample.co.uk. -
tld: the top-level domain only. Returns the last dot-separated label, for exampleuk. -
host: the leftmost label. Formail.example.org, returnsmail. -
Arguments: 2 (Domain, Part)
-
Example:
let "org_domain" "domain_part(email_part(envelope.from, 'domain'), 'sld')".
URL functions
Section titled “URL functions”uri_part
Section titled “uri_part”Extracts a component of a URI. The second argument selects which component:
scheme,host,port,path,query,authority,path_query: the corresponding URI component.scheme_host:scheme://host, useful for building the origin of a URL.
Returns the empty string when the argument does not parse as a URI or when the requested component is missing.
- Arguments: 2 (URI, Part)
- Example:
let "origin" "uri_part(header['list-unsubscribe'], 'scheme_host')".
puny_decode (trusted only)
Section titled “puny_decode (trusted only)”Decodes Punycode-encoded (xn--) labels in a domain name. Labels that are not Punycode-encoded pass through unchanged.
- Arguments: 1 (String)
- Example:
let "readable" "puny_decode(env.helo_domain)".
Unicode-safety functions (trusted only)
Section titled “Unicode-safety functions (trusted only)”Unicode offers many characters that look alike, and characters that are invisible or reverse the direction of text. Attackers use them to make an address look like another, or to slip a word past a filter that matches text. These functions help detect such abuse.
has_zwsp
Section titled “has_zwsp”Returns true when the string contains at least one zero-width or invisible separator (U+200B, U+200C, U+200D, U+FEFF, U+00AD).
- Arguments: 1 (String or Array)
has_obscured
Section titled “has_obscured”Returns true when the string contains a character from one of the directional-override or formatting ranges (U+200B to U+200F, U+2028 to U+202F, U+205F to U+206F, and U+FEFF) that can be used to hide or reorder text.
- Arguments: 1 (String or Array)
is_mixed_charset
Section titled “is_mixed_charset”Returns true when the string mixes characters from scripts that should not normally co-occur (for example Latin and Cyrillic letters in the same word), a common indicator of a homograph attack.
- Arguments: 1 (String)
cure_text
Section titled “cure_text”Returns a cleaned copy of the string with confusable, invisible, and zalgo characters removed or normalised.
- Arguments: 1 (String)
unicode_skeleton
Section titled “unicode_skeleton”Returns the Unicode confusables “skeleton” of the string, as defined by UTS #39. Two strings with the same skeleton look alike to a human reader, so comparing skeletons detects visually similar but differently encoded strings.
- Arguments: 1 (String)
- Example:
if eval "unicode_skeleton(envelope.from) == unicode_skeleton('[email protected]')" { ... }flags a lookalike of the CEO address.
Numeric and IP functions
Section titled “Numeric and IP functions”is_empty
Section titled “is_empty”Returns true when the value is an empty string or an empty array. Numeric values are never considered empty.
- Arguments: 1 (Value)
is_number
Section titled “is_number”Returns true when the value is an integer or a float. A numeric string returns false.
- Arguments: 1 (Value)
is_ip_addr, is_ipv4_addr, is_ipv6_addr
Section titled “is_ip_addr, is_ipv4_addr, is_ipv6_addr”Returns true when the string parses as an IP address of the corresponding family.
- Arguments: 1 (String)
- Example:
if eval "is_ipv6_addr(env.remote_ip)" { ... }.
ip_reverse_name (trusted only)
Section titled “ip_reverse_name (trusted only)”Returns the reverse-DNS form of an IP address, suitable for composing DNSBL lookups. For IPv4 the octets are reversed, and for IPv6 each nibble is reversed and separated by dots. The same value is exposed as the environment variable env.remote_ip.reverse.
- Arguments: 1 (IP Address)
is_ip_in_cidr (trusted only)
Section titled “is_ip_in_cidr (trusted only)”Returns true when the first argument is an IP address that falls inside the network described by the second argument. The network argument accepts either a CIDR block (10.0.0.0/8, 2001:db8::/32) or a bare IP address, in which case the function is an exact-match check. IPv4-mapped IPv6 addresses (::ffff:1.2.3.4) are matched correctly against IPv4 networks, and vice versa. Both arguments must parse cleanly, and malformed values yield false.
- Arguments: 2 (IP Address, CIDR or IP Address)
- Example:
if eval "is_ip_in_cidr(env.remote_ip, '10.0.0.0/8') || is_ip_in_cidr(env.remote_ip, '192.168.0.0/16')" { ... }recognises connections from RFC 1918 ranges, useful when a TLS-terminating reverse proxy fronts the SMTP listener and the special handling that normally applies to port 25 needs to be reapplied based on the original source.
Hashing (trusted only)
Section titled “Hashing (trusted only)”Computes a hash of the input string and returns it as a lowercase hexadecimal string. Supported algorithms are md5, sha1, sha256, and sha512. Any other algorithm name produces the empty string.
- Arguments: 2 (String, Algorithm)
- Example:
let "sender_id" "hash(envelope.from, 'sha256')".
Message and MIME functions (trusted only)
Section titled “Message and MIME functions (trusted only)”A message can contain many parts, and these functions work on one part at a time. They inspect the MIME part that a foreverypart loop is currently visiting. When they are called outside a foreverypart block, they inspect the top-level part.
is_body
Section titled “is_body”Returns true when the current MIME part is part of the message body (text or HTML).
- Arguments: none
is_attachment
Section titled “is_attachment”Returns true when the current MIME part is an attachment.
- Arguments: none
attachment_name
Section titled “attachment_name”Returns the declared filename of the current MIME part, or the empty string if none is declared.
- Arguments: none
mime_part_len
Section titled “mime_part_len”Returns the byte length of the current MIME part.
- Arguments: none
is_encoding_problem
Section titled “is_encoding_problem”Returns true when the current MIME part had a transfer-encoding or character-set decoding error during parsing.
- Arguments: none
is_header_utf8_valid
Section titled “is_header_utf8_valid”Returns true when the named header (or every header, if the argument is not a known header name) contains only valid UTF-8. Headers that are not valid UTF-8 are often a sign of a malformed or adversarial message.
- Arguments: 1 (Header name)
received_part
Section titled “received_part”Extracts a component of the Nth Received header (1-based). The second argument selects the component, and the accepted values include from, iprev, iprev.domain, iprev.ip, by, for, id, via, with, tls.version, tls.cipher, date, date.year, date.month, date.day, date.hour, date.minute, date.second, date.dow, date.ordinal, date.iso8601, date.rfc822.
- Arguments: 2 (Index, Part)
- Example:
let "first_hop" "received_part(1, 'from')".
var_names
Section titled “var_names”Returns an array of the names of all global variables currently in scope, upper-cased.
- Arguments: none
Image and file-type functions (trusted only)
Section titled “Image and file-type functions (trusted only)”These functions examine the content of an attachment: the properties of an image, or the file type inferred from the content itself rather than from its declared Content-Type. Like the message functions, they operate on the MIME part that foreverypart is currently visiting.
img_metadata
Section titled “img_metadata”Returns a property of an image attachment. The argument selects which:
type: format name, one ofjpeg,png,gif,webp,heif,bmp,tiff, and others supported by the image decoder. Returns"unknown"for unrecognised formats.width,height: integer dimensions in pixels.area:width * height.dimension:width + height.
Returns the empty string when the part is not a recognised image.
- Arguments: 1 (Property)
- Example:
if eval "img_metadata('type') == 'gif' && img_metadata('area') < 100" { ... }detects suspiciously tiny tracking pixels.
detect_file_type
Section titled “detect_file_type”Returns the inferred MIME type of the current MIME part based on content sniffing, ignoring the declared Content-Type. When the argument is "ext", returns a file-extension instead of a MIME type.
- Arguments: 1 (String, either
"mime"or"ext") - Example:
if eval "detect_file_type('ext') == 'exe'" { discard; }.
DNS functions (trusted only)
Section titled “DNS functions (trusted only)”dns_query
Section titled “dns_query”Performs a DNS query for the given name and record type. Supported record types are ipv4, ipv6, ip (IPv4 with IPv6 fallback), mx, txt, and ptr. For ptr, the first argument must parse as an IP address.
Address and ptr queries return arrays of strings, mx returns an array of "preference host" strings, and txt returns a single concatenated string. When the lookup fails, the function returns a short error code: temp_fail, not_found, io_error, invalid_record, or unknown_error.
-
Arguments: 2 (Name, Record Type)
-
Example: DNSBL lookup against Spamhaus Zen:
if eval "contains(dns_query('${env.remote_ip.reverse}.zen.spamhaus.org', 'ipv4'), '127.0.0.2')" {reject "550 5.7.1 Listed on Spamhaus SBL.";}
dns_exists
Section titled “dns_exists”Returns 1 when a record of the given type exists for the name, 0 when it does not, and -1 on error. Supported types are ip, ipv4, ipv6, mx, and ptr.
- Arguments: 2 (Name, Record Type)
- Example:
if eval "dns_exists(email_part(envelope.from, 'domain'), 'mx') != 1" { reject "550 5.7.1 Your domain has no MX record."; }.
Directory functions (trusted only)
Section titled “Directory functions (trusted only)”is_local_domain
Section titled “is_local_domain”Returns true when the domain is registered in the server’s directory.
- Arguments: 1 (Domain)
- Example:
if eval "is_local_domain(email_part(envelope.to, 'domain'))" { ... }.
Key-value store functions (trusted only)
Section titled “Key-value store functions (trusted only)”Key-value functions let a script check a value against a list, or remember something between messages, such as a temporary block on a client address. The first argument of each function, the store ID, names the lookup list to use. It is the namespace of a lookup defined as a StoreLookup, HttpLookup or MemoryLookupKey. An empty string or * selects the default in-memory store.
A store ID that matches no lookup does not stop the script. The function returns an empty value, the failure is logged, and the script continues.
key_get
Section titled “key_get”Returns the value associated with a key, or the empty string when the key does not exist.
- Arguments: 2 (Store ID, Key)
key_exists
Section titled “key_exists”Returns true when the key exists. When the second argument is an array, returns true as soon as any of the keys is found.
- Arguments: 2 (Store ID, Key or Array)
key_set
Section titled “key_set”Stores a value under a key, creating it if necessary, and optionally sets an expiration time in seconds. Returns true on success. Setting an empty value stores an empty marker.
-
Arguments: 4 (Store ID, Key, Value, Expires)
-
Example: stop-list a repeat offender for an hour:
eval "key_set('', 'block-${env.remote_ip}', '1', 3600)";
SQL queries (trusted only)
Section titled “SQL queries (trusted only)”Runs an SQL statement. The first argument, the store ID, is the namespace of a StoreLookup whose store is an SQL database, and the empty string selects the data store, which must then be an SQL database. The second argument is the statement, and the third argument binds parameters to the query’s placeholders, in order. The query is passed to the database driver verbatim, so the statement must use the configured database’s native placeholder syntax: ? for SQLite and MySQL, and $1, $2, … for PostgreSQL.
The shape of the result follows the shape of the rows:
-
A
SELECTreturning exactly one row and one column returns the scalar value. -
A
SELECTreturning a single row with multiple columns returns that row as an array. -
A
SELECTreturning multiple rows returns an array of arrays. -
A statement other than
SELECTreturnstrueon success. -
Arguments: 3 (Store ID, Query, Parameters)
-
Example: count the messages a sender has sent through a reporting table on SQLite or MySQL, where the placeholder is
?(PostgreSQL uses$1):require ["variables", "vnd.stalwart.expressions", "envelope", "editheader"];let "sent" "query('reporting', 'SELECT COUNT(*) FROM sent_log WHERE sender = ?', [envelope.from])";if eval "sent > 1000" {addheader "X-High-Volume-Sender" "yes";}
Greylisting needs no script, because it is built into the SMTP server, as described in Greylisting.
HTTP functions (trusted only)
Section titled “HTTP functions (trusted only)”http_header
Section titled “http_header”Performs an HTTP GET request and returns the value of a response header, or the empty string if the header is absent or the request fails. Redirects are not followed and invalid TLS certificates are accepted, so the function is suitable only for querying known internal endpoints. When the URL is built from message content, such as a sender address or a link in the body, the sender of the message controls which host the server contacts, so build such URLs only from values that have been validated.
- Arguments: 4 (URL, Header Name, User-Agent, Timeout in milliseconds)
- Example:
let "tier" "http_header('https://internal.example.org/api/tier', 'X-Tier', 'stalwart-sieve', '2000')".
Message-modification functions (trusted only)
Section titled “Message-modification functions (trusted only)”add_header
Section titled “add_header”Queues a header addition on the message. Returns true when both arguments are strings. The headers are appended to the message after the script completes.
- Arguments: 2 (Name, Value)
- Example:
eval "add_header('X-Greeting', 'Hello from Stalwart')".
Tokenisation (trusted only)
Section titled “Tokenisation (trusted only)”tokenize
Section titled “tokenize”Extracts tokens from a string. The second argument selects the token class:
words: alphanumeric whitespace-separated tokens.uriorurl: URLs, including bare hostnames without a scheme (rewritten tohttps://) and email addresses.uri_strictorurl_strict: URLs with a scheme only.email: email addresses.
Returns an array of tokens.
- Arguments: 2 (String, Token Class)
- Example:
let "urls" "tokenize(body.to_text, 'url_strict')".
External programs (trusted only)
Section titled “External programs (trusted only)”Runs an external program. The first argument is the path to the executable, and the second is an array of arguments to pass. Returns true when the process exits successfully and false otherwise. The program runs with the privileges of the Stalwart process, and every message that reaches the script starts it. Arguments taken from the message or the envelope are chosen by the sender, so the program must treat them as untrusted input.
-
Arguments: 2 (Path, Arguments)
-
Example:
require "vnd.stalwart.expressions";if eval "!exec('/opt/stalwart/bin/validate.sh', [env.remote_ip, envelope.from])" {reject "550 5.7.1 You are not allowed to send email.";}
AI models (trusted and untrusted)
Section titled “AI models (trusted and untrusted)”llm_prompt
Section titled “llm_prompt”Sends a prompt to an AI model and returns the response, or false when the call fails. Available in the trusted interpreter, and in the untrusted interpreter for accounts with the interactAi permission. AI models in Sieve describes the function in full.
- Arguments: 3 (Model name, Prompt, Temperature)