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 "PUTSCRIPT "LISTSCRIPTS — no args - SETACTIVE "GETSCRIPT "DELETESCRIPT "RENAMESCRIPT "
## 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. — 64 KiB / 256 KiB / 1 MiB. - Script name: RFC 5804 §2.1 1-512 octets.
Throws GuardManageSieveCommandError on every refusal.
b.guardManageSieveCommand.announcedLiteral(line) #
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?) #
{
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) #
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) #
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