Account Sieve and ManageSieve

Each account can store 32 named Sieve scripts in mail.db, with one active script. An empty active name disables Sieve. Uploads compile the whole script before changing stored data, including replacements of active scripts.

The interpreter is Rust inside maild with no subprocess, filesystem or network access. The maintained Stalwart sieve-rs was evaluated first: its AGPL licence fails MixOS's permissive dependency policy. No suitable maintained permissive Rust interpreter was found. This implementation uses the existing MIME parser and native mail store without new dependencies.

Language

Base commands are require, if/elsif/else, stop, keep, discard and redirect. Tests are address, header, exists, size, allof, anyof, not, true and false. Quoted and text: multi-line strings, hash and bracketed comments, :is/:contains/:matches, i;ascii-casemap and i;octet are supported.

Extensions: fileinto, mailbox (fileinto :create and account-scoped mailboxexists for writable mailboxes), envelope, imap4flags, copy, body, variables, vacation, relational, comparator-i;ascii-numeric, reject, ereject and index. Required capabilities must precede executable statements. Unknown commands/tags/capabilities are refused, including in dead branches. date/currentdate are deferred and not advertised.

require ["fileinto", "mailbox", "imap4flags", "body"];
if header :is "X-Mixos-Spam-Verdict" "SPAM" {
    fileinto :create "Spam";
    stop;
}
if body :text :contains "release announcement" {
    fileinto :create :flags "\\Flagged" "Releases";
}

fileinto uses slash-separated mailbox paths; INBOX resolves by role. :create creates missing parents. Non-writable destinations are treated as missing, consistently with mailboxexists. Missing destinations without :create keep the message at its default destination. Flags map to the same MDS flags and validated user keywords as IMAP/JMAP. File actions snapshot the current flags; repeated filings into one mailbox use the last flag list. Invalid, unsupported and excess native keywords are ignored. :copy preserves implicit keep. Variables support substitution, wildcard captures, case modifiers, wildcard quoting and length.

body uses decoded MIME text, strips HTML markup for :text, selects MIME types with :content, and exposes the untransformed body with :raw. MIME selectors support variables, and selected binary parts retain their decoded octets. Body wildcards preserve earlier match captures. Relational :value/:count support gt, ge, lt, le, eq, ne; :count requires i;ascii-numeric. Header :index N selects one occurrence, with :last counting from the end. address parses every field named by the script, including custom address headers. ASCII casemap ordering folds letters to uppercase before comparison.

Delivery and policy

SMTP classifies spam, runs the global Mix inbound_filter, then runs account Sieve. This preserves the existing filter's spam inputs. The Mix destination, or classifier Inbox/Junk default, is Sieve's keep destination. The stored message has daemon-generated X-Mixos-Spam-Verdict and X-Mixos-Spam-Score headers; incoming copies are removed. Verdicts are HAM, SPAM or UNKNOWN when classification is disabled or fails. JMAP/native Bus submissions also run recipient Sieve on local delivery, with Inbox as keep, without rerunning the inbound classifier or global Mix filter. These local copies receive fresh UNKNOWN/zero spam headers before the recipient's script runs.

Evaluation produces a complete action plan. Syntax/runtime errors, missing destinations and refused redirects cancel the plan and keep the message. Limits: 64 KiB per script, 10,000 evaluation/matching/allocation steps, 64 filing/redirect actions, 1 MiB of variables and four redirects. Matching charges for scanned bytes as well as instructions. Header parsing is charged once for the header section; attachments do not spend the header-test budget. Body transforms charge only selected parts. Filing and mailboxexists read at most 10,000 metadata records and 1 MiB of names/attributes, omitting message counts. Their path processing also has a 10,000-visit, 1 MiB budget; exhaustion keeps the message. Local redirects run the destination's script; SMTP redirects also rerun the destination's classifier and global filter using the original verified ingress. Local hop counts stay out of band; remote loops are bounded by this host's own Received trace lines. A hop/action limit drops only excess redirects, retaining other actions and restoring implicit keep when the suppressed redirect was the only delivery action. External SMTP ingress strips X-Mixos-Sieve-Hop; that field never supplies a trusted hop count. Off-site redirects add a Received trace and strip the private hop marker and all X-Mixos-Spam-* fields, including folded continuations.

Once a filing or redirect has committed, later secondary-action I/O failures are logged and the accepted delivery is retained. This follows maild's existing partial-delivery behaviour and avoids retrying an already accepted copy. Secondary actions have no durable replay across the separate MDS/mail.db stores.

reject/ereject return SMTP 550 5.7.1 after DATA only for one normal RCPT that resolves to one mailbox; these refusals appear in the SMTP rejection log. An alias with multiple local targets, mixed recipients or asynchronous delivery instead queues a DSN for the rejecting target with a null reverse path. DSNs to null/unverified senders are suppressed to prevent backscatter. Discard is compatible with reject. Combining reject with filing, redirect or vacation is a runtime error and keeps the original message.

Off-site redirects default to refused. sieve_external_domains lists exact account domains allowed to redirect to non-local addresses. Vacation replies to external senders are independently allowed by default; set sieve_vacation_external: false to suppress them. These are operator configuration, never script data. Local destinations must be account addresses.

Vacation supports :days (1..31, default 7), :addresses, :subject, :from :handle and :mime entities. :from must be the account address. :addresses recognises additional recipient identities; it does not authorise sender spoofing. Recognised identities include the account and final envelope recipient, and must appear in To/Cc/Bcc or Resent-To/Resent-Cc/Resent-Bcc. Replies require verified ingress. Spam, null senders, self-mail, list/bulk mail and automatic responses receive no reply. A sender/response-handle stamp and its native queue row commit in one mail.db transaction, permitting one reply per interval across concurrent deliveries and restarts. Handles are stored as fixed-size hashes. Tracking retains at most 10,000 pairs per account and drops stamps past the maximum 31-day interval. At capacity, new pairs receive no automatic reply until space expires; live suppression records are preserved so existing senders cannot receive repeats. Replies use a null reverse path, Auto-Submitted: auto-replied, and the native outbound queue. Its worker selects the account domain's DKIM signer; absent signing configuration retains the existing unsigned behaviour. An active Sieve script takes ownership of automatic responses; the older JMAP VacationResponse runs only without an active Sieve script, preventing two automatic replies to the same message. Without :subject, replies use Auto: followed by the decoded original subject, or Automated reply if it is absent. Non-ASCII subjects are emitted as encoded words; ASCII subjects remain plain text.

ManageSieve

Disabled by default. In the maild configuration:

sieve_listen: ["192.0.2.10:4190"]
sieve_implicit_tls: false
sieve_external_domains: []
sieve_vacation_external: true

false selects RFC 5804 STARTTLS, the normal mode on 4190; true selects implicit TLS. Both share SMTP/IMAP's TLS identities, SNI resolver and hot-reload slot. Enabling the listener without TLS material fails startup. Port 4190 needs no new unit privileges. NodeConfig fallback leaves this optional listener off; set the keys in the maild configuration used by the daemon.

Commands: CAPABILITY, AUTHENTICATE PLAIN/LOGIN, LISTSCRIPTS, GETSCRIPT, PUTSCRIPT, CHECKSCRIPT, SETACTIVE, DELETESCRIPT, RENAMESCRIPT, HAVESPACE, NOOP, LOGOUT. Authentication and script access require TLS. Passwords use the shared account verifier. PLAIN authorisation identity must be empty or equal the login identity. Only the authenticated account's scripts are accessible. Uploads accept quoted strings and non-synchronising octet literals ({N+}). When external vacation replies are disabled, valid PUTSCRIPT and CHECKSCRIPT commands containing a vacation action return OK (WARNINGS) "vacation replies to external senders are disabled"; scripts are still accepted. An oversized PUTSCRIPT returns NO (QUOTA/MAXSIZE) and closes the session without reading its non-synchronising literal. Other malformed/oversized frames close the session. Limits: 128 connections, 180-second pre-authentication and 30-minute authenticated command timeouts, 30-second TLS/authentication and response-write timeouts, three failed logins. STARTTLS discards buffered plaintext commands.

Bus editing and versions

Like maild's existing operator commands, broker callers select an account; ManageSieve instead enforces authenticated account isolation.

CommandRequestResponse
maild.sieve.listaccountscripts: name, version, active, size
maild.sieve.getaccount, namename, version, active, source
maild.sieve.putaccount, name, source, optional expected_versionnew version
maild.sieve.activateaccount, name (empty disables)active name

Account identifiers are email addresses. PUT's optional version guard uses zero for a new script and refuses stale writes. Replacements preserve active state. sieve_scripts stores current source; sieve_versions retains up to 16 revisions per name and 512 per account, including deleted/renamed names. Versions increase across the account and are never reused after deletion. ManageSieve uses the RFC's last-writer-wins editing semantics. Account deletion cascades to all script/version/vacation-stamp rows.

This page as Markdown · This page describes the current main branch of MixOS.