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
This commit is contained in:
Ali Yousuf
2026-10-17 18:41:31 -04:00
parent 107d731af7
commit 6fe9f8f7f1
2 changed files with 21 additions and 5 deletions
+9 -1
View File
@@ -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<Email>) });
expect(await call(READER, 'email.get', 'open')).toEqual(expect.objectContaining({ id: 'open', text: 'x' }));
+12 -4
View File
@@ -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<Email |
export async function doEmailGet(id: unknown): Promise<EmailReadView | null> {
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;
}