Sieve interpreter
RFC 5228 Sieve interpreter that walks the AST produced by b.safeSieve.parse(script) and emits an ordered action list the delivery agent applies to the inbound message. Runs under a gas counter (default 10 000 ops) so a hostile or runaway script can't stall the delivery thread. The interpreter is synchronous and pure — every test reads from the operator-supplied env object; the interpreter never touches the mail store, never opens a socket, never executes operator-supplied code. Side-effects (file- into / redirect / discard) materialize as result entries the caller dispatches against the store.
Tests implemented at v0.9.55: - address — header-address-list test with :all / :localpart / :domain address-parts and :is / :contains / :matches match-types - header — header-value test with the same match-types - envelope — RFC 5228 §5.4; reads env.envelope.{from,to} - exists — header-presence test - size — :over N / :under N byte-count test - not / allof / anyof / true / false
Actions implemented at v0.9.55: - keep — implicit default per §2.10.2 - fileinto "Folder" — RFC 5228 §4.1 - discard — RFC 5228 §4.4 - redirect "addr" — RFC 5228 §4.2; tagged with the address - stop — RFC 5228 §3.2; halts further command execution
Comparators: i;octet (default, exact byte) + i;ascii-casemap (case-insensitive ASCII). Other comparators refused at script parse time (require 'comparator-NAME' not in KNOWN_CAPABILITIES).
Match-type wildcards: :matches uses * (any sequence) and ? (one byte), per RFC 5228 §2.7.1. Both wildcards are converted to a bounded RegExp built from escaped literal byte segments — no user-controlled backtracking surface.
The interpreter does NOT execute multi-script chains, sieve includes (RFC 6609), notify actions (RFC 5435), or vacation responses (RFC 5230); each of those will land with the corresponding extension RFC slice. Until then, scripts declaring them via require are refused at parse time.
b.mail.sieve.run(ast, env, opts?) #
{
maxGas: number, // default 10000; cap 1_000_000
}
Walk a parsed Sieve AST against the message environment + return the ordered action list. The interpreter is pure — it reads only from env and never mutates it; every action surfaces as an entry in the returned list for the caller to dispatch.
var ast = b.safeSieve.parse('if header :contains "X-Spam" "yes" { fileinto "Junk"; }');
var rv = b.mail.sieve.run(ast, {
headers: [{ name: "X-Spam", value: "yes" }],
envelope: { from: "sender@example.com", to: "rcpt@example.com" },
sizeBytes: 1024,
});
// → { actions: [{ kind: "fileinto", folder: "Junk" }, { kind: "keep" }], gas: 3, stopped: false }
b.mail.sieve.runScript(script, env, opts?) #
{
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
maxGas: number,
}
Parse + run in one call. Most call sites — JMAP SieveScript/validate, MX delivery hook — want this shape.
var rv = b.mail.sieve.runScript(
'require ["fileinto"];\nif header :is "From" "boss@x.com" { fileinto "Important"; }',
{ headers: [{ name: "From", value: "boss@x.com" }] }
);
rv.actions[0].folder; // → "Important"
b.mail.sieve.create(opts?) #
{
maxGas: number,
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
audit: { safeEmit: function },
}
Returns a stateful Sieve handle the delivery agent + JMAP SieveScript/validate method compose. Distinct from the bare b.mail.sieve.run(ast, env) entry — the handle carries operator- supplied opts (maxGas, profile, compliancePosture, audit) so every invocation runs with the same posture.
var sieve = b.mail.sieve.create({ profile: "strict", audit: b.audit });
sieve.validateScript(operatorScript);
var rv = await sieve.runScript(operatorScript, mailEnv);
Last updated 2026-08-08T16:39:49.652Z by seeder.