From 15c3acd7791d1c85bf838f30a6ff767dd2ef95b7 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:05:22 +0200 Subject: [PATCH] perf: open large HTML messages with one request instead of two --- lib/__tests__/jmap-truncated-body.test.ts | 22 ++++++++++++++++++++++ lib/jmap/client.ts | 6 +++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/__tests__/jmap-truncated-body.test.ts b/lib/__tests__/jmap-truncated-body.test.ts index 75b89923d..dbfef35b5 100644 --- a/lib/__tests__/jmap-truncated-body.test.ts +++ b/lib/__tests__/jmap-truncated-body.test.ts @@ -172,4 +172,26 @@ describe('JMAPClient truncated body refetch (#884)', () => { expect(emails.find((e) => e.id === 'email-1')?.bodyValues?.html1.value).toBe('full-1'); expect(emails.find((e) => e.id === 'email-2')?.bodyValues?.html1.value).toBe('full-2'); }); + + it('getEmail fetches a body just over the list cap in one request', async () => { + // Opening one message needs all of its body; at the list-sized 256 kB cap + // a 300 kB newsletter came back truncated and was fetched twice. + const client = await createConnectedClient(); + const html = '' + 'x'.repeat(300_000) + ''; + fetchSpy.mockImplementation(async (_input, init) => { + const call = JSON.parse((init as RequestInit).body as string).methodCalls[0][1]; + const cap = call.maxBodyValueBytes as number; + return mockFetchResponse(200, { + methodResponses: [['Email/get', { list: [{ + ...truncatedEmail(), + bodyValues: { html1: { value: html.slice(0, cap), isTruncated: html.length > cap } }, + }] }, '0']], + }); + }); + + const email = await client.getEmail('email-1'); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(email?.bodyValues?.html1.value).toBe(html); + }); }); diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 47cb56c2d..92db5f63c 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -27,6 +27,10 @@ import { toInertBlob } from "@/lib/file-preview"; // / #884). Bounded well above the default so it stays rare in practice while // still guarding against unbounded memory use on a malicious/oversized body. const TRUNCATED_BODY_REFETCH_MAX_BYTES = 8_000_000; +// Opening one message needs its whole body, so its fetch takes more than the +// list-sized cap: a newsletter just over 256 kB came back truncated and was +// fetched a second time. Larger bodies still take the refetch above. +const OPEN_EMAIL_MAX_BODY_BYTES = 1_000_000; // Names of nodes created over WebDAV come back percent-encoded from // FileNode/get (see decodeFileNodeName / #869). Normalize at the boundary so @@ -2369,7 +2373,7 @@ export class JMAPClient implements IJMAPClient { fetchTextBodyValues: true, fetchHTMLBodyValues: true, fetchAllBodyValues: true, - maxBodyValueBytes: 256000, + maxBodyValueBytes: OPEN_EMAIL_MAX_BODY_BYTES, }, "0"], ]);