Skip to content

fix(platform): let bulk Inbox Send reach API threads and keep refusals - #4747

Merged
yannickmonney merged 1 commit into
mainfrom
fix/bulk-inbox-send-recovery
Oct 11, 2026
Merged

yannickmonney merged 1 commit into
mainfrom
fix/bulk-inbox-send-recovery

Conversation

@yannickmonney

Copy link
Copy Markdown
Contributor

Bulk Inbox Send messages now reaches a conversation mirrored over the REST API that has no email address, the same way a single reply does (#3912). A refused send keeps the dialog, the message and only the refused conversations, so Send retries those alone and never re-sends to a conversation that already got the message (#3924).

Root causes

  • bug(platform): bulk Inbox Send rejects valid API conversations #3912: "who a reply reaches" existed in three copies. The reply door (replyToConversation) routes channel: 'api' to the source's queue before its email check. The single reply (conversation-panel.tsx) exempts API threads. The bulk hook refused every row whose email was blank or the unknown@example.com stand-in, and the row projection gives API rows ''. All of them now use one rule: hasReplyRecipient in lib/shared/conversations/reply-recipient.ts, shared by the door, the projection, the single reply and the bulk send. The door's behaviour is unchanged.
  • Bug: bulk Inbox send discards drafts and failed recipients after refusal #3924: handleSendMessages closed the dialog and called onComplete([]) whatever happened, so useInboxList cleared the selection, and the draft unmounted with the dialog.

Behaviour

  • Full success: unchanged. One summary toast, then the dialog closes and the selection clears.
  • Any refusal:
    • The same truthful summary toast: counts plus the first refusal's words (bulk.outcomeWithReason).
    • The dialog stays open with the message as typed.
    • An inline destructive Alert names each refused conversation and why (the import dialogs' pattern, up to 10 listed, then a count).
    • Focus returns to Message. Send was disabled while it ran, which took focus off it.
  • Selection after a refusal:
    • When something went out, only the refused conversations stay selected (onComplete(refused), the status verbs' contract). The title count follows, and Send retries only them.
    • When nothing went out, the selection stays as it was.
  • Recipients are fixed when Send is pressed: a list refresh does not retarget a running send. A refreshed row is read again on retry, for example once a contact gains an address.
  • No recipients: with no selected conversation left in the list, Send is disabled.
  • While sending: Escape and Cancel stay held (shared ConfirmDialog). A close that lands mid-send is not reopened by a refusal.
  • Plain text: fix(platform): preserve bulk Inbox plaintext in HTML sends #4389's escaping is unchanged, content as escaped HTML and sourceMarkdown as the literal text. It is now also proven for an API conversation without an address, through the reply door to queueApiReply.

The change also ships the EN/DE/FR keys (conversations.bulkSend.refused*, no ß), the EN/DE/FR product docs (docs/*/platform/index.md), manual box CONV-F39 (suite Cost 63 → 64) and an automation-register row.

Evidence (local, isolated worktree on main c043a64b)

Check Scope Result
jsdom suites at head aad75de1 (clean tree) 10 files: bulk hook, send summary, dialog, refused dialog, Home list + wire flow, panel, Inbox page, failure toasts, selection 116/116 pass
Server suites at head shared rule, projection, reply door, bulk door from the hook, routes, single-failure-toast / error-message-description / domain-specs guards, i18n catalogs 274/274 pass
Real Chromium at head (Chrome for Testing 145.0.7632.6, r1208) bulk-send-dialog.browser.test.tsx plus #4389's EmailPreview cases 8/8 pass
Fail-before: the same tests over main's product bytes (restored by sha256) jsdom 14 fail (every new regression and the 2 changed assertions). 55 pass: the controls, including the single-reply boundary, a refresh during a send and a second Send while pending.
native door 1 fail: the API row without an email never reaches queueApiReply
Chromium 2 fail: the API row is refused (2 of 3 sent), and there is no Alert after a refusal. 1 pass: the refresh control.
Mutants on the candidate, each restored by sha256 5 All killed: no API lane (7 tests), no focus return (2), clear on partial (6), narrow when nothing went out (2), close on refusal (5)
Commit hooks husky pre-commit (lint-staged: conflict markers, oxfmt, opengrep) and commit-msg (commitlint) pass
Static changed files type-aware oxlint clean (16 TS files), opengrep 0 findings, lint:manual ok (1670 boxes), check-guide on the conversations suite ok, docs locale/links/components checks 25/25
Isolated tsc of the changed files 0 errors in changed files. 50 errors appear only in lib/engine/core/**, from 3 packages missing in the read-only dependency donor (a harness limit). CI's Type check is authoritative.
knip:check 1 unused export in tools/cli/src/lib/config/releases/warning-policy.ts. Not in this change: main's own Knip job at c043a64b fails the same way (job).

Local runs used a read-only dependency donor whose lockfile differs from main's by 19 added, 2 removed and 44 changed packages, none in the test toolchain. Chromium is the exact r1208 headless shell, reused read-only (sha256 27e55cc3…). Not run locally: Postgres integration (no SQL statement changes), the full-workspace typecheck and E2E.

No provider was contacted, and nothing was delivered. The reply door ran with fake SQL and a stubbed API queue. Browser tests use an in-memory reply write. Real delivery to a mailbox or an API source stays manual (CONV-F39, CONV-F11).

Closes #3912
Closes #3924
Refs #3920

Bulk Send refused every conversation without a contact email before
dispatch, although the single reply and the reply door answer a
conversation mirrored over the REST API through its source, without an
address. The three copies of that rule now share one,
hasReplyRecipient in lib/shared/conversations/reply-recipient.ts, used
by the reply door, the Inbox row projection, the single reply and the
bulk send.

A refused bulk send closed its dialog and cleared the selection, so the
message and the failed recipients were lost, and selecting the same
conversations again re-sent to those that already had the message. The
summary toast stays as it was. On a refusal the dialog now stays open
with the message, names each refused conversation and why, and returns
focus to the message. Once something went out, only the refused
conversations stay selected, so Send tries those alone; when nothing
went out, the selection stays as it was. A full success still closes
the dialog and clears the selection.

The plain-text escaping from #4389 is kept and is now also covered for
an API conversation without an address.

Closes #3912
Closes #3924
Refs #3920
@yannickmonney
yannickmonney force-pushed the fix/bulk-inbox-send-recovery branch from aad75de to 123587b Compare October 11, 2026 04:11
@yannickmonney
yannickmonney added this pull request to the merge queue Oct 11, 2026
@yannickmonney
yannickmonney removed this pull request from the merge queue due to a manual request Oct 11, 2026
@yannickmonney
yannickmonney added this pull request to the merge queue Oct 11, 2026
@yannickmonney
yannickmonney added this pull request to the merge queue Oct 11, 2026
@yannickmonney
yannickmonney added this pull request to the merge queue Oct 11, 2026
@yannickmonney
yannickmonney added this pull request to the merge queue Oct 11, 2026
@yannickmonney
yannickmonney added this pull request to the merge queue Oct 11, 2026
@yannickmonney
yannickmonney enabled auto-merge (squash) October 11, 2026 12:02
@yannickmonney
yannickmonney merged commit 4d9268d into main Oct 11, 2026
64 checks passed
@yannickmonney
yannickmonney deleted the fix/bulk-inbox-send-recovery branch October 11, 2026 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: bulk Inbox send discards drafts and failed recipients after refusal bug(platform): bulk Inbox Send rejects valid API conversations

1 participant