Repository navigation
fix(sync): a draft synced from Outlook or Gmail opens in the editor, and one sent elsewhere stops being a draft - #164
Merged
Conversation
…s in the editor The editor, the send queue and the thread read "sentAt is set" as "has gone out". The Graph, Gmail and IMAP builders stamped every synced message with sentAt (Outlook: sentDateTime ?? receivedDateTime, which a draft has), so a draft written in another client was listed in the thread and then refused by the editor with a 404 (ComposeController::assertEditableDraft). The builders now leave sentAt null for a draft, the same state a draft written here has. receivedAt is unchanged. IMAP compares the flag canonically, since webklex reports `Draft` from some servers and `\Draft` from others. ReadReceiptPolicy::decide used "has sentAt" to recognise our own mail; a draft no longer has one, so it checks the draft flag as well. Visible change: MessageThreader skips an unsent draft when moving a thread's lastMessageAt, so a synced draft no longer floats its thread up the list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…raft() reads it webklex strips the backslash from a server's flags, so a real `\Draft` is stored as `Draft` and is not a draft to Message::isDraft(). Matching it canonically gave a row that was neither a draft nor sent. sentAt now follows the stored flag exactly, and the changelog no longer claims IMAP drafts. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sync stores a draft once and never rebuilds a stored row, so sending it
elsewhere left it flagged \Draft with no sentAt. Now that a synced draft has no
sentAt the editor opens it (it used to 404, which was an accidental guard), so
the stale row could be opened, sent a second time, and discarded, which deletes
the sent copy at the provider.
Message::settleAsSent() is the one place that says what "was sent" means for a
stored draft: flag off, sentAt set, thread ranked by it. It is called when
- Graph files a stored message in Sent Items (same id, immutable ids),
- the Graph batch handler re-points a stored message whose fetched resource
says isDraft = false (new id, no immutable ids),
- Gmail enrichment re-reads a stored draft whose labels no longer include
DRAFT (before the flag refresh).
Trash and Drafts do not settle: a draft thrown away or put back is unsent.
RemoteFlagState::storedFlags() rebuilt Gmail's and Graph's flag mirror from the
read and starred facts alone, which dropped \Draft the first time a stored draft
was re-read, despite the refresh's own comment saying it survives. It now
carries the flag over.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…sent Also drops three needless nullsafe calls from the settle test (phpstan). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…w what counts as sent Review of the settle change: - A draft stored before synced drafts were kept unsent already has a sentAt. settleAsSent() ranked the thread by "now" instead, so the mass re-read an upgrade causes would float every old conversation. It keeps its sentAt and ranks by it. - Gmail settled on "DRAFT label absent"; Graph only on Sent Items. Gmail now needs SENT present and DRAFT absent. - The Graph handler settled on an absent isDraft; only an explicit false counts. - Comments that overstated (JMAP announcement of the move, "a delta never changes draft state") corrected. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s-editable # Conflicts: # CHANGELOG.md
The builders now leave a synced draft's sentAt null, but drafts already stored kept the receivedAt they were given as sentAt, so they still gave a 404 in the editor. The migration clears it only for drafts the provider still files as drafts (Gmail DRAFT without SENT, Graph Drafts without Sent): a stored draft that was sent elsewhere before the upgrade keeps its sentAt, so it cannot be sent a second time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s-editable # Conflicts: # CHANGELOG.md
With #162's guard, a re-report that moves nothing records nothing. A draft filed in Sent Items before settling existed still wears \Draft beside Sent, and its re-report is where it settles; that changes its keywords and the list's draft marker, so it now counts as a change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gration Doctrine prepares a migration's statement, and a ? is a placeholder to it: the jsonb ? operator reached Postgres as $1 and app-init failed on every upgrade. The test executed the SQL raw, which let it through; it now prepares it the way Doctrine Migrations does, and fails on the old statement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s-editable # Conflicts: # CHANGELOG.md
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.
Closes #135
Takes over #123 by @Flouss, whose fork was archived, so that branch can no longer be pushed to. All their commits are kept. Squash-merge with
Co-authored-by: Flouss.Changes on top of #123
Version20261015130000for drafts stored before the upgrade, which fix(sync): a draft synced from Outlook or Gmail opens in the editor, and one sent elsewhere stops being a draft #123 left at a 404. It clearssent_atonly on rows flagged\Draftthat the provider still files as drafts: GmailDRAFTwithoutSENT, Graph's Drafts label without Sent. A stored draft that was sent elsewhere before the upgrade keeps itssent_at, so it can't become sendable a second time. IMAP rows aren't touched (their flag isDraft, whichisDraft()doesn't read as a draft). Threads aren't re-ranked.down()is a no-op.SyncedDraftSentAtMigrationTestruns it against rows: six cases, each "cleared" with a "kept" partner. Mutation-checked: without the narrowing, the three kept cases fail.GraphDraftSentElsewhereTestnow buildsGraphApiSyncerwith the two arguments main added since (ThreadLabelSynchronizer, SyncNotifier). Without them PHPStan and the test failed.\Draft, and its re-report is where it settles. That is now counted as a change, so it's recorded for JMAP clients and the open list refreshes. Tested inGraphDraftSentElsewhereTest::testADraftAlreadyFiledInSentIsSettledAndRecordedOnItsReReport(fails without the change).How it was checked
Left as is
now), not the provider's send time. That only affects the sort time of a draft stored after this change.#123's own description:
🤖 Generated with Claude Code