Guard ManageSieve Command

ManageSieve command-line validator (RFC 5804 — "A Protocol for Remotely Managing Sieve Scripts"). Gates every verb the framework's ManageSieve listener accepts from peers — AUTHENTICATE / STARTTLS / LOGOUT / CAPABILITY / HAVESPACE / PUTSCRIPT / LISTSCRIPTS / SETACTIVE / GETSCRIPT / DELETESCRIPT / RENAMESCRIPT / NOOP.

ManageSieve is a line-oriented text protocol that mixes simple single-line commands with literal-syntax payloads ({N} / {N+}) carrying script bytes verbatim. Responses are OK ... / NO ... / BYE ... per RFC 5804 §1.2.

## Smuggling defense — bare-CR / bare-LF refusal

Same wire-protocol concern as SMTP / IMAP / POP3. Command lines MUST be canonical CRLF-terminated; bare-CR or bare-LF inside a command (outside the literal-payload window) is refused. The listener clears the receive buffer at STARTTLS upgrade to defend the same pre-handshake injection class that affected STARTTLS in Exim (CVE-2021-38371) / Dovecot (CVE-2021-33515) / Postfix (CVE-2011-0411).

## Cleartext-AUTH refusal under strict

RFC 5804 §1.1 + RFC 4954 §4 — AUTHENTICATE with credential- bearing mechanisms (PLAIN / LOGIN) over a cleartext channel exposes the password to passive observation. Strict + balanced refuse AUTHENTICATE PLAIN / AUTHENTICATE LOGIN pre-TLS; the listener composes this gate at the validate boundary AND re-checks at the dispatch boundary as defense-in-depth.

AUTHENTICATE EXTERNAL (RFC 4422 §4) is exempt — the credential is the TLS client certificate already presented, so cleartext is not the concern. AUTHENTICATE SCRAM-SHA-256 (RFC 7677) and AUTHENTICATE OAUTHBEARER (RFC 7628) are mechanism-side credential-protected and may run pre-TLS under permissive; strict still requires TLS (defense-in-depth + active-MITM resistance).

## Script-name shape (RFC 5804 §2.1)

Script names are UTF-8 strings of 1-512 octets containing no NUL (0x00), CR (0x0D), LF (0x0A), forward-slash (0x2F), backslash (0x5C), or double-quote (0x22). The forward-slash + backslash refusal blocks path-traversal-style storage-backend collisions; the NUL/CR/LF refusal blocks wire-protocol smuggling.

## Literal syntax (RFC 5804 §2.3 + RFC 7888 LITERAL+)

PUTSCRIPT name {N} / PUTSCRIPT name {N+} introduces an N-byte script payload. The bare {N} form is synchronizing (server replies with a continuation request before the client sends the payload); {N+} (RFC 7888) is non-synchronizing. The validator refuses N values above the per-profile script-byte cap (matching b.safeSieve's maxScriptBytes: 64 KiB strict / 256 KiB balanced / 1 MiB permissive).

## Per-verb shape

RFC 5804 §2.1-§2.10:

- AUTHENTICATE "" [] - STARTTLS — no args - LOGOUT — no args - CAPABILITY — no args - NOOP [string] — optional echo-tag arg - HAVESPACE "" — name + non-negative integer - PUTSCRIPT "" - LISTSCRIPTS — no args - SETACTIVE "" — single script-name arg (empty string deactivates all per §2.8) - GETSCRIPT "" — single script-name arg - DELETESCRIPT "" — single script-name arg - RENAMESCRIPT "" "" — two script-name args

## Caps

- Per-line cap (excluding the literal payload itself): 8 KiB strict / 16 KiB balanced / 64 KiB permissive. ManageSieve's command lines are LONGER than POP3/IMAP because script names may carry UTF-8 + the literal-payload announcement. - Script-byte cap (literal {N} value): same as b.safeSieve.PROFILES..maxScriptBytes — 64 KiB / 256 KiB / 1 MiB. - Script name: RFC 5804 §2.1 1-512 octets.

Throws GuardManageSieveCommandError on every refusal.

b.guardManageSieveCommand.announcedLiteral(line) #

stable0.20.32

Read the literal a command line announces, as { size, nonSync }, or null when the line ends in no literal opener. validate reads the opener this way, and a listener that REFUSES a line needs the same answer to decide what to do with the octets the client is about to send: a non-synchronizing literal ({n+}, RFC 7888) is already in flight, while a synchronizing {n} waits for a continuation the refusal never sends.

The size is reported as announced, with no cap applied, so the caller can refuse a size it will not read rather than read it to find out. A run of digits is an announcement at any length, because leading zeroes spell a small size in as many digits as a sender likes; a value past what validate reads reports size: null beside the form, since the question a refusing caller asks is whether octets are in flight, and an unreadable size is not an absent announcement.

b.guardManageSieveCommand.announcedLiteral("PUTSCRIPT \"x\" {24+}");
// → { size: 24, nonSync: true }

b.guardManageSieveCommand.validate(line, opts?) #

stable0.9.57
{
  profile:   "strict" | "balanced" | "permissive",
  posture:   "hipaa" | "pci-dss" | "gdpr" | "soc2",
  tls:       boolean,    // when false + AUTHENTICATE PLAIN/LOGIN
                           under strict, refuse with
                           `guard-managesieve-command/cleartext-auth`
                           (RFC 4954 §4 + RFC 5804 §1.1)
}

Validate a single ManageSieve command line (without its CRLF terminator, and without the literal-script payload that may follow). Returns a shape describing the parsed verb + arguments + (when applicable) the trailing literal-byte count the listener must read from the wire. Throws GuardManageSieveCommandError on refusal, with the code naming what the line did.

Options: guard-managesieve-command/bad-profile for a profile name outside the table, guard-managesieve-command/bad-posture for a posture name outside it, and guard-managesieve-command/bad-opt for an option this guard does not read, including compliancePosture, whose regime belongs in posture.

Line shape: guard-managesieve-command/bad-input when line is not a string, guard-managesieve-command/empty-line for a zero-length line, guard-managesieve-command/line-too-long past the profile's maxLineBytes, guard-managesieve-command/bad-byte for a forbidden control byte, and guard-managesieve-command/unterminated-string for a quoted string the line never closes.

Verb: guard-managesieve-command/unknown-verb for a verb outside RFC 5804 §2, and guard-managesieve-command/unexpected-args for arguments after a verb that takes none.

Script names: guard-managesieve-command/missing-name, guard-managesieve-command/bad-name for a name that is not a quoted string, guard-managesieve-command/empty-name, guard-managesieve-command/bad-name-byte for a control or NUL byte inside it, and guard-managesieve-command/name-too-long past the profile's name cap.

Literals and payload size: guard-managesieve-command/bad-literal for an announcement that is neither {N} nor {N+}, guard-managesieve-command/literal-plus-refused for LITERAL+ under a profile that does not allow it, guard-managesieve-command/literal-too-large for an oversize AUTHENTICATE initial response, and guard-managesieve-command/script-too-large for a PUTSCRIPT or HAVESPACE size past the profile's script cap.

AUTHENTICATE: guard-managesieve-command/missing-mechanism, guard-managesieve-command/bad-mechanism for a mechanism that is not a quoted string, and guard-managesieve-command/bad-initial-response for an initial response that is neither a {N} / {N+} literal announcement nor a quoted base64 string.

Per-verb argument shape: guard-managesieve-command/bad-putscript, guard-managesieve-command/bad-rename, guard-managesieve-command/bad-havespace and guard-managesieve-command/bad-noop-arg.

var p = b.guardManageSieveCommand.validate('PUTSCRIPT "myscript" {52+}', { tls: true });
// → { verb: "PUTSCRIPT", args: ["myscript"], literalBytes: 52, literalPlus: true }

var c = b.guardManageSieveCommand.validate("CAPABILITY", { tls: true });
// → { verb: "CAPABILITY", args: [] }

b.guardManageSieveCommand.parseQuotedString(s) #

stable0.18.54

Read a leading RFC 5804 §1.2 quoted string off s and return { value, rest }, where value is the unescaped content and rest is what follows with leading whitespace removed. Returns null when s does not begin with a double quote.

The production honors the \" and \\ escapes, and refuses NUL, CR and LF inside the quotes: those end a line-oriented protocol record, so a string carrying one would split the command it appears in.

Exposed because a string also arrives outside a command, as the client's reply to a SASL challenge. That line never reaches validate, and a second hand-rolled unquoting there would be free to lose the escape handling and the control-byte refusal this one has. b.mail.server.managesieve reads its SASL responses through this.

b.guardManageSieveCommand.parseQuotedString('"PLAIN" {12+}');
// → { value: "PLAIN", rest: "{12+}" }
b.guardManageSieveCommand.parseQuotedString("PLAIN");
// → null

b.guardManageSieveCommand.compliancePosture(name) #

stable0.9.57hipaapci-dssgdprsoc2

Return the effective profile NAME for a compliance posture, or null for a name this parser does not map. Unlike the content-guard variant this returns the resolved profile string (every line-protocol parser composes gateContract.ALL_STRICT_POSTURES, so "hipaa" / "pci-dss" / "gdpr" / "soc2" all resolve to "strict") and never throws — the parser shape carries no overlay-clone, no buildProfile, and no loadRulePack. Wired by gateContract.defineParser.

b.guardManageSieveCommand.compliancePosture("hipaa");                   // → "strict"
b.guardManageSieveCommand.compliancePosture("not-a-regime");            // → null

Last updated 2026-10-12T14:21:56.879Z by seeder.