Modern API 1.0: full IMAP4rev1/rev2 client with per-requirement compliance measurement (M0–M6) - #18
Modern API 1.0: full IMAP4rev1/rev2 client with per-requirement compliance measurement (M0–M6)#18LoveAndCoding wants to merge 138 commits into
Conversation
- Fix newline.transform test asserting undefined where modern Node's stream end() callback delivers null - Remove useless regex/string escapes (verified char-for-char regex equivalence by codepoint sweep for every touched character class) - Targeted eslint-disable comments for intentional control-char regexes and the typed-emitter declaration-merging pattern - Scope case-block declarations, drop dead assignments, drop unused eslint-disable directives No behavior changes. npm test 232/232, typecheck clean, lint 0 errors, compliance suite pass/fail totals byte-identical to pre-change baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Add src/connection/tls.ts as the single TLS policy module: every TLS socket (implicit connect and STARTTLS upgrade) is created via openTls(), which always sets servername from the configured host, keeps Node's default checkServerIdentity and rejectUnauthorized:true, and rejects caller tlsOptions that try to override identity-critical keys (RangeError). Handshake/identity failures now destroy the socket and reject with a typed TLSSocketError (reason: identity-mismatch/handshake/ policy) instead of resolving false or hanging. Also fixes the starttls() entry guard that checked this.connected before connect() ever sets it, which made the upgrade path unreachable. Compliance: 20 requirement-rows flip violation→pass (RFC9525-6.6-1, RFC7817-3-1/-3-7/A-1, RFC2595-2.4-1/-2.4-8, RFC8314-3.2-1/-5.3-1, RFC3501-11.1-3/-4/-7, RFC9051-11.1-6); violations 118→98, zero regressions. RFC3501-11.1-8 stays annotated: it additionally requires the M0.2 STARTTLS choreography. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…4, I-1, I-2) - CommandQueue.hold()/release(): while held no command anywhere in the queue may write bytes; contexts consult the live held flag at dispatch time so a context promoted to active mid-hold still withholds writes. Used to guarantee nothing is written between the STARTTLS tagged OK and TLS handshake completion. - CapabilityRegistry precursor on Connection: invalidated on socket close and on STARTTLS handshake success; repopulated by a post-TLS CAPABILITY round trip before connect() resolves. Pre-TLS capability data is only read locally to decide whether to attempt STARTTLS and never exposed as current. - Session.start() reuses the registry when valid instead of issuing a redundant second CAPABILITY round trip. - Fix strict-STARTTLS connect() always throwing: the post-upgrade check read this.isSecure, which is gated on this.connected and is never set at that point; check the local upgrade outcome instead. - RFC3501-11.1-8 script: a compliant client aborts the handshake on a wrong-host cert, so the server-side TLS wrap can never complete; ended the script at the STARTTLS reply + destroy (documented in-test). Compliance: 24 rows flip violation→pass (RFC3501-6.2.1-1/-2/-3, RFC9051-6.2.1-1/-2/-3, RFC9051-6.1.1-1, RFC2595-2.5-1/-3.1-1/-2/-3/-9-3, RFC9051-11.1-7/-11.2-1, RFC8314-5.1-7/-5.2-4, RFC3501-11.1-8); violations 98→74, zero regressions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…arisons (spec §11.1, I-5) - src/lexer/case-insensitive.ts: ciEquals/ciIncludes/ciCanonicalize/ ciCanonicalFrom; placed in the lexer layer because lexer rules need it too and parser already depends on lexer - matchesFormat atom comparison now ci (covers ~20 dispatch sites); status dispatch (OK/NO/BAD/PREAUTH/BYE) stores canonical uppercase; capability map keys canonicalized; FlagList stores/looks up canonical keys while Flag.name keeps original casing for display; LIST/LSUB, SEARCH/ESEARCH+MODSEQ, resp-code kinds, untagged response type all canonicalized; NIL matching in the lexer now ci - Fixes a real X-GM-THRID/X-GM-MSGID misclassification for lowercase input and an exact-case QUOTA dispatch failure Compliance: RFC3501-9-2, RFC9051-9-2, RFC9208-7-1 flip violation→pass, plus RFC9585-5-1 (lowercase inprogress resp-code) as a side effect of resp-code kind canonicalization. Violations -6, zero regressions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…i-implementation-x3r57j
…pec §10.5, §10.6, I-7, I-8)
- awaitGreeting() extracted and used on every connect path: OK proceeds,
BYE rejects with the server text, timeout rejects; PREAUTH marks the
connection authenticated (exposed to Session) and, when the config
demands STARTTLS on a cleartext socket, closes immediately and rejects
with a policy error (§10.5 — PREAUTH forecloses STARTTLS-before-auth)
- ALERT (I-7): always logged at warn with the alert text; on a
non-confidential transport the log detail carries
{ code: "ALERT", trusted: false } and the response is not emitted as
a serverStatus event; on a confidential transport it is logged and
emitted normally
- Connection config gains an optional logger (driver.connectLow paths
now produce observable logs)
Adjudications (docs/compliance-adjudications.md, new):
- RFC9051-7.1-1 (SHOULD-ignore unprotected ALERT): deliberate deviation
mandated by spec I-7 — display-with-untrusted-marking; incompatible
with RFC3501-7.1-1 (rev1 MUST-present) and RFC9051-7.1-2 (MUST-mark).
Annotated as a permanent expected violation.
- RFC9051-7.1.4-2 test was mis-scripted (security:"none" cannot satisfy
the catalog's 'clients configured to require mandatory TLS' condition);
corrected to security:"starttls", now passes against the §10.5 policy.
- RFC9051-11.3-1/-3 (pre-auth LIST / non-selected EXISTS suppression):
blocked-on-surface until M1's router splits Layer-1 events from
client-level state effects — 10+ currently-passing acceptance rows
assert the same events ARE surfaced on the same observation channel.
Deferred to M1, annotations left in place.
Compliance: 7 rows flip violation→pass (RFC3501-7.1-1, RFC3501-7.1.4-1,
RFC9051-7.1-2/-3, RFC9051-7.1.4-1/-2, RFC9051-11.3-2); RFC9051-7.1-1
becomes a deliberate violation; net 273 pass / 62 violation, problems 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…pec §11.2, I-6)
- UntaggedResponse: per-matcher try/catch + UnknownContent fallback —
unknown or malformed-but-framed untagged responses become tolerated
data; the Transform stream never dies on valid IMAP framing
- VanishedResponse: real parse for VANISHED (EARLIER) and bare VANISHED
- AtomTextCode: bare (unparenthesized) resp-code arguments preserved in
wire order (REFERRAL, UNDEFINED-FILTER, NOUPDATE, MAXCONVERT*,
BADCOMPARATOR)
- ESearchReturnData: ordered multi-entry map — repeated ESEARCH return
items (ADDTO ADDTO) no longer clobber
- MailboxListing: RFC 5258 extended data (OLDNAME) captured raw instead
of throwing past the mailbox name
- ThreadMessage.children: fix self-recursive getter (RangeError)
- BODYSTRUCTURE: fix parseDisposition token-count check; extension data
accepts bare NILs per §7.4.2
- SortResponse: tolerate trailing (MODSEQ n); lexer accepts literal8
~{n} with NUL-safe payloads
Compliance: 48 rows flip violation→pass across RFC 7162/5255/5256/5259/
5267/5465/5466/4467/5524/2193/2221. Zero regressions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…i-implementation-x3r57j
The tolerance batch switched AtomTextCode to bare-list splitting, which
preserved REFERRAL/UNDEFINED-FILTER/NOUPDATE-style bare arguments but
broke the parenthesized tuple forms (INPROGRESS, MAILBOXID, BADEVENT):
paren tokens leaked into contents and nested grouping stopped working,
regressing 8 requirement-rows that the batch's totals-only verification
masked. Pick the split mode by the leading token: '(' means the tuple
form with the original paren-stripping delimiters; anything else is a
bare argument list.
Merged result: 329 pass / 6 violations (remaining: RFC2971-3.3-2 (M0.7),
RFC9051-D-1 (M0.6), RFC9051-11.3-1/-3 (deferred to M1 router),
RFC9051-7.1-1 (adjudicated deviation)); zero regressions vs every prior
snapshot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
IdCommand validates at the command boundary and throws RangeError on input exceeding RFC 2971 §3.3's MUST NOT limits (>30 pairs, field >30 octets, value >1024 octets). Session sanitizes consumer-supplied ID config instead of rejecting it — over-long field names are dropped (a truncated field name is a different field), over-long values are truncated at a UTF-8 code-point boundary to 1024 octets — so the ID exchange still happens with a compliant command. Compliance: RFC2971-3.3-2 flips violation→pass on both profiles (verified via targeted spec run; full-suite snapshot lands with the milestone close). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…3, I-10) The lexer already promotes values above 2^32 to BigIntToken, but the RFC822.SIZE and QUOTA matchers only accepted number tokens, so any 63-bit size failed the msg-att parse and the response was dropped. Widen size/usage/limit to number | bigint with the same alternation pattern MODSEQ already uses. Values within 32-bit range still surface as plain number; UID/UIDVALIDITY/UIDNEXT keep their strict 2^32-1 bound. Flips RFC9051-D-1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
- SORT/SEARCH (MODSEQ n) with zero preceding ids parsed via a negative slice index and threw/mis-parsed on grammatically valid input; read the MODSEQ value positionally and guard the id-list slice (empty list + modseq value now round-trip) - Capability classification (standard/extension/unknown, AUTH= scheme collection) now canonicalizes before comparing; display casing kept - NilRule gained an atom-continuation boundary check so atoms beginning with a case-insensitive 'nil' prefix (NILVANA, Nilsson) tokenize as atoms, not NIL + fragment - IdCommand: document the deliberate synchronous RangeError contract; truncateToOctets distinguishes a mid-code-point cut from a value that legitimately ends in U+FFFD by re-encoding; quota.ts comment cites RFC 9051 Appendix D / RFC 9208 correctly Findings 008/009/010/013/014d/014e from the M0 review; regression tests added for each. Targeted compliance runs byte-identical to pre-fix baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHssyTzswqsW
- Reset preauthed at the start of every connect() attempt and in the new teardownFailedConnect() helper used by every failure path — a rejected cleartext-PREAUTH attempt could previously leave the flag set, silently skipping STARTTLS on a retried connect() (a server-triggerable plaintext downgrade that also misreported authenticated state) - Attach a transient socket 'error' handler the moment a socket exists in every connect phase (raw, implicit TLS, post-upgrade), raced against every hazardous await; previously most of connect() had no error listener, so an ordinary connection failure could crash the process or hang forever. Queue stop() also resets the held flag so a socket death mid-STARTTLS-hold cannot park commands forever - Fire greeting detection from an internal pre-suppression signal so a legal '* OK [ALERT] ...' greeting no longer hangs connect() until timeout (consumer-facing pre-confidentiality ALERT suppression unchanged) - Opportunistic STARTTLS: capability-absent now continues on plaintext per spec §3.3 instead of tearing down; strict mode still fails - Swap the upgraded socket in before releasing the queue hold so a command parked during the hold cannot write to the pre-upgrade socket - Discard buffered partial-line pipeline state at the STARTTLS tagged OK (plaintext-residue defense). Residual: a complete injected line in the same TCP segment as the OK is parsed before the upgrade code can intervene; closing that requires pipeline-level changes, tracked for the M1 router work - Session.start() failure path resets authed/capabilityList/serverInfo and awaits disconnect() Findings 001-004, 006, 007 (partial, documented), 011, 014a-c from the M0 review; every fix carries a regression test verified to fail with the fix reverted. 355/355 unit+integration tests green; targeted compliance runs byte-identical to baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…8 discrimination
Security fix (review finding 012, confirmed empirically): Node's
built-in checkServerIdentity falls back to the Subject CN when a
certificate carries no subjectAltName, so a SAN-less CN-matching
certificate was accepted — RFC 9525 §6.6 forbids CN as an identity
source. openTls now wraps checkServerIdentity with a SAN-presence
precondition (rejects as identity-mismatch) before delegating to the
built-in matcher. New cn-only cert fixture + empirical unit test
witness the behavior; spec §10.1 amended to describe the wrapper
mechanism instead of the incorrect 'relies on Node' claim.
Harness fidelity (review finding 005): ScriptedServer.doStartTls now
settles on a client-side handshake abort ('close' without 'secure' on
either socket) instead of hanging, with an expectAbort option; the
RFC3501-11.1-8 test again performs a REAL wrong-host-cert handshake the
client must abort — a client skipping identity verification would
complete the handshake and fail the script, restoring the row's
discriminating power. Stale 'session notes' citation removed.
Full suite after all review fixes: 356 unit/integration green;
compliance 332 pass / 3 violations (RFC9051-7.1-1 adjudicated,
RFC9051-11.3-1/-3 deferred to M1 router) / 0 problems.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Baseline 118 violation rows → 3 (all itemized: one adjudicated SHOULD deviation, two blocked-on-surface rows scheduled for M1's router). Exit criteria met; phase-boundary review completed with all confirmed findings fixed (including one empirically confirmed security gap in Node's default identity check that the review had flagged as speculative). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
- ClientStateMachine: §3.1 transition table encoded as data, single writer, synchronous onTransition (stateChange-before-settle contract), assertIn for command legal-state checks; any→logout and any→disconnected as rules; disconnected→disconnected a tolerated no-op - CapabilityRegistry: ingests parsed CapabilityList or string iterables, canonical storage, AUTH= mechanism parsing, live stable-identity CapabilityView, epoch bumps on every set AND invalidate, onChange hook for capabilitiesChanged 60 unit tests covering every legal edge, illegal-edge rejection, ordering, epochs, ci semantics, both ingestion forms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
ImapError base with standard cause options; ConnectionError (phase, bye), TlsError (reason, certificate), ProtocolError (bytes, context), CommandError (command/tag/status/code/text) with ServerNoError and ServerBadError enforcing their status at type and runtime level, AuthError (mechanismsTried, code), CapabilityError (capability, rfc), StateError (state, required). Legacy error classes untouched — they become wrapped internal causes as each subsystem is ported. src/protocol/response-codes.ts holds the minimal TypedResponseCode fallback shape; the full §5.5 discriminated registry lands with the router. Note: cause is declared with 'declare readonly' — under ES2022 class fields a plain field initializer would wipe the value Error's constructor sets from options.cause. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
… (spec §7.2)
All value emission goes through validated primitives (no raw escape
hatch): atom (strict ATOM-CHAR, throws), astring three-way
atom/quoted/literal decision with byte-length literals, quoted escaping,
CR/LF and 8-bit content routed to literal form (atom alone throws —
bare atoms have no escape mechanism), literal8 ~{N}, LITERAL+/LITERAL−
non-sync selection with the 4096-octet LITERAL− ceiling, INBOX
canonicalization with an mUTF-7 codec hook stubbed until M2,
RFC 3501 date/date-time rendering in UTC, validated flag lists,
nested parenthesized groups with correct spacing, and per-call
atomicity (a throwing call rewinds all partial output).
Output is WireSegment[] — segment boundaries carry awaitContinuation
for the queue's literal gate (§6.2). 83 unit tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
….2 part 1) Mechanism interface exchanges raw bytes only — the AUTHENTICATE command (landing with the client shell) owns base64 framing, SASL-IR, empty responses and * cancellation. Factory-based registry so per-attempt mechanism state never leaks across attempts. - PLAIN (RFC 4616): NUL-framed authzid/authcid/passwd, embedded-NUL rejection, exact RFC example vector tested - OAUTHBEARER (RFC 7628): GS2 header with RFC 5801 saslname escaping (=3D before =2C, order matters), kvsep grammar verified against the RFC's worked example (an initially missing post-header kvsep was caught by the byte-exact test), JSON error-challenge → 0x01 dummy → AuthError with status/scope - XOAUTH2: user/auth kvpair form, error-challenge → empty response → AuthError All three requiresSecureTransport: true (§10.3 policy hook). 32 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…s, literal gate (spec §6, §7.1, §7.3, §8) - Command<TResult>: verb/states/capability/queueMode with protected write/claims/accept/onContinuation/onError hooks; tag assigned by the queue at write time via a static driving surface so subclasses keep encapsulation; one-submission semantics - ResponseCollector with untagged/first/tagged/codes and a conservative TypedResponseCode renderer; FETCH streaming bridge left as a documented seam for M3 - Router owns the tag map, ordered claimants and the single continuation-owner slot (double registration throws); the M0 ALERT/hygiene handling re-homes here as the state-tracker lane; routing is additive (claim attribution never filters the public Connection events - the choice that keeps M0 behavior provably identical) - executeCommand(): the only writer of command bytes besides CommandWriter; implements the §6.2 literal gate (withhold all other writes until + or tagged NO/BAD abort) composing independently with the STARTTLS hold()/release() - Queue: pipeline/serial/isolated modes replace requiresOwnContext; cancellation rejects with ConnectionError(phase:steady) — the old string reject is gone; per-connection tag generator - CAPABILITY/NOOP/ID/STARTTLS ported; ID keeps its RFC 2971 limits; commands/encoding.ts deleted (no importers remain) 615/615 unit+integration tests; full compliance per-row byte-identical to the M0 snapshot (332/3/819/427, problems []), verified twice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…gout/close (spec §2, §3.2, §3.3) - ImapClientConfig validation (synchronous TypeError/RangeError, deep copy, §2 default resolution) in src/client/config.ts - ImapClient over the M0-hardened Connection: connect() ritual steps 1-4 (+ID in any state per RFC 2971; auth is a NotImplementedError seam filled by the next task), state machine wiring with stateChange-before-settle, client-owned CapabilityRegistry fed by bridging Connection's public events, error mapping into the §4 hierarchy, alert/unhandled/close/error events, run() enforcing command states (StateError) and capabilities (CapabilityError) with zero bytes written, LOGOUT command + drain choreography (BYE normal in the logout window), idempotent logout, close() teardown - Fixes two dormant pre-existing parser bugs the shell exposed: the resp-text-code dispatcher matched 'CAPABILITIES' instead of the wire keyword CAPABILITY, and CapabilityTextCode parsed its unparenthesized payload with the parenthesized default, yielding an empty set 671/671 unit+integration; compliance per-row identical to the M0 snapshot (332/3/819/427, problems []). Session stays until the M1.9 driver rewire so the suite is green at every commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…3, §10.3)
- AuthenticateCommand (isolated): owns base64 framing both directions,
SASL-IR initial response inline iff advertised ('=' for empty, RFC
4959), deferred initial response on first continuation otherwise,
'*' cancellation when a mechanism throws mid-exchange, tagged NO/BAD
mapped to AuthError with the typed resp-code and mechanismsTried
- LoginCommand (serial): astring credentials through CommandWriter
(8-bit passwords take the literal path through the M1.4 gate)
- Selection (§9.3): cleartext credential gate first (TlsError policy,
zero bytes, §10.3), preference order from config or defaults,
unadvertised/unregistered/transport-filtered candidates recorded
with exclusion reasons, AUTHENTICATIONFAILED never falls through,
negotiation failures do, LOGIN fallback gated on LOGINDISABLED,
post-auth capability refresh honoring a tagged-OK [CAPABILITY] code
via registry epochs
- ImapClient.authenticate() + connect() step 5 wired
- Continuation-path fixes surfaced by first real use: ContinueResponse
no longer guess-decodes base64 (uppercase-only regex made it
nondeterministic; raw text now, AUTHENTICATE decodes), executeCommand
catches hook rejections as '*' aborts instead of hanging, accept()
may return a promise so mechanism.finish() can reject after tagged OK
(SCRAM server-signature seam)
692/692 unit+integration; compliance per-row identical to the M0
snapshot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…4, RFC 5161) EnabledResponse parser structure (canonical upper-case, empty list valid); EnableCommand (serial — state-changing; authenticated state only; RangeError on empty request; tolerant empty accept); ImapClient.enableExtensions() filtering to advertised capabilities (zero survivors → zero bytes), accumulative client.enabled; connect() step 6 auto-ENABLE with the M1 understood-enable set (UTF8=ACCEPT; QRESYNC/CONDSTORE/UIDONLY join in their milestones, IMAP4rev2 excluded per §3.4). 713/713 unit+integration; compliance per-row identical to the M0 snapshot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…t surface - Compliance driver connect() drives ImapClient (config mapping, allowInsecureAuth:true as a documented harness affordance — the §10.3 policy itself is exercised by the RFC 8314 rows); login/ authenticate/logout/noop/enable stubs wired to client calls with zero protocol logic; connectLow stays Layer 1 - src/session.ts deleted; src/index.ts exports per spec §1.1 (ImapClient, client types, §4 error hierarchy, Connection, parser output classes; no lexer, no Parser stream); package.json exports map for '.', './commands', './sasl' - sessionPrelude sends a bare greeting so its scripted CAPABILITY round trip is always the one consumed; login/auth exchanges can fold [CAPABILITY] codes into the tagged OK for the §3.3 step-5 refresh - enableExtensions treats UTF8=ONLY as advertising UTF8=ACCEPT (RFC 6855 §3: the required reaction is ENABLE UTF8=ACCEPT) Unit/integration 708/708 green. Compliance sweep and remaining script updates land in part 2 (this commit alone leaves known script stalls in rev2 greeting-code scenarios — tracked, in progress). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
62 expectFailure annotations removed across 27 spec files for tests that now genuinely pass with login/authenticate/logout/noop/enable wired (NOOP, LOGIN/LOGINDISABLED, AUTHENTICATE families, SASL-IR, OAUTH, ENABLE, IDLE prohibition rows, credential-policy rows, and capability-gated prohibition witnesses). Adjacent 'not yet implemented' comments corrected only where the verb is now real. problems: [] reproducible across 4 consecutive full runs; totals 440 pass / 32 violation / 682 unimplemented / 427 untestable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Per-milestone plan doc per the repo convention: mailbox-name codec first (mUTF-7/UTF-8 in src/protocol/, writer stub becomes delegator), SELECT/EXAMINE + MailboxSession skeleton, CREATE/DELETE/RENAME/ SUBSCRIBE/UNSUBSCRIBE, unified LIST with RFC 5258 combination rules, LSUB, STATUS, NAMESPACE, APPEND single, RFC 3691 catalog extraction (parallel suite growth), UNSELECT/CLOSE, milestone close. Notes which verbs need freshly authored compliance specs (DELETE/SUBSCRIBE/ UNSUBSCRIBE have zero coverage today), the client.ts merge-order bottleneck, and the PERMANENTFLAGS null-vs-assume-all reading. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
- Greeting-code stalls: scripts that greeted with a [CAPABILITY] code then expected a CAPABILITY round trip the client rightly skips (§3.3) now either greet bare (when the test needs specific capabilities) or witness greeting-code consumption via driver.hasCapability (when greeting handling IS the requirement) - Missing-connect tests (LOGIN-credentials rows) connect first; SELECT/RENAME-dependent rows restored to clean unimplemented classification - RFC4422-3.4/3.5 abort test rewritten to a genuine two-challenge scenario: PLAIN answers the first (respond leg), a surplus second challenge forces step() to throw → real '*' abort (abort leg) - RFC9051-A-1 adjudicated (docs/compliance-adjudications.md): spec §3.4 permanently excludes IMAP4rev2 from auto-ENABLE; test asserts the positive requirement and is annotated as a deliberate violation - RFC9051-11.3-1/-3 (M0-deferred) now genuinely PASS: retargeted to the ImapClient surface; the unhandled diagnostics channel is I-6 tolerance, not a failure to ignore; capabilitiesChanged count asserted stable at exactly 1 467 pass / 2 violations (both adjudicated) / problems []; 708/708 unit+integration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…ncel scripts
EXTERNAL (RFC 4422 App A): client-first single pair, authzid initial
response ('=' zero-length wire form via the existing SASL-IR path),
opt-in only. CRAM-MD5 (RFC 2195): server-first (no IR argument),
keyed-MD5 digest verified against the RFC's worked vector,
requiresSecureTransport false (the password never crosses the wire).
Cancel tests rescripted to a scenario a §9.3-compliant client can
produce: PLAIN + surplus second challenge → step() throws → bare '*'
→ tagged BAD [AUTHENTICATIONFAILED]. Ripple fixes in the EXTERNAL/
CRAM-MD5/SASL-IR spec files (missing [CAPABILITY] fold-ins, a latent
double-continuation script bug, driver identity constants).
725/725 unit+integration. Compliance: 486 pass / 2 adjudicated
violations / problems [], twice, zero deltas between runs. M1 exit
families: both cores §6.1-6.2, RFC4616/4959/7628/XOAUTH2 at 100%;
RFC4422 at 70% with all 6 remaining rows security-layer-only
(no roadmap mechanism negotiates one); RFC5161 SELECT-gated row → M2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
486 pass / 2 adjudicated violations / problems []. All M1-attainable exit families at 100%; RFC4422 security-layer rows itemized as vacuous-by-design (M6 adjudication candidates); RFC5161's remaining row is SELECT-gated (M2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…e test covers §1.1 subpaths Four requirements extracted with verbatim-quote verification and per-entry profile adjudication (rev2 semantics scored via the existing RFC9051-6.4.2-1 row per the RFC 5161 precedent; capability gate is rev1-only per the RFC 4959-3-3 precedent). Registry coverage flips UNSELECT out-of-scope→cataloged (tallies 74/8); new specs/ext/unselect-3691.test.ts with two real-signal passes today and unimplemented annotations awaiting the M2.13 verb. The import-hygiene meta test now allows exactly the spec §1.1 public entry points (., ./commands, ./sasl) instead of src/index only — the driver's src/sasl import is the public subpath surface, not a deep import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
typedoc 0.28.20 pinned exact; npm run docs generates docs/api/ (gitignored — on-demand, rationale in typedoc.jsonc). Entry points are exactly the package exports map (., ./commands, ./sasl) with entryPointStrategy resolve, so parser/lexer/router internals stay undocumented as a deliberate 1.0 surface statement. ~990 missing-docs warnings fixed with real TSDoc summaries across ~83 src files; 3 broken links de-linked; 29 internal shape types listed intentionallyNotExported with per-symbol comments; Connection.router/.capabilityRegistry tagged @internal; one type-only restructuring (IdCommandValues mapped-type → equivalent explicit object literal so RFC 2971 fields carry per-key docs — byte-identical resulting type). The M2.2 permanentFlags null-vs-implied and M5.15 UIDONLY polarity-inversion judgment calls verified readable in generated HTML. Flagged for a future exports pass: MailboxUpdate/MailboxUpdatesOptions/MailboxSessionDriver/LiveUpdatesMode appear in updates()'s signature but aren't re-exported from the root. npm run docs: 0 warnings. Tests 1936 pass, typecheck 0 (incl. docs samples), eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Full-document rewrite keeping M3.11's already-correct examples: what-it-is → install → quickstart → feature tour (client/session verbs, search, fetch/streaming, flags, copy/move, append, seq facet, events, updates()/idle(), the four facets, compression, UNAUTHENTICATE, UIDONLY, TLS/auth config, errors, connection escape hatch) → compliance story (matrix cited by command/link only — M6.7 fills no numbers here) → MIGRATION.md pointer → API docs pointer. Deleted the ~750-line stale node-imap Legacy API section (superseded by docs/MIGRATION.md) and the false Roadmap claiming IDLE/CONDSTORE/NOTIFY/UIDONLY/COMPRESS were unimplemented. Project-identity block kept (badge, Node >=22, MIT, node-imap attribution). All 18 samples compile-gated via test/docs/readme-samples.ts (gate proven live by deliberate-error injection). 1194 → 645 lines. Tests 1936 pass, typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
package.json → 1.0.0 with the new identity description; CHANGELOG.md (Keep-a-Changelog: the M0–M6 arc, the compliance-measurement origin, and an honest breaking-changes-vs-0.9 section pointing at docs/MIGRATION.md). Build verified: exports map matches dist/, npm pack includes README/MIGRATION/dist and excludes test/. Final measurement: 1936 unit tests; THREE more consecutive compliance runs byte-identical across all 1597 rows (nine consecutive total incl. M6.2's six runs) at 1138 pass / 6 adjudicated violations / 2 adjudicated-unimplemented / 451 untestable / problems []. Snapshot + NOTES.md (exit-target table, task ledger, adjudication tally, post-1.0 carry-forwards) at docs/compliance-history/M6/. test:e2e excluded per standing directive (no real-server credentials in this environment). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Three lenses over the whole M0–M6 effort. Invariants+scope: all 13 spec §12 invariants verified single-chokepoint-enforced with pinning tests; every §13 non-goal confirmed absent. Security: TLS option enforcement, dual credential chokepoints, constant-time SCRAM verification with terminal no-fallback, log hygiene, injection-proof CommandWriter, bounded literals — clean. Docs+ledger: ledger complete both directions, README quickstart RUNS verbatim against a scripted server, typedoc matches the three public entry points, all links resolve. Fixed pre-PR: (1) package.json files allowlist — tarball was shipping 708 files / 24.9 MB unpacked incl. internal planning docs and typedoc HTML; now 344 files / 2.2 MB (dist + README + CHANGELOG + LICENSE + MIGRATION.md + compliance-adjudications.md). (2) M6.1 I-9 ledger entry wording corrected (gate-before-dispatch, not uniformly before-construction). (3) README quickstart UID-grain note added, mirrored in the compiled samples. Itemized as post-1.0 hardening notes in M6 NOTES: zlib boundary-synchronicity assumption in the compressed-UNAUTHENTICATE injection guard; RFC 2047 header-field decoding boundary. Tests 1936 pass, typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
LoveAndCoding
left a comment
There was a problem hiding this comment.
Review Report — "Modern API 1.0" (M0–M6, v1.0.0)
Recommendation: HOLD FOR CHANGES
This is impressively thorough, well-documented work, and the compliance-measurement discipline is real. But the review surfaced three Critical and six High findings that are merge-blocking, two of which were reproduced as live defects during validation:
- Two remote, one-line process crashes: a malformed tagged or untagged server response throws a raw exception that no listener catches, taking down the whole Node process — reproduced end-to-end (exit 1) against the real parser pipeline. For a network client that parses untrusted server bytes, this is a denial-of-service in the library's own stated "never crash" (I-6) invariant.
- A SCRAM mutual-authentication fail-open: the "terminal-failure MITM protection" the PR headlines can be bypassed by a server/MITM that never presents a verifiable server-signature; the client accepts the session as mutually authenticated.
- Plus a silent connection wedge, an unbounded hang, an auth-breaking reused-instance bug, and a
logout()that silently no-ops after reconnect (leaving a live, authenticated connection the caller believes is torn down).
None of these are subjective. Each has a concrete file/line and, for the reproduced ones, a trigger. They should be fixed (and regression-tested) before merge. The large Medium tier is mostly hardening and wire-correctness edge cases that can be follow-ups, but several (injection surfaces, capability-epoch staleness) deserve a look given this is a 1.0 security-sensitive library.
Validated finding counts: 3 Critical · 6 High · 26 Medium · 12 Low. (3 candidate findings dropped as invalid after validation; 2 reclassified as not-relevant dead code.)
Critical
1. SCRAM server-signature can go unverified (MITM fail-open)
ScramMechanism.finish() (src/sasl/scram.ts:351-360) takes zero parameters while its interface declares finish(data, ctx) (src/sasl/mechanism.ts:105); TS bivariance hides the mismatch. AuthenticateCommand.accept() always calls finish(null, ctx) (src/commands/authenticate.ts:161-163). Combined with handleServerFinal's "case 1" (scram.ts:41-48) resolving normally when no server-final message was ever presented, a server/MITM that never delivers a verifiable v= through the one channel this code inspects is accepted as mutually authenticated. Directly undercuts the PR's headline MITM protection claim.
Fix: Have finish(data, ctx) parse and verify v= via timingSafeEqual whenever a server-final wasn't already consumed; make a tagged-OK "success" with no verifiable signature fail closed. Also update authenticate.ts to pass the real data.
2. Malformed tagged response crashes the whole process
TaggedResponse (src/parser/structure/tagged.ts:30) calls StatusResponse.match(tokens,2) with no try/catch, unlike the untagged path. APPENDUID/COPYUID (RFC 4315) ride the tagged OK, so their parse-throw sites are primarily reached unprotected. Reproduced a real process exit(1) feeding A1 OK [UIDVALIDITY 0] / malformed [APPENDUID …] through the real pipeline.
Fix: Wrap TaggedResponse's StatusResponse.match() in try/catch with an UnknownContent fallback; also fix #4 below.
3. Trivial malformed untagged line crashes the process
UntaggedResponse's constructor reads tokens[1] (src/parser/structure/untagged.ts:85) and contentTokens[0] (:97) without null-guards, before its own try/catch loop. Reproduced TypeError-to-process-crash from the one-line inputs "*\r\n" and "* \r\n", in the exact file meant to be the tolerance backstop.
Fix: Guard both accesses (!secondToken || !secondToken.isType(...), and check contentTypeToken) → ParsingError/UnknownContent instead of TypeError.
High
4. Parser pipeline has no 'error' listener (crash amplifier)
connection.ts resetProcessingPipeline() (src/connection/connection.ts:782-830) attaches untagged/tagged/continue/unknown listeners to this.parser but never 'error'; no process.on("uncaughtException") exists anywhere in src. .pipe() does not forward destination errors. This is the linchpin turning #2/#3 (and any future parser error) into full process crashes.
Fix: Attach an 'error' listener to the parser (and lexer/pipeline) that converts a stream error into a controlled connection-level failure.
5. Compression codec errors silently wedge the connection
compress() routes zlib inflate/deflate errors to onSocketError (src/connection/connection.ts:1209-1211), which only emits connectionError — no socket destroy, queue stop, or state reset. A codec error fires on the transform, not the socket, so no teardown runs. connected stays true, reads are dead, writeBytes keeps feeding a destroyed deflate — a permanent silent wedge from one malformed compressed frame.
Fix: Route wrapCompression's onError through a real teardown (destroy socket + stop queue), distinguishing codec from socket errors.
6. No timeout on post-greeting negotiations → indefinite hang
execute-command.ts has no timeout mechanism at all. The post-greeting isolated-command awaits — STARTTLS (connection.ts:997/1032/1104), COMPRESS (:1172), UNAUTHENTICATE (:1272) — and runCommand() generally are unbounded; only TCP-connect/greeting/TLS-handshake are timed. A server that accepts the connection then goes silent (or a graceful FIN during the STARTTLS window) hangs connect()/the caller forever and wedges the held queue — contradicting the file's own "never hangs" claim.
Fix: Bound these negotiations with a command timeout that tears down + rejects on expiry; extend teardown detection to graceful close throughout the connect() ritual.
7. Command submitted after CommandQueue.stop() never settles
AsyncQueueContext.add() (src/connection/queue.ts:157-169) only dispatches if (this.running) with no rejecting else-branch. After stop(), a subsequent add() parks the command, never settled; because the queue is reused across reconnects, a later start() can dispatch that stale command against a fresh connection. unauthenticate() also lacks the !this.socket guard that starttls()/compress() have.
Fix: Reject synchronously in add() when stopped/torn down; add the missing unauthenticate() not-connected guard.
8. Reused SASL mechanism instances retain stale per-attempt state
mechanism.ts's contract (62-72) requires implementations usable as a literal mechanisms object to reset ALL per-attempt state in start(). Violated across the board: ScramMechanism.start() never resets verificationFailure; CramMd5Mechanism/OAuthBearerMechanism/XOAuth2Mechanism never reset stepCalled/errorPayload. On a reused instance, one transient failure permanently poisons the mechanism, and any finish() throw is force-classified terminal:true, blocking all fallback — turning a benign reconnect into unrecoverable auth failure.
Fix: Reset all per-attempt fields at the top of each start(); correct the false "ScramMechanism follows this rule" contract claim in mechanism.ts:71.
9. logout() silently no-ops after reconnect
logout() (src/client/client.ts:612-621) caches _logoutPromise but never clears it (unlike _unauthenticatePromise). Reconnect on the same instance is explicitly supported, so connect→logout→connect→logout returns the stale resolved promise from cycle 1 on the second logout — no LogoutCommand, no disconnect(), no state transition. The caller believes the session was torn down while the connection remains fully live and authenticated.
Fix: Clear _logoutPromise in the disconnected bridge; add a logout→connect→logout regression test.
Medium (26 findings — highlights)
- Implicit-TLS
connect()re-entrancy gap — two un-awaitedconnect()s onimaps://can leak/cross-wire a TLS socket. - Stale transient-error listener across STARTTLS swap, never detached from the pre-upgrade socket.
- COMPRESS/UNAUTHENTICATE boundary-injection defense has an unproven split-write smuggling risk across the async inflate boundary.
- Post-negotiation topology surgery (STARTTLS/COMPRESS/UNAUTHENTICATE) not guarded before
commandQueue.release()— a sync throw permanently wedges the queue. disconnect()isasyncbut resolves before cleanup actually completes.- Connection-local
capabilityRegistrynever invalidated on COMPRESS/UNAUTHENTICATE for Layer-1-only callers. - Unbounded PBKDF2 iteration count in SCRAM blocks the whole event loop (server-controlled
i=). - OAUTHBEARER/XOAUTH2 don't reject
\x01in user/host/token fields — kvpair-separator injection risk if any field carries untrusted input. - OAUTHBEARER/XOAUTH2 structured error diagnostics are dead code against a spec-compliant server (only fire on tagged OK; real failures end in tagged NO).
authenticate()/logout()race leaks a raw internalIllegalStateTransitionErrorinstead of the public error hierarchy.- Public
connectionescape hatch bypasses every client-side safety gate, including the cleartext-credential policy —client.connection.runCommand(new LoginCommand(...))sends a password in cleartext even withallowInsecureAuth: false. - STARTTLS doesn't invalidate the client-level capability registry — violates the PR's own "capability epochs" invariant; a greeting
[CAPABILITY]can firecapabilitiesChangedwith pre-TLS (forgeable) data before STARTTLS runs. - Unbounded UID expansion from
VANISHED (EARLIER) 1:4294967295— a one-line DoS viaexpandUidSet's unbounded loop. - LOGIN fallback can't be excluded via an explicit
mechanismslist — a SASL-only intent can still send the real password via LOGIN. CommandWriter's documented atomic-rollback guarantee is broken (chunksreassignment vs. length-truncation restore) — reproduced data loss, though latent (no shipped command currently triggers it).listMailbox()regresses a previously-fixed&-escaping bug thatmailbox()already has — reproduced:listMailbox("Sent&Received")emits a raw, unescaped&, corrupting LIST/LSUB patterns on ordinary input.- Empty SEARCH
or/fuzzyoperand breaks OR/FUZZY wire arity — reproduced:{or:[{},{subject:"x"}], from:"y"}silently compiles toOR SUBJECT x FROM y, changing AND semantics to OR. updates()iterator listener leak on a specific driver-acquisition-reject path inMailboxSession.- NUL byte and DEL (0x7F) serialized without proper escaping in several
CommandWriterpaths. date()/dateTime()don't bound-check the year, producing malformed wire dates for out-of-rangeDatevalues.- KEYWORD/UNKEYWORD SEARCH criteria emitted via
astringinstead ofatom, silently quoting invalid keywords instead of throwing. - Binary APPEND (
literal8/RFC 3516) sent without checking the server advertisedBINARY, unlike every other optional extension in that file. zlib.createInflateRaw()has no output-size cap — a decompression-bomb DoS vector from a malicious/compromised server.
Low (12 findings)
Mostly hardening, documentation-accuracy, and spec-hygiene items — including a contained (non-crashing) TypeError in ENVELOPE parsing, dead/unreachable defensive code (assertIn(), src/connection/utils.ts), unsanitized server text embedded in thrown error messages, an ALERT-trust-marking bypass via a stray space in the resp-code bracket, and several RFC-citation/documentation-accuracy nits.
Notably invalidated during independent verification (flagged for transparency, not for action)
- A suspected mUTF-7 decoder crash — the
utf7package never throws on malformed input; not a real issue. - A suspected
SequenceSetbug where"5:*,10:*"coalescing to"5:*"would drop messages — disproven; the coalescing is provably RFC 3501 §9-correct for any actual mailbox max. - A suspected
HIGHESTMODSEQ=0rejection bug — disproven; conflated two distinct response-code grammars (the resp-code position correctly requires ≥1; the STATUS data-item position, which allows 0, is handled correctly elsewhere).
Scope notes
Deep-reviewed: all of src/ across 16 file groups using state/correctness/error-handling/security/spec-compliance/api-compatibility/fidelity/maintainability lenses as fit each group, including both mega-files (client/client.ts, client/mailbox.ts) read in full. Not independently re-audited: the generated docs/compliance-history/ snapshots, and the ~4MB test corpus's assertion strength/coverage (the "1936 passing tests" claim was not re-verified). RFC text could not be fetched directly in this environment (network-restricted); spec claims were checked against this repo's own verbatim-quoting compliance catalog plus established ABNF.
Automated review orchestration — findings independently re-verified against the running code where feasible, including live reproductions for the process-crash, SCRAM, and several writer/search findings.
Generated by Claude Code
Addresses seven reproduced review findings (all verified real, each revert-verified with a new test): - listMailbox() now applies the same &→&- mUTF-7 escaping mailbox() has (wildcards */% still pass through) — a literal & no longer corrupts LIST/LSUB patterns. - Empty SEARCH or/fuzzy/not operand now throws RangeError instead of silently vanishing and flipping (A OR B) AND C into A OR (B AND C). - CommandWriter snapshot/restore now captures the chunks array reference and truncates it (was reassigned on a synchronizing literal, silently discarding rolled-back bytes). - NUL rejected from plain literals (allowed only under binary/literal8), DEL (0x7F) excluded from bare ATOM-CHAR, raw() rejects the whole C0+DEL range (was CR/LF only). - date()/dateTime() reject years outside [0,9999] (date-year is 4DIGIT; padStart didn't truncate a 5-digit or negative year). - KEYWORD/UNKEYWORD emit via atom not astring (flag-keyword = atom; astring silently quoted invalid keywords). - Binary APPEND (literal8) gated on BINARY/IMAP4rev2 capability (CapabilityError, zero bytes), like every sibling extension. - expandUidSet caps at 1M entries (VANISHED (EARLIER) 1:4294967295 took 10.6s / massive memory before this — a one-line DoS). Tests 1977 pass (+41), typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…crash the process Addresses the two Critical process-crash findings + the contained ENVELOPE TypeError (all verified real, revert-verified with tests): - UntaggedResponse guards its secondToken/contentTypeToken access — the one-line inputs "*\r\n" and "* \r\n" now yield ParsingError / the UnknownContent tolerance backstop (I-6) instead of a raw TypeError that killed the parser Transform (and the process). - TaggedResponse wraps StatusResponse.match in try/catch (the untagged path already had this): a malformed resp-code riding a tagged OK (e.g. [UIDVALIDITY 0], a bad [APPENDUID]) no longer throws uncaught; tag+status are preserved so the waiting command still settles, and a following tagged response still parses. - Envelope.match validates the 10-field arity before destructuring (was a raw TypeError from new AddressList(undefined); already contained by the FETCH backstop, now a typed ParsingError). The connection-level parser 'error' listener (review High #4, the crash amplifier) is handled in the connection-lifecycle change. Tests +new parser structure cases, typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Addresses six review findings (all verified real, revert-verified): - CRITICAL: SCRAM server-signature is now actually verified. finish() fails closed unless a matching v= was consumed — a server/MITM that jumps from client-final straight to a tagged OK with no verifiable server signature is REJECTED (was silently accepted; the old code's RFC5802-5.1-14 justification only excuses omitting server-final on FAILURE, a tagged-NO path that never reaches finish()). Preserves the existing terminal-AuthError-on-forged-signature behavior. - HIGH: all four reusable mechanisms (SCRAM/CRAM-MD5/OAUTHBEARER/ XOAUTH2) reset their per-attempt state at the top of start() — a reused instance no longer stays poisoned by one transient failure (which also force-classified every later finish() throw terminal, blocking fallback). mechanism.ts's false 'ScramMechanism follows this' claim corrected. - PBKDF2 iteration count capped at 1M (server-controlled i=99999999 ran a 28s event-loop-blocking PBKDF2 before this). - OAUTHBEARER/XOAUTH2 reject \x01 / CR / LF in user/host/token fields (kvpair-separator injection). - Explicit non-empty mechanisms list without LOGIN now excludes the LOGIN password fallback (opt back in by listing "LOGIN"); the mechanisms:[] 'skip SASL' idiom and LOGINDISABLED are preserved. - OAUTHBEARER/XOAUTH2 failure diagnostics wired to the real tagged-NO path via a describeFailure() hook (were dead code on tagged OK only). 4 SCRAM compliance scripts updated to present a genuine v= (they used a bare-OK shortcut the Critical fix now correctly rejects) — no matcher weakened; RFC5802/7677 MUST rows stay 100%. Tests 2019 pass, typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Addresses the remaining High findings + connection Mediums/Lows (all verified real except one disproven; each revert-verified with tests): - HIGH #4: parser/lexer/pipeline 'error' events now route through onPipelineError → controlled connection teardown (was uncaught → process crash; the amplifier behind the two parser-crash Criticals). - HIGH #5: compression codec errors now force a real teardown via the socket close cascade (was a silent permanent wedge — connected stayed true, reads dead, writes fed a destroyed deflate). - HIGH #6: STARTTLS/COMPRESS/UNAUTHENTICATE negotiations + CAPABILITY round trips bounded by a command timeout (ImapClientTimeouts.command is now actually wired through; was dead config) — a silent server no longer hangs connect() forever. - HIGH #7: CommandQueue gained a stopped flag; add-after-stop rejects synchronously instead of parking forever (and can't dispatch a stale command against a reconnected socket); unauthenticate() got the missing !this.socket guard. - HIGH #9: _logoutPromise cleared in the disconnected bridge — logout() after reconnect actually sends LOGOUT and tears down (was a stale resolved-promise no-op leaving a live authenticated connection). - Capability epochs (I-2): connection-local registry invalidated on COMPRESS/UNAUTHENTICATE; new secureUpgrade event invalidates the client registry the instant STARTTLS succeeds. - Also: implicit-TLS connect() re-entrancy guard; stale STARTTLS-swap error listener detached; boundary-injection split-write hardening (postBoundaryDiscard) + topology-surgery-throw guards that always release the queue; disconnect() awaits real socket close; authenticate()/logout() race maps to a typed StateError; decompression-bomb output cap (256 MiB); updates() iterator listener-leak on the CapabilityError join-reject path; replace.ts binary now gated on BINARY/IMAP4rev2; ALERT stray-space trust-marking bypass tightened; two throw-site server-text sanitizations. Escape-hatch cleartext-credential bypass: doc-strengthened, not functionally guarded (a real fix needs a CommandWriter-level security-context plumb-through — architecturally significant, flagged for follow-up). assertIn() dead-code finding DISPROVEN (it is live and tested in state.ts). Negotiation FIN-detection: timeout bound judged sufficient; instant-FIN reaction flagged as a larger follow-up. Tests 2053 pass (+34 files), typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). The 22 raw compliance vitest failures are pre-existing (8 adjudicated rows + 14 driver-env/ import-hygiene carry-forwards) — verified byte-identical with these changes reverted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Review addressed — all Critical & High findings fixed, plus the Medium/Low tierThank you for the exceptionally thorough review — the reproductions especially made these fast to confirm and fix. Every finding was independently verified against the code before touching it (I did not treat any as automatic), each real fix landed with a dedicated unit test and an undo→red→redo→green revert-verification, and every push kept the full gate suite green ( Four commits, grouped by concern:
Critical
High
Medium — all addressedFixed: implicit-TLS One judgment call — the public Low — addressedFixed: contained ENVELOPE Notes on your three invalidated candidatesAgreed on all three (mUTF-7 decoder, Deferred, explicitly (not silently)
Compliance stayed byte-identical throughout, so the fixes tighten client-side refusal/robustness on malformed-or-adversarial inputs the suite's fixtures don't exercise in those exact shapes, without moving any scored row. Ready for another look whenever you are — and let me know on the escape-hatch item. Generated by Claude Code |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Automated multi-agent merge-readiness review — PR #18 (Modern API 1.0)Recommendation: HOLD for changes (address the Critical + the untrusted-server-data robustness items), then merge-with-follow-upsThis is a large, unusually well-engineered and heavily self-documented 1.0. The compliance adjudications and migration/README docs were checked against the actual code and hold up (see G26 below) — the "6 violations" are genuinely deliberate, documented decisions, not defects. However, the review surfaced one Critical and several High-severity robustness defects, concentrated in two areas: (1) the response parser's handling of malformed/adversarial server data, and (2) connection/client teardown races. These are exactly the areas a spec-compliant client must get right because the server is an untrusted peer. None are in the adjudication ledger, so they read as genuine gaps rather than accepted trade-offs. Confidence: Moderate. Because of a tooling limitation in this environment, the sub-agent Coverage (transparency)27 file-groups were dispatched across ~154 Findings (most-severe first; single-reviewer, validator pass unavailable)CRITICALC1. Nested-multipart BODYSTRUCTURE mis-parsed → uncaught HIGHH1. Teardown racing an in-flight H2. H3. Opportunistic STARTTLS may abort the whole connection on server decline — H4. H5. Empty-array SEARCH criteria bypass the empty-operand guard → wire-invalid command — H6. H7. Empty LIST mailbox name incorrectly rejected → breaks delimiter discovery — H8. Empty list block → raw H9. H10. MEDIUM
LOW (grouped; titles + refs)Parser/robustness: partial-decode state not cleared on non-partial decode failure ( Cross-cutting themes for the maintainer
Totals1 Critical · 11 High · 19 Medium · ~28 Low. No finding contradicts the documented adjudications; the adjudication ledger, MIGRATION.md, and README were verified accurate against code. Generated by an automated multi-agent code review (orchestrator + per-group review-runner deep passes). The Generated by Claude Code |
Addendum to the automated review — two more groups completed (lexer + test-quality)Two review groups returned after the main report above. Both add material findings, including a new High and a confirmed production bug tests route around. The overall recommendation is unchanged (HOLD for changes), and these reinforce the two dominant themes (untrusted-server-data robustness; under-tested complex parsers). Added HIGHH12. Lexer swallows unrecoverable tokenization errors as "incomplete" → unbounded memory / DoS — H13. Added MEDIUM
Added LOW
Positive signal from the test-quality sample (G27)Where tests exist, quality is high: SASL/SCRAM tests use real RFC worked-example vectors and fail-closed assertions; the wire-writer and sequence-set suites are byte-exact; STARTTLS tests use real sockets. Importantly, the compliance harness is not self-certifying — RFC text is extracted into a catalog independently of the implementation, unannotated failures default to "violation" (not "unimplemented"), and acknowledged gaps still count against the denominator. That is an honest scoring model. Caveat: only 9 of 133+ unit files and 2 of ~35 compliance specs were sampled. Revised totals and coverage1 Critical · 13 High · 25 Medium · ~30 Low. Groups: 19 of 27 returned complete reviews, 1 partial (ENABLE capability-gate finding, folded into M16); 7 did not complete in this environment — mailbox.ts parts 2-3, client config/facets, commands base/writer/collector, select/status/list, misc mailbox commands, and parser-misc (incl. the 15KB Automated multi-agent review addendum. Findings are single-reviewer deep-pass results (validator second-pass unavailable in this environment); confirm before acting. Start with reproduction tests for C1 (nested-multipart BODYSTRUCTURE) and H13 ( Generated by Claude Code |
Addendum 2 — parser-misc group completed (resp-text-code / capability / THREAD)One more previously-incomplete group returned ( Added MEDIUM
Added LOW
Coverage updateNow 20 of 27 groups returned complete reviews + 1 partial. Still incomplete (forced-async pipeline stalls): 6 groups — Running totals: 1 Critical · 13 High · 28 Medium · ~32 Low. Automated multi-agent review, final addendum. The recurring theme across the parser findings (C1, H8, H12, M4-M8, M23, M27, M28, and the THREAD/QUOTA items) is untrusted-server-data robustness — a shared nesting-depth cap, bounded expansion, and a uniform "degrade the code, keep the response" tolerance policy on the untagged path would resolve most of them together. Generated by Claude Code |
Addendum 3 — validation pass complete; four previously-incomplete groups now returned + validatedFour of the six groups flagged incomplete in the main report finished after it was posted, each with its Groups now complete + validated: G4 ( Added HIGHH14. Added MEDIUM
Added LOW (grouped)
Validation rejected one candidate (working as intended)REJECTED — deferred-dispatch ( Coverage after this pass24 of 27 groups complete (23 with a validator pass; G8 validated by direct re-inspection). Still without a dedicated findings pass: 2 groups — Final running totals1 Critical · 14 High · ~36 Medium · ~45 Low, across 24 reviewed groups. No finding contradicts the documented adjudications; the adjudication ledger, MIGRATION.md, and README were verified accurate against code. Start remediation with C1 (nested-multipart BODYSTRUCTURE), H13 ( Automated multi-agent review — validation pass. Findings validated by Generated by Claude Code |
Addendum 4 — coverage complete: final two groups reviewed + validated (27/27)The two groups still open after Addendum 3 — select/status/list and the Added MEDIUMM37. Added LOWL. Attribute-algebra step order lets a contradictory listing keep both child-state attributes — Validation rejected one candidate (working as intended)REJECTED — "SPECIAL-USE gate missing IMAP4rev2 fold-in." I suspected Verified clean (no findings)
Final coverage & totals27 of 27 groups reviewed. 23 groups carried a Final totals: 1 Critical · 14 High · ~37 Medium · ~47 Low. No finding contradicts the documented adjudications; the adjudication ledger, MIGRATION.md, and README were verified accurate against code. Remediation priority remains C1 (nested-multipart BODYSTRUCTURE), H13 ( Automated multi-agent review — final coverage-closing pass. The last two groups were reviewed inline (single-reviewer + catalog-validated) because the account spend limit blocked further sub-agent runs; that constraint is stated rather than hidden. Generated by Claude Code |
…ening Second-review parser/fetch findings (all verified; each revert-verified with tests): - CRITICAL C1: nested-multipart BODYSTRUCTURE no longer throws an uncaught TypeError. The multipart-children loop built every child as a single-part structure; and the 'correct' MESSAGE/RFC822 detection the review cited was ITSELF broken (misparsed every embedded single-part body into a bogus multipart). Fixed with one shared parseBodyStructureFromParts() using the real discriminant (is the first field a parenthesized list) at all three call sites. - H6: body-fld-octets/body-fld-lines accept number|bigint (RFC 9051 App. D-1), matching RFC822SIZE — a >2^32 body part no longer throws. - H13: MessageHeader.mergeIn iterates fields.entries() correctly (Map.forEach callback is (value,key), not [key,value]) — the split-header merge path was silently discarding field names. The test that documented-and-tested-around the bug now asserts correct behavior. - M4: BODY_STRUCTURE_MAX_DEPTH=100 recursion cap (stack-overflow DoS). - M6: min-field-count guard on single-part BODYSTRUCTURE (typed ParsingError, not raw TypeError, on truncated data). - M24: new 18-test body.structure.test.ts (single/multipart/nested/ malformed/bigint/truncated/depth/embedded). - Lows: INTERNALDATE/SAVEDATE reject Invalid Date; address.ts truncated- tuple guard. envelope.ts finding disproven (already guarded). Tests 2087 pass, typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Second-review lexer/capability/types findings (all verified; each
revert-verified with tests; M21 benchmarked as a non-issue):
- H12: the lexer no longer buffers forever on a terminal tokenization
error. New UnterminatedStringError (a quoted string can't cross CRLF,
so on a complete line it's terminal, not mid-literal) is propagated
via done(error); a 10 MiB buffer cap backstops any other runaway. A
hostile unterminated quote no longer accumulates all later bytes or
gets retroactively 'closed' by a stray quote.
- M20: IMAPLogMessage's discriminated union now discriminates —
'warn' removed from the error arm (verified every warn call site
carries only detail), so level==="error" narrows correctly. Compile-
only type test added under test/docs.
- M23: {n+} literal marker now tolerated consistently in StringRule +
token stripping (was tolerated by the newline/lexer framing layers
but not the string rule → below-threshold desync).
- M26: COMPRESS=DEFLATE (and other KIND=VALUE registered capabilities)
no longer misreport isUnknown=true.
- Lows: encoding.ts clears stale partial-decode state on the hard-
failure path; dead AtomRule '*' branch removed.
- M21: tokenize() benchmarked linear (V8 SlicedString), NOT O(n²) —
no rewrite; a perf regression guard added.
Tests 2111 pass, typecheck 0 (incl. doc-samples), eslint 0 errors,
compliance per-row identical (1138/6/2/451, problems []).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…e stream Second-review parser-core findings (all verified; each revert-verified; four Low candidates disproven as already-guarded): - M8 (linchpin): a per-line ParsingError now degrades to an 'unknown' event and parsing continues; only a genuinely non-ParsingError exception stays fatal (Parser holds no cross-line state). This makes H7/H8/M5/M7 non-catastrophic. - H7: empty LIST mailbox name accepted — LIST "" "" delimiter- discovery (* LIST (\Noselect) "/" "") parses instead of falling to UnknownContent. - H8: '( )'-shaped empty list block raises ParsingError, not a raw TypeError that permanently errored the Transform. - M5 / THREAD: shared MAX_NESTED_LIST_DEPTH cap on nested ESEARCH and THREAD recursion (measured multi-second DoS at deep nesting). - M7: unbalanced parens (over- or under-closed) raise ParsingError instead of silently corrupting grouping. - M27: [MODIFIED 2:*] accepts the '*' wildcard (RFC 7162 §3.8 — MODIFIED carries a sequence-set, not a uid-set) via an opt-in UIDSet.allowWildcard; UID-only usages untouched. - M28: untagged * OK/NO/BAD/BYE with a throwing resp-code preserves the status word + text (mirrors the tagged-path tolerance) instead of losing all structure. - M19: stale parseTokens comment corrected (no bug). - Lows: quota.ts truncated-triplet guard; ESEARCH oversized COUNT/MIN/MAX preserved as bigint not dropped; anchored ESEARCH key regex. sort/thread-match/vanished/urlauth findings disproven (already guard truncated input). Tests 2150 pass, typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…uthzid
Second-review search/SASL/config findings (all verified; each revert-
verified; two candidates verified not-bugs, one Low deferred):
- H5: empty-array SEARCH criteria ({keyword:[]}, {header:[]}, {and:[]},
nested empty or/fuzzy/not) now throw instead of compiling to a
wire-invalid bare SEARCH / short-operand OR — estimateKeyCount counts
an empty array as 0 (was Math.max(len,1)), with a top-level zero-token
check and empty-operand throws in not/and.
- M2: STORE MODIFIED expansion routed through collector's bounded
expandUidSet (deleted the duplicate that omitted the 1M cap) — a
[MODIFIED 1:4294967295] response no longer attempts a billion-entry
allocation.
- M3: ESEARCH ALL/PARTIAL range expansion bounded (delegates to the
shared expandUidSet where possible; local cap mirrors it otherwise).
- M13: ScramMechanism.describeFailure() surfaces the server e=
diagnostic on the tagged-NO path (mirrors the OAUTH siblings;
fail-closed verdict untouched).
- M35: authzid config support closed end-to-end — ImapAuthConfig gains
authzid?, validateAuth() relaxes the pass/token requirement only for
an explicit EXTERNAL/ANONYMOUS-only mechanisms list, and
performAuthSelection threads authzid into the SaslContext (which
EXTERNAL/ANONYMOUS already consume).
- Lows: NFKC now applied to authzid too (was username-only); config
allowInsecureAuth validates (was silent coercion); SEARCH RETURN
error hint lists RELEVANCY. Verified-not-bugs: empty FLAGS () is legal
grammar; defaultCandidates() omitting CRAM-MD5 is spec §9.3. Deferred:
the public SCRAM nonce-override footgun (test infra imports it via the
public path — flagged for a separate follow-up).
Tests 2198 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…ctor + rev2 gates
Second-review message-op findings (all verified; each revert-verified):
- H9: BODY[section] and BINARY[section] no longer collide on the parts
Map key (composite section+binary key; part() gains a {binary}
disambiguator, BODY-first for back-compat).
- H10: part.buffer() rejects (ImapError) when the stream is destroyed
mid-drain (added the missing 'close' listener) instead of hanging.
- H14: seq.replace() now applies assertSequenceGrainSafeUnderNotify
like every sibling — a stale MSN under an active MessageExpunge
NOTIFY can't silently replace/delete the wrong message (reuses the
existing RFC5465-5.3-2 refusal class).
- M1: FETCH section-spec / binarySize validated against the RFC grammar
before emission (blocks ]/space injection into BODY[...]/BINARY[...]).
- M9: stream()-after-buffer() while a drain is in flight now throws
(single-consumer discipline).
- M29: UID EXPUNGE OR-gates IMAP4rev2 (mailbox gate + ExpungeCommand
capability array) — pure-rev2 servers no longer get expunge(uids)
rejected.
- M30: [CAPABILITY] resp-code reports the wire keyword "CAPABILITY"
(was the plural display label "CAPABILITIES").
- M31: ResponseCollector.settle() gains abort()'s first-wins terminal
guard (settle-after-abort no longer disagrees with itself).
- M32: expandUidSet on the resp-code path (COPYUID/APPENDUID/MODIFIED)
rethrows ProtocolError (ImapError), not a bare RangeError.
- M33: dropped the fabricated APPENDLIMIT typed branch (RFC 7889
defines no such resp-code; falls through to the tolerant fallback);
BADCOMPARATOR comment corrected (kept — the suite pins a real server
emitting a trailing charset).
- M34: ExpungeCommand + FetchCommand added to the commands barrel
(fixed a latent SequenceSetLike double-export collision).
- M36: idle() tears down its IdleController on session close/reselect
(once('closed')), mirroring updates().
- M37: RETURN (STATUS) OR-gates IMAP4rev2 (LIST-STATUS rev2 fold-in);
SPECIAL-USE correctly left non-folded.
- Lows: list attribute-algebra order (contradiction after implication);
corrected stale base.ts/fetchOne() doc comments; factored a duplicated
uid/modseq spread helper.
Flagged for follow-up (out of R2-E1 territory): comparator/convert/
gmail-labels/language/replace/store also missing from the barrel;
CreateCommand's RFC 6154 §6 USE() misquote.
Tests 2233 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Second-review connection/client findings (all verified; each revert- verified; H2 disproven, M16 investigated-and-correctly-unchanged): - H1: teardownFailedConnect() now emits 'disconnected' and a mid-connect disconnect() drives the same abort mechanism a socket error uses, so close()/logout() racing an in-flight connect() no longer hangs past timeout; port validation moved before connectInProgress is set. - H3: an opportunistic (STARTTLS_OPTIONAL) server decline — a tagged NO/BAD mapped to TlsError — now continues plaintext instead of tearing down the connection; mandatory STARTTLS still hard-fails. - H4: the client 'unhandled' bridge passes the full response union, so unmatched-tag TaggedResponse / unclaimed ContinueResponse (protocol- desync signals) surface instead of being silently dropped. - M10: COMPRESS activation preserves residual bytes captured at the tagged-OK instant by pausing line-splitting (beginOpaqueCapture) and seeding them into the fresh inflate, instead of discarding its own opening bytes and desyncing decompression. (UNAUTHENTICATE side is correctly discard-only — its boundary sits downstream of any live codec; the 'preserve' variant there was reverted as a no-op.) - M11: disconnect() works during the implicit-TLS handshake window (openTls exposes the socket synchronously via onSocket). - M14: server-text sanitization (shared sanitizeForErrorMessage) applied to authenticate.ts + starttls.ts (the latter literally built message += CRLF + text) — log-injection from an untrusted/pre-auth peer is escaped. - M15: CompressCommand declares capability COMPRESS=DEFLATE (Layer-2 escape-hatch gate). M17: ImapClient.compress() rejects StateError from disconnected/connecting/logout (no longer conflates no-connection with server-declined). M18: unauthenticate() checks state first and rejects a typed StateError on a concurrent logout() race instead of mislabeling the session close + falsely resolving. - M12: flushPending() nesting doc corrected (non-nesting is structural). - Lows: tautological isSecure conjunct removed; onSocketError typed; dead deepEqual helper removed. H2 disproven (teardown already resets postBoundaryDiscard). M16 (ENABLE capability gate) left as-is — OR-with-rev2 broke real-world-server fixtures; documented in-file. newline.transform.ts gained beginOpaqueCapture/endOpaqueCapture/ injectBytes for M10 (last touched at M3.2; no concurrent edits). Tests 2252 pass, typecheck 0, eslint 0 errors, compliance per-row identical (1138/6/2/451, problems []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…misquote
Follow-ups the R2-E1 agent flagged as out of its territory:
- M34 (rest): comparator/convert/gmail-labels/language/replace/store
were also missing from the src/commands/index.ts barrel (the public
./commands subpath), so a caller couldn't hand-build them for
client.run(). Added all six exports (no export collisions) and
extended the barrel test to pin them.
- Low: CreateCommand's empty-specialUse RangeError message misquoted
RFC 6154 §6 as requiring >=1 use-attr; the catalog's own §6 ABNF
brackets the list as optional ("USE" SP "(" [use-attr *(SP
use-attr)] ")"), so an empty (USE ()) is legal. Reframed the message
+ comment + test title as the honest rationale (it's a client-side
guardrail against a no-op, not a grammar requirement); behavior
unchanged.
Tests 2258 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…ssed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Second review addressed — the Critical, all 14 High, and the Medium/Low tierThank you for the second, much wider pass (and for the candid coverage/validation disclosures — they made triage faster). Everything actionable is fixed. Same discipline as before: every finding verified against the code before any change, each real fix landed with a unit test and an undo→red→redo→green revert-verification, and the compliance matrix stayed byte-identical throughout (1138 pass / 6 adjudicated violations / 2 adjudicated-unimplemented / 451 untestable / Seven commits, file-disjoint clusters:
Critical
High — all 14 fixedH1 connect()-teardown race (teardownFailedConnect now emits Medium — addressedM1 section-spec injection validated · M2/M3 STORE-MODIFIED & ESEARCH range expansion now share the bounded-expansion cap · M4/M5 recursion-depth caps (BODYSTRUCTURE, ESEARCH, THREAD) · M6 truncated-BODYSTRUCTURE typed guard · M7 unbalanced parens → ParsingError · M9 stream()-after-buffer() single-consumer · M10 COMPRESS boundary now preserves straddling bytes into the fresh inflate (rather than discarding its own opening bytes) · M11 disconnect() works during the implicit-TLS window · M13 SCRAM Low — addressedNFKC on authzid, Verified not a bug / disproven (matching your validator's spirit)
Deferred, explicitly
M10's fix required touching Generated by Claude Code |
Cleaning up with prettier **Test plan:** Run tests
Removing all the compliance history files as those aren't needed to retain in the repo. **Test plan:** Make sure the files are gone
What this is
The complete modern-API implementation, M0 through M6, ending at version 1.0.0: a spec-compliant, fully typed IMAP client with a promise/async-iterable API, measured throughout by the repo's per-RFC-requirement compliance suite. Every milestone ratcheted the same matrix per-row (status + failure kind), with regressions blocked at every merge; every milestone's snapshot and close notes live in
docs/compliance-history/.The M0–M6 arc
ImapClient, typed config, the SASL framework (PLAIN/OAUTHBEARER/XOAUTH2/LOGIN-fallback), ENABLE, capability epochs, the typed error hierarchy, and removal of the legacySessionclass. 486 pass.MailboxSession, create/rename/delete/subscribe, STATUS, NAMESPACE, APPEND, the mUTF-7 codec. 602 pass.SequenceSet, and theseqfacet mirroring every UID-grain verb. 780 pass.updates()iterator, CONDSTORE/QRESYNC, NOTIFY, SORT/THREAD (+ SORT=DISPLAY, ESORT), SEARCHRES, WITHIN, FUZZY. 882 pass.compress()/unauthenticate()racing an in-flight command). 1138 pass.docs/MIGRATION.md, README rewrite, CHANGELOG, version 1.0.0, and a three-lens final review (zero criticals).Compliance: before → after
[], nine consecutive byte-identical full runsAdjudications (read this before reading "violations: 6" as defects)
All non-passing rows — the 6 violations and 2 unimplemented — are documented, deliberate decisions in
docs/compliance-adjudications.md(~25 entries):$Junk/$NotJunkconflict STOREs and no refusal of$Forwardedremoval — keyword policy belongs to callers; the library exposes the raw signal.*-PLUS is a documented non-goal with reactivation conditions written into the catalog.Breaking changes vs 0.9
The public API is new — 0.9 was a TypeScript port of node-imap.
Sessionis gone, callbacks are gone, event-DSL fetch/search are gone, booleantlsconfig is gone.docs/MIGRATION.mdmaps every node-imap idiom to its 1.0 equivalent (all destination samples are compile-checked in CI vianpm run typecheck), including the one data-model change (getBoxes()tree → flatlist()).Deferred, with rationale
*-PLUS channel binding (tls-exporter per RFC 9266): legitimate post-1.0 feature; catalog rows carry reactivation conditions.updates()-signature types.npm run test:e2e(real-server suite) was not run in this environment (no credentials); all other gates ran on every merge.Verification
npm test(1936),npm run typecheck(incl. compiled doc samples),npm run typecheck:compliance,npx eslint .(0 errors),npm run docs(0 warnings),npm run build+npm pack --dry-run(tarball: 344 files / 2.2 MB), andnpm run test:compliance— nine consecutive byte-identical runs across all 1597 requirement-profile rows,problems: [].Per the session's standing decision this PR is opened for human review and is not merged by the automation.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Generated by Claude Code