fix(inbox): a thread page is the newest 200 messages the caller may read; cspell words (#11096 review)

Merged
EV
8d ago
+152-36

Fixes the Greptile P1 on #11096 ("Earlier thread messages disappear") and the Cspell failure on the same PR.

Scenario

InboxEmailService.thread() (since #11094, review F110) took the newest 200 messages of a thread and then dropped the ones the caller may not read. A former member of a shared mailbox (no current access, reads only what was addressed to them) whose thread received 200+ newer messages after they left got back only the message they opened, although earlier messages addressed to them remain in the thread. The old ascending query still showed them, and the route has no cursor to reach them.

Fix

  • thread() now builds the page with a TypeORM query builder and checks everything it lists before the LIMIT:
    • without current access to the mailbox: email.id = :emailId OR email.inboxItemId IN (SELECT recipient.inboxItemId FROM inbox_item_recipient WHERE userId = :userId AND tenantId = :tenantId). This is the same rule as before (addressed to the caller, or the message asked for), now applied in SQL;
    • the failed-inbound rule (NOT (status = FAILED AND direction = IN), the same SQL as countThreadMessages) moved into the query too, so failed inbound work takes no place in the page either.
  • Nothing else changes: same order (newest 200 by createdAt DESC, id DESC, returned oldest first), the message asked for is still added when it is older, and the access rules are the same. Only plain query-builder SQL is used (IN (:...), an uncorrelated IN (subquery) with no inner LIMIT, NOT (...)), so it is valid on PostgreSQL, MySQL and SQLite. Both inboxItemId columns are uuid.
  • .cspell.json: sigv (ses.provider) and tablist (inbox-page component) appended to words.

Tests

Added 2 specs to packages/plugins/inbox/src/lib/email/inbound/inbox-inbound.db.spec.ts (SQLite):

  • shows a former member their earlier messages of a thread that grew by a page after they left: Bob receives m-2 and m-3, then leaves; Eve becomes the member and gets m-4 plus 200 newer messages in the thread. Bob opening m-2 gets [m-2, m-3]. Before the fix he got only [m-2].
  • fills a thread page with the messages it lists: failed inbound work takes no place in it: 199 newer messages, then a failed inbound one and an unsent (failed OUT) reply. The page is 200 listed messages plus the older message asked for (201), without the failed inbound one. Before the fix it was 200 (the failed inbound one took a slot).

Fail-before / pass-after: both new specs failed against the unfixed service (Received: ["<m-2@acme.test>"]; Received length: 200) and pass with the fix.

NODE_OPTIONS="--experimental-vm-modules --max-old-space-size=6144" node node_modules/jest/bin/jest.js -c packages/plugins/inbox/jest.config.ts packages/plugins/inbox gives 26/26 suites, 251/251 tests passed.

cspell on the ses provider + spec, the inbox-page component, the service and the changed spec finds 0 issues. Prettier --check passes on the changed files.

šŸ¤– Generated with Claude Code


Summary by cubic

Fixes the thread page so former members of a shared mailbox can still see their earlier messages even when newer mail pushes the thread past a page. The page is now built from the newest messages the caller may read, instead of the newest messages filtered down afterwards.

  • The recipient access check (no current access: messages addressed to the caller, plus the one asked for) now runs in the SQL query before the LIMIT, as a subquery on inbox_item_recipient.
  • The failed-inbound exclusion (NOT (status = FAILED AND direction = IN)) moved into the query too, so failed inbound work no longer takes a slot in the page.
  • Order, sorting, and access rules are unchanged; only query-builder SQL is used, valid on PostgreSQL, MySQL, and SQLite.
  • Adds sigv and tablist to .cspell.json.

Tests

  • New spec: a former member of a thread that grew by a full page after they left still gets both of their earlier messages.
  • New spec: a page with failed inbound and unsent OUT messages lists a full page of readable messages plus the older one asked for, without the failed inbound message.