Skip to content

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.

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.

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.

Converts a string to lowercase or uppercase.

  • Arguments: 1 (String or Array)
  • Example: let "domain" "to_lowercase(email_part(envelope.from, 'domain'))".

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"; }.

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.

Returns true when the string contains at least one ASCII digit.

  • Arguments: 1 (String or Array)
  • Example: if eval "has_digits(envelope.from)" { ... }.

Counts the number of Unicode characters in a string.

  • Arguments: 1 (String)
  • Example: count_chars('héllo') returns 5.

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"; }.

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')" { ... }.

Like contains, but compares case-insensitively.

  • Arguments: 2 (String or Array, Substring or Element)

Compares two strings for equality, ignoring ASCII case.

  • Arguments: 2 (String, String)
  • Example: if eval "eq_ignore_case(env.helo_domain, 'mail.example.org')" { ... }.

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')" { ... }.

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')".

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)

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'] for mx1.example.org.

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)

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'].

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)".

Converts an HTML string to its plain-text equivalent.

  • Arguments: 1 (String)
  • Example: let "body" "html_to_text(body.to_html)".

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"; }.

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.

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)

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)

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)

Returns a copy of the array with empty elements removed.

  • Arguments: 1 (Array)

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.

Returns true when the input string is a syntactically valid email address.

  • Arguments: 1 (String)

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')".

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. For mail.example.co.uk, returns example.co.uk.

  • tld: the top-level domain only. Returns the last dot-separated label, for example uk.

  • host: the leftmost label. For mail.example.org, returns mail.

  • Arguments: 2 (Domain, Part)

  • Example: let "org_domain" "domain_part(email_part(envelope.from, 'domain'), 'sld')".

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')".

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 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.

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)

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)

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)

Returns a cleaned copy of the string with confusable, invisible, and zalgo characters removed or normalised.

  • Arguments: 1 (String)

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.

Returns true when the value is an empty string or an empty array. Numeric values are never considered empty.

  • Arguments: 1 (Value)

Returns true when the value is an integer or a float. A numeric string returns false.

  • Arguments: 1 (Value)

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)" { ... }.

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)

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.

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')".

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.

Returns true when the current MIME part is part of the message body (text or HTML).

  • Arguments: none

Returns true when the current MIME part is an attachment.

  • Arguments: none

Returns the declared filename of the current MIME part, or the empty string if none is declared.

  • Arguments: none

Returns the byte length of the current MIME part.

  • Arguments: none

Returns true when the current MIME part had a transfer-encoding or character-set decoding error during parsing.

  • Arguments: none

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)

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')".

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.

Returns a property of an image attachment. The argument selects which:

  • type: format name, one of jpeg, 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.

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; }.

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.";
    }

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."; }.

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 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.

Returns the value associated with a key, or the empty string when the key does not exist.

  • Arguments: 2 (Store ID, Key)

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)

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)";

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 SELECT returning exactly one row and one column returns the scalar value.

  • A SELECT returning a single row with multiple columns returns that row as an array.

  • A SELECT returning multiple rows returns an array of arrays.

  • A statement other than SELECT returns true on 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.

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)”

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')".

Extracts tokens from a string. The second argument selects the token class:

  • words: alphanumeric whitespace-separated tokens.
  • uri or url: URLs, including bare hostnames without a scheme (rewritten to https://) and email addresses.
  • uri_strict or url_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')".

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.";
    }

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)