Sending an edited draft destroys it server-side, but the viewer kept
rendering the deleted message after the composer closed: the mailbox
list refreshed (row gone, counter down) while selectedEmail still
pointed at the destroyed draft. Clear the selection like a delete does
- and return to the list on mobile - when the email on display is the
draft the compose session was opened from or its latest autosaved
successor (autosave replaces the draft under a fresh id on every save,
so matching the sent draftId alone would miss).
A delayed send composed from a shared/group identity is created in the
*shared* JMAP account, because sendEmail() passes the target account to
getSubmissionAccountId(). Every read/write on the scheduled surface,
however, asked the primary submission account only:
getScheduledEmails / cancelEmailSubmission / rescheduleEmailSubmission
-> getSubmissionAccountId() // no argument = primary account
So a message scheduled from a shared address is accepted and stays
pending on the server — it *will* be delivered at sendAt — while the UI
cannot list it, cancel it or reschedule it. Cancelling appears to work:
EmailSubmission/set against the primary account answers `notFound` in
notUpdated, which the client never surfaced, and the mail goes out
anyway. Silent unrecallable mail is worse than a failed send.
Stalwart advertises urn:ietf:params:jmap:submission (with FUTURERELEASE
and a non-zero maxDelayedSend) on group accounts as well as personal
ones, so scheduling from a shared address is legitimate.
Data layer:
- getSubmissionAccountIds() enumerates every submission-capable account;
getScheduledEmails() queries each, tags every result with the account
that owns it, and fetches each submission's Email from that same
account (it lives beside the submission, not in the primary account).
It also returns per-account totals computed over all pending
submissions, so a badge stays correct past the first page.
- resolveSubmissionAccountId() locates a submission's owning account so
cancel/reschedule (and the sendAt/envelope helpers) act on the right
one. Callers that know the account pass it and skip the lookup.
- sendEmail() reports the submission's account in SendEmailResult, and
the pending-undo record carries it, so the "Undo send" / "Send now"
toast actions — which know only a submission id — address the right
account directly rather than searching for it inside the undo window.
- The delayed-send gate considers any reachable submission account
rather than only the primary one.
UI:
- Each shared account gets its own "Scheduled" sidebar row, scoped to
that account; the own row keeps showing the combined view. The scope
travels beside the virtual mailbox id, so the store's single
__scheduled__ id is unchanged, and it also drives which row renders as
selected and whether the scoped list has more to load.
Tested against a real Stalwart (integration/tests/11-shared-scheduled-send):
two server-truth tests pin the mechanism (the submission is invisible to
the primary account, and cancelling through it is a silent no-op) and two
UI tests cover the fix. All three UI-level assertions — the combined
list, the scoped row, and which row is selected — fail without this
change.
The Pro-mode bridge that turns an inline compose intent into a compose
tab derived its working title from the draft subject or, failing that,
the selected email's subject - regardless of mode. For reply/forward the
selected email is the message being acted on; for a fresh compose it is
whatever happens to be highlighted in the list, so "New Email" opened
with an unrelated subject in the tab strip (#856). Extract the rule into
buildComposeTabTitle: a fresh compose only ever uses its own draft
subject (mailto, resumed draft) and otherwise starts on the "New
message" fallback. Typing a subject already renames the tab live via
handleSaveState, so nothing else changes.
Opening a mailto: link while the composer held unsaved text used to
replace the session outright and drop the text on the floor. The mailto
entry point now hands a follow-up to the composer's dirty-aware close
handler, so the "Save or discard draft?" dialog runs first: save and
discard continue into the mailto draft, cancel leaves the current draft
untouched. When a failed save hands the text back to the continue-draft
slot (#702), that rescue wins and the follow-up is dropped.
Composing a new message while a shared/group folder was selected started
it as the reaching login's primary address instead of the shared one,
even when a matching send-as identity existed.
`composeFromAccountEmail` was resolved from
`getAccountById(viewingAccountId ?? activeAccountId)`. A delegated
mailbox has no separate login and no AccountEntry, and
`setViewingAccount` is only ever called with `null`, so selecting a
shared folder leaves both ids pointing at the primary account:
`findComposeIdentityId` was handed the wrong address and correctly
matched the primary identity.
Resolve the address from the selected mailbox instead, falling back to
the account lookup for own folders. `accountName` is the JMAP
`Account.name`, which RFC 8620 describes as "e.g., the email address of
the account"; a server that puts a human label there produces no
identity match and the composer keeps the primary identity, which is the
previous behaviour. Replies are unaffected - `composeFromAccountEmail`
is only read when `mode === 'compose'`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tags, pins and $answered/$forwarded/$mdnsent set on a message in a
shared/group mailbox opened directly from the "Shared" sidebar section
did not persist: they showed until the next reload and were then gone.
The call sites resolved the target account as
`isUnifiedView ? email.sourceAccountId : undefined`. Outside the unified
view that is always `undefined`, so the write went to the reaching
client's own account, which the server answers with `notUpdated` /
`updated: null` and no error - the same silent no-op that
`email-store-shared-folder-actions.test.ts` already covers for the batch
actions. Emails fetched from a shared folder this way are undecorated
(no `sourceAccountId`); the owner is only recoverable from the selected
mailbox.
`toggleStar` never had the bug because it routes through
`resolveEmailActionContext`, which resolves the owner from the email's
source in an aggregate view and from the selected mailbox otherwise.
That resolver was reimplemented inline at each keyword call site, and
the inline copies only ever covered the aggregate half - so starring a
shared message persisted while tagging the same message did not.
Move the two keyword writes into store actions built on that resolver
and route every call site through them, including the read-receipt and
Pro-compose paths that previously passed no accountId at all.
The store actions resolve only for an email the store actually knows.
A Pro tab fetches its own email and never enters `emails` or
`selectedEmail`; for those the selected mailbox says nothing about the
message's owner, so guessing from it would send an own-account write to
a shared account whenever a shared folder happened to be open. An
unknown email keeps the previous behaviour instead.
Behaviour on own-account and unified-view messages is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Folder management performed on a shared/group account was sent to the
logged-in user's primary JMAP account instead of the mailbox owner's
account. Subfolder creation therefore failed with invalidProperties for
parentId, root folders could be created silently in the personal account,
and rename, delete, role, and reorder targeted the wrong mailbox tree.
Shared mailboxes use namespaced store ids to avoid cross-account
collisions, while Mailbox/set requires the owner's accountId and the bare
server mailbox id. Resolve that mutation context centrally: recover the
mailbox originalId and owner account, use a directly connected owner
client when one exists, otherwise reach delegated/group accounts through
the active client with an explicit accountId.
Thread the account target through create, update, and delete JMAP methods
and all folder-management actions. Scope role replacement to the same
owner account, and expose explicit root-folder creation from shared
account headers without making personal root creation depend on the
currently selected folder.
Add unit coverage for JMAP request scoping, shared store routing, role
isolation, direct-client handling, and reorder. Add a Stalwart integration
scenario that creates root and nested group folders, renames them, and
deletes them through the UI.