Repository navigation
fix(platform): let bulk Inbox Send reach API threads and keep refusals - #4747
Merged
Merged
Conversation
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
force-pushed
the
fix/bulk-inbox-send-recovery
branch
from
October 11, 2026 04:11
aad75de to
123587b
Compare
yannickmonney
enabled auto-merge
October 11, 2026 04:13
yannickmonney
enabled auto-merge (squash)
October 11, 2026 12:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
replyToConversation) routeschannel: '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 theunknown@example.comstand-in, and the row projection gives API rows''. All of them now use one rule:hasReplyRecipientinlib/shared/conversations/reply-recipient.ts, shared by the door, the projection, the single reply and the bulk send. The door's behaviour is unchanged.handleSendMessagesclosed the dialog and calledonComplete([])whatever happened, souseInboxListcleared the selection, and the draft unmounted with the dialog.Behaviour
bulk.outcomeWithReason).onComplete(refused), the status verbs' contract). The title count follows, and Send retries only them.ConfirmDialog). A close that lands mid-send is not reopened by a refusal.contentas escaped HTML andsourceMarkdownas the literal text. It is now also proven for an API conversation without an address, through the reply door toqueueApiReply.The change also ships the EN/DE/FR keys (
conversations.bulkSend.refused*, noß), the EN/DE/FR product docs (docs/*/platform/index.md), manual boxCONV-F39(suite Cost 63 → 64) and an automation-register row.Evidence (local, isolated worktree on main
c043a64b)aad75de1(clean tree)single-failure-toast/error-message-description/domain-specsguards, i18n catalogsbulk-send-dialog.browser.test.tsxplus #4389's EmailPreview casesqueueApiReplylint:manualok (1670 boxes),check-guideon the conversations suite ok, docs locale/links/components checks 25/25tscof the changed fileslib/engine/core/**, from 3 packages missing in the read-only dependency donor (a harness limit). CI's Type check is authoritative.knip:checktools/cli/src/lib/config/releases/warning-policy.ts. Not in this change: main's own Knip job atc043a64bfails 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