Skip to content

Modern API - #3

Draft
LoveAndCoding wants to merge 159 commits into
mainfrom
modern-api
Draft

Modern API#3
LoveAndCoding wants to merge 159 commits into
mainfrom
modern-api

Conversation

@LoveAndCoding

@LoveAndCoding LoveAndCoding commented Jul 3, 2022

Copy link
Copy Markdown
Owner

Goals

Have clean and easy to understand API that utilizes modern JS functionality. Under the hood, the code should also better differentiate between an IMAP connection and an IMAP session, and utilize the rewritten parser engine.

Work

  • Split connection code from the concept of a session
  • Change connection API to utilize async/await/promise methods
  • Create command/control structured queue
  • Port supported IMAP commands to new command/control queue
    • CAPABILITY
    • NOOP
    • LOGOUT
    • ID
    • STARTTLS
    • AUTHENTICATE
    • LOGIN
    • ENABLE
    • SELECT
    • EXAMINE
    • CREATE
    • DELETE
    • RENAME
    • SUBSCRIBE
    • UNSUBSCRIBE
    • LIST
    • NAMESPACE
    • STATUS
    • APPEND
    • IDLE
    • CLOSE
    • UNSELECT
    • EXPUNGE
    • SEARCH
    • FETCH
    • STORE
    • COPY
    • MOVE
    • UID
  • Create wrapper classes for IMAP objects
    • Session
    • Box
    • Message
  • Convert existing tests

This is the beginning of the new command style strcture. Commands will
be added as self managing classes that know how to generate and send the
actual command text as well as process parsed responses.

The base class implements a common strategy of sending a tagged command
and listening for a tagged response, capturing everything in between.

The no-op is the most simple implementation of this command system.
Adding command classes for the ID and Capability commands, which will be
common to run on intiating the connection.

Also included is the first command encoding function for creating IMAP
safe strings.
This class will eventually serve as the base class that is exposed and
that other systems would interface with. This will contain the public-
facing API for the library and will be both a mirror and an update of
existing functionality.

Note that this commit does not expose this class nor remove or change
the original `Connection` class which currently acts as the public
interface. Eventually the `Connection` class will be cleaned up and it
will serve purely as the connection for the session without any logic
for actual commands.
Pulled everything out of the connection class that is not actually
related to the connection. This means almost all IMAP related commands
and controls will be in a new class. The code went from almost 2k lines
to around 300, which should be a lot easier to understand.

From a practical perspective, I chose to write a brand new class and
just remove the old copy completely. The old code was inter-weaved with
a lot of implicit assumptions and untangling that would have been a
nightmare. This does, however, mean this is all (or almost all) new
code. Which is extra terrifying. But that's a future me problem.
@LoveAndCoding LoveAndCoding added the enhancement New feature or request label Jul 3, 2022
@LoveAndCoding LoveAndCoding added this to the v1.0.0 milestone Jul 3, 2022
@LoveAndCoding LoveAndCoding self-assigned this Jul 3, 2022
LoveAndCoding and others added 23 commits July 4, 2022 10:43
Actually connects and exposes the Session class so that it functions and
can create and manage an IMAP session!

This is exciting, and I was able to connect a new session to GMail and
get the capabilities and server info 💃

**Test plan:**
Connect to GMail using the Session class
Celebrate 🙌
Instead of just using A0, A1, etc. we want to use A1, B1, etc. This
us to send *a lot* more commands before we reuse tags
Creating a baseline E2E suite that can be used to test compatibility
against various servers.

**Test plan:**
yarn test:e2e
With the update to tag names, the tests needed updating.

**Test plan:**
`yarn test`
`yarn test:e2e`
Testing out reporting coverage via GH Actions.

**Test plan:**
Hope that this all works because GH actions are a pain sometimes
Languages that use indentation as syntax should burn. Spacing should not
determine validity 🔥
Switching to only create a single new context for isolated commands
rather than premptively making a new context. This was causing issues
when the next command queue was empty. We wouldn't properly idle.

**Test plan:**
`yarn test`
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1. aggregate: sort ProfileResult.tests and problems arrays for diffable JSON output run-to-run
2. render: add cell() helper that collapses whitespace and escapes | as \|; apply to all table cell interpolations; append … only when 160-char truncation actually occurs; Math.floor in pct() so 0.995 → 99%
3. aggregate: failed test with no failureKind annotation defaults to "violation" (vitest timeout bypasses catch)
4. aggregate: warn when a test cites a req under a profile not in that req's profiles list
5. compliance-reporter: print console summary before file writes; wrap mkdir+writes in try/catch with clear one-line error
6. New tests: __tests__/render.test.ts (5 tests), 4 new aggregate tests — 55 total, all green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LoveAndCoding and others added 8 commits July 4, 2026 11:15
Full client-binding catalog for the closing family: 186 new requirements
(catalog 770 to 956). Per-source: RFC5255 LANGUAGE/I18NLEVEL 21, RFC5259
CONVERT 26, RFC4467 URLAUTH 16, RFC5524 URLAUTH=BINARY 7, RFC5802
SCRAM-SHA-1 29, RFC7677 SCRAM-SHA-256 2, RFC4505 ANONYMOUS 12, RFC4422
EXTERNAL +3 (Appendix A append), RFC2221 LOGIN-REFERRALS 8, RFC2193
MAILBOX-REFERRALS 11, RFC3348 CHILDREN 3, X-GM-EXT-1 vendor 23; plus 5
reconciliation-delta sources: RFC7889 APPENDLIMIT 6, RFC8438 STATUS=SIZE 3,
RFC8440 LIST-MYRIGHTS 4, RFC8970 PREVIEW 6, RFC9585 INPROGRESS 6.

Mechanical quote verification per source (X-GM-EXT-1 against the Google
Developers page, 5 WebFetch passes cross-checked). Cross-catalog
adjudication: CHILDREN all rev1-only vs RFC9051-7.3.1/RFC5258; STATUS=SIZE
dual w/ cross-ref (rev2 folds the caution not the wire vocabulary);
SCRAM-SHA-256 cross-refs 5802 without duplication; EXTERNAL append-only,
existing 16 entries untouched. Corrections caught: 63-bit not 64-bit SIZE
(verbatim RFC); registry references RFC 9755 not 6855 for UTF8 (flagged).

REAL-signal findings noted for the spec batches: REFERRAL resp-code hits
the same AtomTextCode bare-argument-drop defect as Phase 5's NOUPDATE/
UNDEFINED-FILTER (kind survives, URL dropped - third instance of that
root cause); INPROGRESS's parenthesized argument parses cleanly (genuine
testable); \HasChildren likely real passes.

problems=0, schema valid, 956 catalog requirements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 6 audit fixes across 6 catalog files (7 items):

- rfc5255.ts: fixed a spliced-artifact quote on RFC5255-3.1-3 -- the
  section 3.1 text wrongly borrowed section 7's "re- issue" line-wrap
  space; corrected to the unbroken "re-issue" that section 3.1 actually
  uses, with the section 7 restatement moved to a side observation in
  notes.
- rfc5524.ts: (a) RFC5524-1-1 was mis-sectioned as section "1" when its
  quoted sentence lives in section 3 -- renumbered to RFC5524-3-1 /
  section "3" (verified no collision with the 3.1-*/3.2-* ids) and
  updated all cross-references; (b) corrected the extractionNote's
  keyword-less-exception list, which wrongly included RFC5524-3.2-2
  despite its text containing MUST/SHOULD, and removed the overstated
  claim that the Abstract names the URLAUTH=BINARY capability string.
- rfc4467.ts: (a) fixed RFC4467-9-2's notes, which wrongly described
  url-rump as the URL without any ";URLAUTH=" suffix -- per section 6,
  url-rump retains ";URLAUTH=<access>" and drops only ":<mech>:<token>";
  (b) added RFC4467-3-6, a new testable entry for the client's duty to
  supply a valid access identifier in its GENURLAUTH url-rump argument,
  evidenced by the section 5 worked BAD example.
- rfc5259.ts: (a) fixed RFC5259-6-1's note, which wrongly located the
  "CONVERT/ UID CONVERT" line-wrap at a page 6/7 boundary -- it is a
  plain line-wrap within page 6; (b) added RFC5259-8.2-1 for the
  client's MIME-type-match expectation/error-treatment duty; (c) added
  RFC5259-9-8 for the ERROR TEMPFAIL mm client wait-time SHOULD, and
  corrected RFC5259-9-4's note, which wrongly claimed RFC5259-9-1
  already covered this distinct ERROR-phrase form.
- rfc2193.ts: corrected three stale notes (RFC2193-5.1-1, RFC2193-5.2-1,
  and the extractionNote) claiming the rlist/rlsub driver verbs don't
  exist -- they were added in commit 74a9620 and throw
  NotImplementedError (self-actualizing unimplemented).
- xgmext1.ts: (a) fixed X-GM-EXT-1-labels-8's note, which claimed no
  worked SEARCH example was found -- added the confirmed example from
  the Google page; (b) added X-GM-EXT-1-labels-10 for the XLIST-based
  label-list-retrieval sentence, graded MAY/untestable (internal-
  decision) given XLIST's documented deprecation.

Full yarn test:compliance run: the three meta suites (catalog,
registry-coverage, import-hygiene) all pass with zero schema problems;
catalog entry count rose from 956 to 960 (+4), matching the four new
entries added (RFC4467-3-6, RFC5259-8.2-1, RFC5259-9-8,
X-GM-EXT-1-labels-10). All new/changed quotes were verified as verbatim
substrings of the source RFC .txt files (or cross-checked WebFetch
passes for the vendor doc) via a throwaway Node script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 6 Task 8 (G3 — legacy/vendor/deltas): spec batch for LOGIN-REFERRALS
(RFC 2221), MAILBOX-REFERRALS (RFC 2193), CHILDREN (RFC 3348, rev1-only),
the X-GM-EXT-1 Gmail vendor extension, and the small reconciliation-delta
sources (APPENDLIMIT/RFC 7889, STATUS=SIZE/RFC 8438, LIST-MYRIGHTS/RFC 8440,
PREVIEW/RFC 8970, INPROGRESS/RFC 9585).

Coverage (8 new files, 67 tests total):
- referrals-2221-2193.test.ts: REFERRAL resp-code acceptance across LOGIN/
  AUTHENTICATE (RFC2221-4.1-1/-2/-3, RFC2221-4.2-1), SELECT/EXAMINE/DELETE/
  SUBSCRIBE/UNSUBSCRIBE/STATUS/APPEND/CREATE/RENAME/COPY (RFC2193-3-2/-3-3,
  RFC2193-4.1-1/-4.2-1/-4.3-1/-4.4-1), plus RLIST/RLSUB command forms
  (RFC2193-5.1-1/-5.2-1). 22 tests: 12 genuine passes (kind-acceptance legs),
  6 genuine violations (URL-exposure legs), 4 unimplemented (RLIST/RLSUB).
- children-3348.test.ts: RFC3348-3-3 (LSUB not authoritative for CHILDREN
  hierarchy), rev1-only. 1 genuine pass.
- xgm-ext1.test.ts: X-GM-MSGID/X-GM-THRID/X-GM-LABELS FETCH+SEARCH forms,
  X-GM-LABELS STORE (+ADD) form, X-GM-RAW search passthrough, capability
  gate, and CREATE-based label lifecycle. 20 tests: 2 genuine passes
  (capability parsing), 18 unimplemented (no fetch/search/store/create
  surface).
- appendlimit-7889.test.ts, statussize-8438.test.ts, listmyrights-8440.test.ts,
  preview-8970.test.ts: STATUS/LIST-STATUS/APPEND/FETCH wire-form specs for
  each delta capability, all self-actualizing unimplemented (22 tests).
- inprogress-9585.test.ts: RFC9585-4-1, all four ABNF-permitted INPROGRESS
  shapes (bare, all-NIL, counting-with-unknown-goal, known-goal triple).
  8 genuine passes.

Real-signal findings confirmed:
- REFERRAL (RFC 2221/2193): the `[REFERRAL imap://...]` resp-code parses via
  the AtomTextCode fallback — kind === "REFERRAL" survives intact (genuine
  PASS on kind-acceptance, 12 tests across LOGIN/AUTHENTICATE/OK/BYE/SELECT/
  CREATE/COPY), but the bare (unparenthesized) URL argument is dropped by
  splitSpaceSeparatedList's default '(' start-token (genuine VIOLATION on
  URL-exposure, 6 tests including multi-URL and RENAME-pair cases) — the 4th
  confirmed instance of this AtomTextCode bare-arg-drop defect after
  RFC5466 UNDEFINED-FILTER.
- INPROGRESS (RFC 9585): `[INPROGRESS ("A001" 454 1000)]` and all three
  sibling ABNF shapes parse cleanly (parenthesized argument survives the
  same start-token mechanism that drops REFERRAL's bare argument) — 8
  genuine passes keying on the exact parsed tag/progress/goal tuple values.
- \HasChildren/\HasNoChildren (RFC 3348): confirmed real — the client
  parses LIST/LSUB attributes; RFC3348-3-3 (LSUB not authoritative) is a
  genuine pass: a stale LSUB \HasNoChildren does not suppress or override a
  subsequent LIST's \HasChildren for the same mailbox name.

Full `yarn test:compliance` run: 0 problems (no stale hints, no unknown ids,
no profile mismatches). `yarn test`: 229 passed, the pre-existing
newline.transform failure only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 6 batch G1 (i18n + URL): three new spec files covering RFC 5255
(LANGUAGE/I18NLEVEL/COMPARATOR), RFC 5259 (CONVERT), RFC 4467 (URLAUTH),
and RFC 5524 (URLAUTH=BINARY, folded into the URLAUTH file per its small
7-entry delta). All four catalog sources now have zero untested-testable
requirements; full yarn test:compliance run confirms problems: [] and
yarn test shows only the pre-existing newline.transform failure.

Coverage:
- language-5255.test.ts: 21/21 RFC5255 entries (LANGUAGE command forms,
  COMPARATOR command forms, [BADCOMPARATOR] resp-code, active-comparator
  scoping over SEARCH/SORT/THREAD).
- convert-5259.test.ts: 28/28 RFC5259 entries (CONVERT/UID CONVERT/
  CONVERSIONS command forms, CONVERTED/CONVERSION response acceptance,
  TEMPFAIL/MAXCONVERTMESSAGES/MAXCONVERTPARTS resp-codes, ERROR-phrase
  shapes, cross-session caching prohibition).
- urlauth-4467.test.ts: 17/17 RFC4467 + 7/7 RFC5524 entries (RESETKEY/
  GENURLAUTH/URLFETCH command forms, access-identifier construction,
  GENURLAUTH/URLFETCH response acceptance, URLMECH resp-code, extended
  URLFETCH BINARY/BODYPARTSTRUCTURE/BODY parameters).

Real-signal findings (probed via connectLow before writing, per
REAL-SIGNAL-FIRST):
- LANGUAGE, COMPARATOR, CONVERTED, CONVERSION, GENURLAUTH, and URLFETCH
  (base and RFC 5524 extended forms) untagged responses have NO parser
  handler. UntaggedResponse's fixed matcher checklist has no entry for any
  of them, so the constructor throws ParsingError. Connection attaches no
  'error' listener to the Parser transform, so the parser stream dies
  silently mid-connection — no event surfaces, and every subsequent
  response on the wire is lost too (identical failure shape to the Phase 5
  QRESYNC VANISHED finding). All six response families are measured as
  honest, non-vacuous violations (expectFailure: "violation") asserting
  both acceptance and stream survival.
- BADCOMPARATOR, TEMPFAIL, MAXCONVERTMESSAGES, and MAXCONVERTPARTS
  resp-codes DO parse (no throw) via text.code.ts's generic AtomTextCode
  fallback for unrecognized resp-text-code atoms — kind is exposed
  correctly. But a CONFIRMED defect recurs across all of them: AtomTextCode
  calls splitSpaceSeparatedList with the function's default "(" / ")"
  delimiters (not null, null, unlike AppendUIDTextCode/CopyUIDTextCode), so
  a bare unparenthesized trailing argument (a charset word, a retry-sizing
  number) never finds an opening paren and is silently dropped — contents
  ends up [] instead of carrying the argument. Argument-less forms
  ([BADCOMPARATOR], [TEMPFAIL]) are clean REAL passes; argument-bearing
  forms are measured as honest violations pinning the dropped value.
- URLMECH resp-code is a clean REAL pass (both the RESETKEY tagged-OK and
  the SELECT/EXAMINE untagged-OK placements) since RFC4467-8-1 requires
  only non-erroring kind acceptance, not argument fidelity.
- Every command-emission duty (LANGUAGE, COMPARATOR, CONVERT/UID CONVERT/
  CONVERSIONS, RESETKEY/GENURLAUTH/URLFETCH, extended URLFETCH parameters)
  self-actualizes as unimplemented — language()/convert()/genurlauth()/
  urlfetch()/resetkey() all throw NotImplementedError, so each wire form
  is pinned via a tightly-anchored scripted-server matcher for when a
  driver surface exists.

Distribution across the three files (72 language + 76 convert/urlauth-ish
test instances, both profiles): a handful of clean REAL passes (URLMECH
x2, BADCOMPARATOR/TEMPFAIL no-argument forms), a set of REAL honest
violations (the six untagged-response families, the argument-drop
defects), and the remainder self-actualizing unimplemented — no vacuous
passes anywhere.

Catalog concerns surfaced (informational only, no catalog edits made):
none beyond what's already documented in the RFC5259/RFC4467 extraction
notes (CONVERT's near-zero real-world deployment, URLAUTH's heavy
server-side key-management skew).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 6 Task 7 (G2) — remaining SASL mechanism family: SCRAM-SHA-1/-256
(RFC 5802 + RFC 7677), ANONYMOUS (RFC 4505), and EXTERNAL (RFC 4422
Appendix A). All specs self-actualize as 'unimplemented' since
driver.authenticate() throws NotImplementedError today; every matcher
independently recomputes the expected wire bytes so the same assertions
become genuine once AUTHENTICATE lands.

test/compliance/specs/ext/sasl-scram-5802-7677.test.ts
  20 tests (10 x 2 profiles). Cites: RFC5802-5-1, -5-2, -5-3, -5.1-2,
  -5.1-4, -5.1-6, -5.1-7, -5.1-8, -5.1-9, -5.1-10, -5.1-12, -5.1-13,
  -6-1, -6-3, -7-1, -7-2, -7-3, -3-1; RFC7677-3-1. Skipped (untestable/
  out-of-scope, see catalog notes): RFC5802-5.1-1 (authzid escaping,
  not exercised), -5.1-3/-3-2 (SASLprep, internal-decision), -5.1-5/
  -15/-16 (extension attributes, none exercised), -5.1-11 (cbind-data
  content, needs real TLS channel binding), -5.1-14 (no-final-message
  failure shape), -6-2/-6.1-1/-6.1-2 (channel-binding preconditions
  unmet), RFC7677-4-1 (-PLUS session-hash, channel binding unmet).

  SCRAM-SHA-1 derivation (RFC 5802 §5 worked example, user/pencil,
  independently recomputed via Node crypto -- PBKDF2-HMAC-SHA-1 for
  Hi(), HMAC-SHA-1 for ClientKey/ServerKey/ClientSignature/
  ServerSignature, SHA-1 for StoredKey):
    c-nonce="fyko+d2lbbFgONRv9qkxdawL" s-nonce="3rfcNHYJY1ZVvWVs7j"
    salt=base64("QSXCR+Q6sek8bf92") iterations=4096
    client-first-message = "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"
    server-first-message = "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096"
    client-final-message = "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="
    recomputed p= == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=" (matches RFC verbatim)
    recomputed v= (ServerSignature) == "rmF9pqV8S7suAoZWja4dJRkFsKQ=" (matches RFC verbatim)

  SCRAM-SHA-256 derivation (RFC 7677 §3 worked example, user/pencil,
  same formula with SHA-256 substituted for HMAC()/H()):
    c-nonce="rOprNGfwEbeRWgbNEkqO" s-nonce="%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0"
    salt=base64("W22ZaJ0SNY7soEsUEjb6gQ==") iterations=4096
    client-first-message = "n,,n=user,r=rOprNGfwEbeRWgbNEkqO"
    server-first-message = "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"
    recomputed p= == "dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=" (matches RFC verbatim)
    recomputed v= (ServerSignature) == "6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=" (matches RFC verbatim)

  Username escaping (RFC5802-5.1-4): escape(",")->"=2C", escape("=")->
  "=3D", e.g. escape("a=b,c") = "a=3Db=2Cc".

  Both vectors were independently recomputed with a Node scratch script
  (crypto.pbkdf2Sync + createHmac + createHash, manual XOR of Buffers)
  and matched each RFC's own published client-first-message, server-
  first-message, p=, and v= constants byte-for-byte before being pasted
  into the spec file's header comment.

  Matcher-rigor: constructed and confirmed rejection of -- wrong
  gs2-cbind-flag ('y'/'p=' when 'n' expected), unescaped comma/equals
  in the username, a nonce containing ',', reversed attribute order
  (r= before n=, or p= before r=), non-base64 'p=' proof, spurious
  cbind-data appended to the gs2-header for a non-channel-binding
  flag, a wrong/truncated echoed nonce, and a missing gs2-header in
  'c='. Dedicated legs: nonce-uniqueness across two exchanges
  (RFC5802-5.1-7), nonce-echo-mismatch abort via '*' (RFC5802-5.1-8),
  ServerSignature-mismatch rejection despite a tagged OK
  (RFC5802-5-3/-5.1-13), and 'e=' server-error acceptance.

test/compliance/specs/ext/sasl-anonymous-4505.test.ts
  8 tests (4 x 2 profiles). Cites: RFC4505-2-1, -2-2, -2-3, -2-5, -2-6,
  -3-1, -3-4. Skipped (untestable, out-of-band/internal-decision, see
  catalog notes): RFC4505-2-4/-5-1 (permission-before-sending --
  out-of-band user consent), -3-2/-3-3 (no-mapping/no-normalization --
  internal-decision), -3-5 (bidirectional check -- internal-decision).

  Base64 derivation: email "fred@example.com" -> ZnJlZEBleGFtcGxlLmNvbQ==;
  RFC 4505 Section 4's own worked-example token "tim" -> dGlt; empty
  trace -> "" (zero-length, accepted as "=" or a bare empty line).

  Matcher-rigor: rejects a token containing '@' (violates the
  email-or-non-'@'-token dichotomy), a token exceeding 255 Unicode
  characters, non-base64 wire bytes, and a StringPrep-prohibited
  ASCII control character (U+0001) surviving verbatim in the prepared
  message.

test/compliance/specs/ext/sasl-external-4422a.test.ts
  8 tests (4 x 2 profiles). Cites: RFC4422-A.1-1, -A.1-2, -A.1-3 (all
  three Appendix A entries; none skipped).

  Base64 derivation: authzid "userA" -> dXNlckE=; empty authzid -> ""
  (zero-length initial response).

  Matcher-rigor: rejects a non-empty response when an empty authzid
  was requested (and vice versa), a response containing a NUL byte,
  invalid UTF-8, and a second unsolicited AUTHENTICATE data line after
  the single initial-response/challenge-response pair (A.1-3's
  exactly-one-round-trip rule). Both client-first sequences A.1-1
  permits (inline SASL-IR vs. bare AUTHENTICATE + server empty
  challenge) are scripted as separate legs.

yarn test:compliance: problems: [] (clean). yarn test: only the
pre-existing test/unit/newline.transform.test.ts failure remains
(229 passed, 1 failed, unchanged baseline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes all 21 untested-testable catalog ids flagged by a Phase 6 spec review,
restoring the compliance suite's zero-untested-testable invariant. Builds on
the review's 3 uncommitted header-comment fixes (sasl-scram-5802-7677,
referrals-2221-2193, inprogress-9585 — gap disclosures, kept as-is).

Coverage added per file:

- sasl-scram-5802-7677.test.ts (RFC5802/RFC7677): RFC5802-3-2, -5.1-1,
  -5.1-3, -5.1-5, -5.1-11, -5.1-14, -5.1-15, -5.1-16, -6-2, -6.1-1, -6.1-2,
  RFC7677-4-1. All self-actualizing (authenticate() throws
  NotImplementedError) with tight wire matchers scripting the exact SCRAM
  exchange fragment each duty binds (authzid escaping, SASLprep-or-refuse,
  m= rejection, cbind-data content, no-final-message-on-failure, extension
  handling, 'p' flag selection, tls-unique default, -PLUS session-hash
  requirement) so a wrong future implementation would fail these once
  AUTHENTICATE lands.

- referrals-2221-2193.test.ts (RFC2221/RFC2193): RFC2221-3-1
  (self-actualizing — no referral-following surface exists; LOGIN throws
  NotImplementedError before an 11-hop chain could even be scripted) and
  RFC2193-3-1 (REAL pass — a non-IMAP-scheme URL in a REFERRAL resp-code
  still surfaces with kind === "REFERRAL" and the session continues
  normally, confirming the client is "prepared for" an arbitrary URL type).

- appendlimit-7889.test.ts (RFC7889): RFC7889-2-1 is a REAL pass — probed
  and confirmed that CapabilityList.add() (src/parser/structure/capability.ts)
  stores each CAPABILITY token verbatim, uppercased, as its own Map key, so
  driver.hasCapability("APPENDLIMIT=1234") and
  driver.hasCapability("APPENDLIMIT") are genuinely distinguishable today —
  this closes a stale "no dedicated capability-parsing surface" claim in the
  file's old header. RFC7889-3.2-2 and RFC7889-4-2 are self-actualizing
  (status()/list()/append() all throw NotImplementedError).

- listmyrights-8440.test.ts (RFC8440): RFC8440-6-1 (self-actualizing —
  list() throws NotImplementedError; scripts a scoped-vs-wildcard LIST
  RETURN (MYRIGHTS) pattern matcher).

- preview-8970.test.ts (RFC8970): RFC8970-3.3-1 and RFC8970-4.2-1
  (self-actualizing — fetch()/uidFetch() throw NotImplementedError
  unconditionally; script the UTF-8/no-content-transfer-decoding duty and
  the re-request-cadence duty respectively).

- inprogress-9585.test.ts (RFC9585): RFC9585-5-1 is a REAL, non-vacuous
  VIOLATION — empirically probed (throwaway harness script, since removed)
  that src/parser/structure/text.code.ts's match() keys its resp-text-code
  kind off the raw, un-case-folded lexer token value with no normalization
  step. A scripted lower-case '* OK [inprogress ("A001" 175 NIL)]' produces
  a parsed serverStatus event with content.text.code.kind === "inprogress"
  (verbatim lower-case), not "INPROGRESS" — a genuine, wire-observable
  failure to accept the resp-text-code case-insensitively, distinct from
  RFC9585-4-1's canonical-case pass.

Verification: yarn test:compliance run to completion; reconciliation
report confirms untested-testable: 0 and problems: [] (both required
invariants restored). No catalog entries were re-tagged — every one of the
21 ids proved genuinely testable once probed/scripted correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promote all Phase 6 tokens from PENDING to live cataloged registry entries:
the 13 scope tokens (LANGUAGE, I18NLEVEL=1/2, CONVERT, URLAUTH,
URLAUTH=BINARY, CHILDREN, AUTH=SCRAM-SHA-1, AUTH=SCRAM-SHA-256,
AUTH=ANONYMOUS, AUTH=EXTERNAL, LOGIN-REFERRALS, MAILBOX-REFERRALS,
X-GM-EXT-1), the 5 reconciliation-delta tokens (APPENDLIMIT, STATUS=SIZE,
LIST-MYRIGHTS, PREVIEW, INPROGRESS), and UTF8=ONLY (source RFC6855 w/ the
RFC 9755 maintenance note), each with measured-outcome notes.

UPGRADE specs/meta/registry-coverage.test.ts from internal-consistency to
the LIVE IANA cross-check: every token in the committed iana-snapshot.ts
(78 tokens) must have a coverage entry; every cataloged source must resolve
to a module that actually carries requirements (not an empty skeleton);
every non-cataloged entry must carry an explanatory note; no duplicate
capability. Case-insensitive token match (IMAP capabilities are
case-insensitive; IANA renders IMAP4REV1 vs the conventional IMAP4rev1).
Meta-tests 15/15 green. The registry checklist is now COMPLETE - every IANA
IMAP-capabilities token is reconciled to an auditable status.

Suite state: problems=0, untested-testable=0, 960 requirements, 61 sources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Append the closing untestability delta: 32 new untestable entries across 6
existing themes (internal-decision +18 dominant, out-of-band +6), no new
theme. Records the FINAL combined totals - 249 untestable across the
960-requirement catalog, taxonomy final at 11 themes (Phases 4-6 added 116
untestable entries with zero new themes, confirming completeness).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@LoveAndCoding

Copy link
Copy Markdown
Owner Author

IMAP Spec-Compliance — Progress Report

A black-box specification-compliance test suite for the modern-api client is now in place (see #13, which targets this branch). It drives the client only through its public API and scores every requirement as a verbatim RFC quote with its RFC 2119 level, independently under both IMAP4rev1 and IMAP4rev2. Tests are self-actualizing — they encode each duty's exact wire behavior, so an unimplemented feature reads as unimplemented today and becomes a live pass/fail the moment it ships. No src/ code is modified.

Where the client stands

960 requirements across 61 sources, both profiles (1,581 requirement×profile cells):

outcome cells meaning
✅ pass 217 genuine, measured compliance
❌ violation 118 measured non-compliance
⚪ unimplemented 819 feature not yet present
➖ untestable 427 excluded from scoring, each with a documented rationale

Of the duties the client actually has a surface for today (pass + violation), ~65% pass. Reporter integrity: problems: 0, zero testable requirements left untested, and all 78 IANA "IMAP Capabilities" tokens reconciled to an auditable status (enforced by a live cross-check).

Coverage breakdown (960 requirements / 61 sources)

Core protocol — 288

RFC scope reqs
3501 IMAP4rev1 111
9051 IMAP4rev2 173
2971 ID 4

Connection & security — 169

RFC capability reqs
8314 implicit-TLS BCP 32
2595 STARTTLS / LOGINDISABLED 20
9525 server identity (certs) 25
7817 email server identity 8
4422 SASL framework + EXTERNAL 19
4616 / 2195 / 7628 AUTH=PLAIN / CRAM-MD5 / OAUTHBEARER 7 / 4 / 6
XOAUTH2 AUTH=XOAUTH2 (vendor) 6
4959 / 5161 SASL-IR / ENABLE 3 / 4
4978 / 8437 COMPRESS=DEFLATE / UNAUTHENTICATE 7 / 10
7888 / 6855 LITERAL+/- / UTF8=ACCEPT 5 / 13

Mailbox, listing & metadata — 136

RFC capability reqs
4315 / 6851 UIDPLUS / MOVE 7 / 10
2342 NAMESPACE 5
5258 / 5819 / 6154 LIST-EXTENDED / LIST-STATUS / SPECIAL-USE 9 / 4 / 7
4314 / 9208 / 5464 ACL / QUOTA / METADATA 8 / 10 / 17
8514 / 8474 SAVEDATE / OBJECTID 6 / 20
3502 / 4469 / 3516 MULTIAPPEND / CATENATE / BINARY 7 / 10 / 7
8508 REPLACE 9

Search, sort, sync & events — 180

RFC capability reqs
7162 CONDSTORE / QRESYNC 43
5256 / 5957 SORT / THREAD / SORT=DISPLAY 19 / 3
4731 / 5267 / 5182 ESEARCH / ESORT+CONTEXT / SEARCHRES 9 / 24 / 7
6203 / 9394 / 5032 FUZZY / PARTIAL / WITHIN 12 / 10 / 4
2177 / 5465 / 5466 IDLE / NOTIFY / FILTERS 7 / 27 / 15

i18n, misc & vendor — 187

RFC capability reqs
5255 / 5259 LANGUAGE+I18NLEVEL / CONVERT 21 / 28
4467 / 5524 URLAUTH / URLAUTH=BINARY 17 / 7
5802 / 7677 / 4505 SCRAM-SHA-1 / SHA-256 / ANONYMOUS 29 / 2 / 12
2221 / 2193 / 3348 LOGIN-REFERRALS / MAILBOX-REFERRALS / CHILDREN 8 / 11 / 3
X-GM-EXT-1 Gmail vendor extensions 24
7889 / 8438 / 8440 APPENDLIMIT / STATUS=SIZE / LIST-MYRIGHTS 6 / 3 / 4
8970 / 9585 PREVIEW / INPROGRESS 6 / 6

Beyond the cataloged sources, every remaining IANA capability token is reconciled with an explicit status (no-client-requirements, obsoleted-by, or out-of-scope), so no registry entry is silently dropped.

claude and others added 20 commits July 11, 2026 11:36
…ator)

Structures the four baseline review roles as Claude Code subagents in
.claude/agents/ so they're directly invocable via the Agent tool, with a
shared findings template as the single source of truth for output format.
Lens-specific agents are left for a future iteration; review-runner spins
up ad-hoc lens sub-agents until predefined ones exist.
context-gathering/review-runner get sonnet (comprehension and
coordination work); review-validator/review-orchestrator get opus, since
they're the trust-critical gatekeeper and the final human-facing output
respectively. Pinning also keeps review rigor consistent regardless of
what model the invoking session happens to be using.
Adds 11 predefined review lenses (correctness, security, test-quality,
error-handling, api-compatibility, performance, maintainability, state,
fidelity, dependency, spec-compliance), each structured as categories of
things to check with grounding examples rather than a numbered procedure —
lenses are a taxonomy applied to code, not a workflow, so the review-runner
and review-orchestrator steps are the only agents in this set that use
sequential steps.

review-runner now selects 2-5 of these predefined lenses per group instead
of defining lenses ad hoc, with a fallback to a custom lens for risk areas
none of them cover. review-orchestrator's rules updated to match.
- All 11 lenses: findings are no longer implicitly limited to confirmed
  bugs. Category output guidance loosened from a narrow 2-3 value mapping
  per lens to open, example-only lists that include Assumption,
  Uncertainty, and Improvement alongside Bug — strict category lists were
  constraining what lenses would surface.
- api-compatibility-lens: added a Developer Experience category (API shape
  value, ergonomics, clarity to consumers); internal-only code worth
  exposing is now explicitly in scope instead of excluded; Category list
  now includes Versioning and Documentation as first-class options.
- maintainability-lens: Abstraction Fit now explicitly hunts for
  over-engineering (a common agentic-coding failure mode) with a
  grounding example; Readability now covers comment verbosity and
  redundant comments, not just staleness.
- test-quality-lens: added a Test Necessity category — overtesting
  (fragile, low-value tests) is flagged as a real cost, not just gaps.
- review-runner: lens selection changed from a 2-5 cap to "at least 2,
  no fixed upper bound" so large/high-risk groups aren't under-scoped.
Review reliability note (PR #13 review, M1): orchestration runs stalled
because sub-agents could be spawned in the background. Both spawning
agents (review-orchestrator, review-runner) now explicitly require
run_in_background: false on every Agent call, with parallelism achieved
by batching synchronous calls in a single message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M17/H1 from the PR #13 review: the 'if (i > 0)' guard meant a
completed context at index 0 was never spliced out, so a completed
isolated command pinned its context and the next context's commands
never ran (run() early-returns on the still-'running' stale context).
The queue also never emitted 'idle'.

Guard is now 'i > -1', with a regression test covering the
isolated-command-then-followup path and idle emission, plus assertions
that isIsolated actually propagates through CommandQueue.add().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses H2 from the PR #13 review: ComplianceReporter rewrote
compliance.json/COMPLIANCE.md on every invocation, so a filtered
'vitest run <file>' (or an interrupted run) clobbered the report —
marking every requirement that simply didn't run as 'untested'.

The reporter now walks specs/**/*.test.ts and only writes when every
spec file was part of the run and it wasn't interrupted; it also
refuses to write when a complete run yields zero compliance-annotated
records (broken meta plumbing would otherwise report everything
untested). Both guards print an explicit diagnostic instead of
silently overwriting, and are covered by new reporter tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M8 from the PR #13 review: drainLines() only ran the bare-LF
check when the buffer contained no CRLF anywhere, so a bare-LF line
arriving in the same chunk as a later valid CRLF line was silently
absorbed into that line's segment. The check now flags any bare LF
that appears before the first CRLF, and gains direct harness tests
(previously disabling the check left every meta-test green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M9 and M11 from the PR #13 review:
- selectExchange (rev1) defaulted to exists=0 with UNSEEN 1, scripting
  '* 0 EXISTS' + '* OK [UNSEEN 1]' — a jointly-invalid state no real
  server sends. UNSEEN is now omitted for an empty mailbox unless the
  caller explicitly sets it.
- The BODY[1.MIME] FETCH fixtures declared {42} for a 44-byte literal
  (the trailing CRLF was uncounted); a correct client would read 42
  octets and desync. Both occurrences now declare {44}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M5 from the PR #13 review: the README claimed the
expired-cert fixture was 'intentionally absent until Phase 3', but the
same directory ships expired-cert.pem/expired-key.pem and generate.sh
has the expired block. Also invoke the script as 'sh generate.sh'
since it is committed non-executable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M7 from the PR #13 review: 'tsc -p test/compliance --noEmit'
produced 50+ errors (import.meta under the inherited commonjs module
setting, unresolvable node:* specifiers under @types/node@12, and
third-party .d.ts written for newer TypeScript), so the scoped
tsconfig's only purpose — editor/CI type-checking — was non-functional.

- module es2020 + moduleResolution node (import.meta support without
  flipping useDefineForClassFields for the pulled-in src/ files)
- skipLibCheck for the vitest/vite/rollup .d.ts written for newer TS
- local shim declaring node:* specifiers until @types/node is bumped
- serverInfo() now returns ReadonlyMap, matching Session's actual type
- new 'yarn typecheck:compliance' script runs the check

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M10 from the PR #13 review: connect()/connectLow() returned
the client promise with no independent timeout or teardown, so a hung
TLS connect (the client never settles on rejected certificate
identity) died at vitest's generic 'Test timed out' with no diagnosis,
and end() could hang on the half-open client.

Both connect paths now race the client promise against a backstop
timer (client timeout + 1s so the client's own timeout gets a head
start). On expiry the driver rejects with a diagnosable message and
best-effort tears the client down. Covered by a new driver TLS test
that asserts identity rejection produces a rejection, never a hang.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M12/M13/M21 and the M3 count assertion from the PR #13
review — properties the suite relied on but never exercised:

- command(): multi-token verbs now require exactly one SP between
  tokens (a doubled SP was collapsed, hiding UID-prefixed framing
  bugs) and preserve args spacing instead of re-joining tokens; both
  behaviors plus exact-string args matching gain direct tests.
- Transcript.clientLines() — the primitive behind every credential-
  redaction assertion — gains its own harness tests.
- The LITERAL+ test now asserts no continuation was sent; the
  chunked-send test now asserts chunking actually happened.
- New reporter guard: on a complete run, any testable-but-untested
  requirement becomes an explicit problems[] entry, enforcing the
  'untested-testable = 0' headline invariant.
- New meta-test: every catalog module file on disk must be registered
  in allCatalogModules exactly once (an unwired file was silently
  unscored), and sources must be unique.
- New meta-test pinning the IANA reconciliation tallies (92 coverage
  rows = 73 cataloged + 5 + 5 + 9; 78 snapshot tokens) so documented
  numbers can't drift from code.
- compliance-test.ts and acceptance-table.ts now share one
  registerCompliance() core (title construction, meta assignment,
  classify-and-rethrow), and the classification wiring is unit-tested
  via runComplianceBody.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
# Conflicts:
#	yarn.lock
…onfig

- Switch package manager from yarn to npm (remove yarn.lock, add
  package-lock.json, update CI to npm ci)
- Migrate the full test suite off jest onto vitest: new root
  vitest.config.ts covers unit/integration/e2e, jest.fn/mock/MockedClass/
  MockedFunction usages converted to vitest's vi equivalents, jest/ts-jest/
  @types/jest removed
- Bump TypeScript to ^6.0.3 (not 7.x - that hard-crashes typescript-eslint
  8.x today) and @types/node to ^22.9.4; fix the ~180 stricter compile
  errors this surfaced without changing runtime behavior, including a few
  genuine latent bugs found along the way (a copy-paste index bug in
  parser/encoding.ts, an unguarded socket access race in connection.ts,
  an uninitialized FlagList.hasWildcard)
- Raise the Node engines floor to >=22 (18/20 are both past EOL and
  vitest 4.x/ESLint 10 require >=20 anyway); update CI's Node matrix to
  22.x/24.x
- Migrate ESLint to flat config (eslint.config.js) with ESLint 10 + the
  unified typescript-eslint 8 package, replacing .eslintrc.js/.eslintignore;
  preserve the old warn-level severity for no-explicit-any/no-unused-vars
  since the v8 recommended config promotes them to errors by default
- Delete the dead pre-TypeScript test/test.js + test-connection-*.js
  files (they called a since-removed default Imap export); the specific
  historical-bug scenarios they encoded are written up in
  docs/superpowers/specs/2026-07-12-legacy-regression-scenarios-to-reverify.md
  for re-verification once FETCH/IDLE are (re)implemented
- Add prettier as a real, runnable devDependency (config already existed
  but the package was never installed); wire the compliance suite into CI
  (it existed but was never run there); fix vitest's reportOnFailure
  default so coverage still gets reported when a test fails, matching
  jest's old behavior
Proposes the three-layer public interface (ImapClient/MailboxSession over
typed command classes over Connection/parser), the developer experience it
targets, and works backwards to the implementation subsystems (CommandWriter,
response router, queue state machine, TLS identity, parser hardening, SASL
framework) and a six-milestone phasing plan driven by the compliance suite,
grounded in today's measured report (960 requirements, ~23% MUST score,
73 distinct violations).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E67Ejgvhi7ZnBnacKcuqGU
All recommended options adopted: ImapClient replaces Session outright with
no alias (unreleased code, version bumps to 1.0 at M6), UID-first message
operations, extension facets, Connection/response classes public at 1.0 with
lexer internals package-private, satisfied-by-mechanism adjudication for
product-decision SHOULDs, and suite-annotation churn as milestone DoD.
Status updated to Accepted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E67Ejgvhi7ZnBnacKcuqGU
The spec (docs/superpowers/specs/2026-07-12-modern-api-spec.md) pins down
the implementable contract: package exports and target source layout, the
full ImapClient/MailboxSession/facet signatures, the client state machine
transition table, the connect() ritual, error hierarchy, SequenceSet/
search-criteria/fetch data types, typed response-code registry, queue
pipelining modes and the literal gate, the Command/CommandWriter/
ResponseCollector Layer-2 contracts, response-router claim ordering, the
SASL framework and mechanism-selection algorithm, the single TLS policy
module, the parser-hardening contract, and a table of 13 compliance
invariants mapped to catalog requirement IDs.

The plan (docs/superpowers/plans/2026-07-12-modern-api-implementation-plan.md)
sequences it: ground rules (compliance ratchet, per-task definition of done,
driver wiring, no parallel wire-writers), M0 and M1 at task granularity with
files and which catalog requirements each task flips, M2-M6 scoped with exit
criteria, the dependency graph, and standing risks. Also corrects two RFC
number mixups against the catalog (RFC 9585 is INPROGRESS; UIDONLY is RFC
9586) and schedules catalog extractions for the two registry entries the
suite currently defers (RFC 3691 UNSELECT in M2, RFC 9586 UIDONLY in M5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E67Ejgvhi7ZnBnacKcuqGU
- Rename the 'enable' config option to 'extensions' across proposal, spec,
  and plan: 'enable' reads as an on/off switch for the client itself rather
  than a list of server extensions to ENABLE.
- Add spec §5.6 'Closed vocabularies': RFC-closed value sets become
  string-literal unions (SpecialUse, SystemFlag/Flag, SortKey — previously
  referenced but undefined — and ThreadAlgorithm), with a two-grade rule:
  strict unions for client-sent values, Known | (string & {}) for
  server-sent positions so unknown wire values stay type-safe data.
  Applied to create(), MailboxInfo, the flag methods, and thread().
- Drop the ~800-line PR size cap from the plan's ground rules: PRs are
  sized by coherent unit of work; automated review tooling absorbs large
  diffs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E67Ejgvhi7ZnBnacKcuqGU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants