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; }