From 6fe9f8f7f15f63de11962c9b0ea271ff20344903 Mon Sep 17 00:00:00 2001 From: Ali Yousuf Date: Sat, 17 Oct 2026 18:41:31 -0400 Subject: [PATCH] fix: let api.email.get read a listed message of a shared account doEmailGet asked the signed-in account for every id, so a message in a folder shared from another account came back as null even though it was in the list. A message in the loaded list is now read from the account that owns it, through resolveEmailBlobContext like attachment downloads. getAttachment and getRaw keep reading from the signed-in account only. Fixes #1273 --- lib/__tests__/plugin-host-mail-read.test.ts | 10 +++++++++- lib/plugin-sandbox/host-api-mail.ts | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/__tests__/plugin-host-mail-read.test.ts b/lib/__tests__/plugin-host-mail-read.test.ts index 9db3e13b1..a7ed397c4 100644 --- a/lib/__tests__/plugin-host-mail-read.test.ts +++ b/lib/__tests__/plugin-host-mail-read.test.ts @@ -54,7 +54,7 @@ beforeEach(() => { afterEach(() => { useAuthStore.setState({ client: null }); - useEmailStore.setState({ mailboxes: [], selectedEmail: null }); + useEmailStore.setState({ mailboxes: [], selectedEmail: null, selectedMailbox: '', emails: [] }); }); describe('api.email.get', () => { @@ -67,6 +67,14 @@ describe('api.email.get', () => { expect(await call(READER, 'email.get', 'gone')).toBeNull(); }); + it('asks the owning account for a listed message of a shared folder', async () => { + client.getEmail.mockImplementation(async (id: string, accountId?: string) => ( + id === 's1' && accountId === 'acc2' ? mail('s1', { subject: 'Shared' }) : null)); + useEmailStore.setState({ selectedMailbox: 'acc2:box', emails: [mail('s1', { mailboxIds: { 'acc2:box': true } })] }); + expect(await call(READER, 'email.get', 's1')).toEqual(expect.objectContaining({ id: 's1', subject: 'Shared' })); + expect(client.getEmail).toHaveBeenCalledWith('s1', 'acc2'); + }); + it('uses the open message when its body is loaded', async () => { useEmailStore.setState({ selectedEmail: mail('open', { bodyValues: { 1: { value: 'x' } }, textBody: [{ partId: '1' }] } as unknown as Partial) }); expect(await call(READER, 'email.get', 'open')).toEqual(expect.objectContaining({ id: 'open', text: 'x' })); diff --git a/lib/plugin-sandbox/host-api-mail.ts b/lib/plugin-sandbox/host-api-mail.ts index 4c2fb4ee8..33d4d7389 100644 --- a/lib/plugin-sandbox/host-api-mail.ts +++ b/lib/plugin-sandbox/host-api-mail.ts @@ -3,13 +3,14 @@ // (api.email.getAttachment / getRaw). Reads go to the server through the // JMAP client and never change what the user has open. // -// Messages are read from the signed-in account itself; mail in folders shared -// from other accounts is not reached by id here. +// api.email.get also reads a message of a shared account when it is in the +// loaded list, from the account that owns it. The bytes of attachments and +// sources are read from the signed-in account only. import type { EmailReadView, EmailSummary, MailboxView } from '../plugin-types'; import type { Email, Mailbox } from '../jmap/types'; import type { IJMAPClient } from '../jmap/client-interface'; -import { useEmailStore } from '@/stores/email-store'; +import { resolveEmailBlobContext, useEmailStore } from '@/stores/email-store'; import { emailSummary, emailToReadView } from '../plugin-projection'; import { MAX_PLUGIN_FETCH_BYTES, readId, readInteger, readObject, sessionClient, withinRate, @@ -30,7 +31,14 @@ async function loadEmail(client: IJMAPClient, emailId: string): Promise { const method = 'email.get'; const emailId = readId(id, method, 'emailId'); - const email = await loadEmail(sessionClient(method), emailId); + const client = sessionClient(method); + // A listed message of a shared account is asked of the account that owns + // it; the signed-in account doesn't know its id. + const listed = useEmailStore.getState().emails.find((e) => e.id === emailId); + const owner = listed ? resolveEmailBlobContext(listed, client) : null; + const email = owner?.client && owner.accountId + ? await owner.client.getEmail(emailId, owner.accountId) + : await loadEmail(client, emailId); return email ? emailToReadView(email) : null; }