From f3e11d4c7c41366c0eab0eff03255501f3cbe8cf Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:54:55 +0100 Subject: [PATCH] fix: stop asking an account that shares only mail for its calendars on every load on Stalwart 1.0 --- src/api/__tests__/calendar.test.ts | 36 ++++++++++++++++++++++++++++++ src/api/calendar.ts | 4 +++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/api/__tests__/calendar.test.ts b/src/api/__tests__/calendar.test.ts index 645a2729..28b9d1d6 100644 --- a/src/api/__tests__/calendar.test.ts +++ b/src/api/__tests__/calendar.test.ts @@ -28,6 +28,8 @@ import { hydrateExpandedOccurrences, maxParticipantsPerEvent, rsvpEvent, + isCalendarAccessDenied, + resetCalendarAccessDenied, } from '../calendar'; import { JMAPSetError, jmapErrorType } from '../jmap-result'; import { useSettingsStore } from '../../stores/settings-store'; @@ -68,6 +70,40 @@ beforeEach(() => { describe('calendar operations', () => { describe('getCalendars', () => { + // Stalwart 1.0 lists an account that shares only a mail folder with the + // calendars capability and answers its Calendar/get with + // accountNotSupportedByMethod (0.16: forbidden). + it.each(['forbidden', 'accountNotSupportedByMethod'])( + 'asks an account that answers %s for its calendars once per session', + async (type) => { + const client = jmapClient as unknown as { currentSession?: unknown }; + client.currentSession = { + accounts: { + 'acc-1': { accountCapabilities: { 'urn:ietf:params:jmap:calendars': {} } }, + 'acc-mail': { accountCapabilities: { 'urn:ietf:params:jmap:calendars': {} } }, + }, + }; + try { + resetCalendarAccessDenied(); + mockRequest.mockImplementation(async (calls: [string, { accountId: string }, string][]) => ({ + methodResponses: calls.map(([name, args, id]) => (args.accountId === 'acc-mail' + ? ['error', { type }, id] + : [name, name === 'Calendar/get' ? { list: [] } : {}, id])), + })); + await getCalendars(); + await getCalendars(); + const asked = mockRequest.mock.calls.filter(([calls]) => + (calls as [string, { accountId: string }][]).some(([, args]) => args.accountId === 'acc-mail')); + expect(asked).toHaveLength(1); + expect(isCalendarAccessDenied('acc-mail')).toBe(true); + } finally { + client.currentSession = undefined; + mockRequest.mockReset(); + resetCalendarAccessDenied(); + } + }, + ); + it('should fetch calendars', async () => { const calendars = [{ id: 'cal-1', name: 'Personal', color: '#3b82f6' }]; mockRequest.mockResolvedValue({ diff --git a/src/api/calendar.ts b/src/api/calendar.ts index f763d3dd..f1b46df8 100644 --- a/src/api/calendar.ts +++ b/src/api/calendar.ts @@ -160,9 +160,11 @@ export function resetCalendarAccessDenied(): void { calendarAccessDenied.clear(); } +// Stalwart 1.0 answers `accountNotSupportedByMethod` for an account that +// shares only a mail folder with the user, where 0.16 says `forbidden`. function isAccessDeniedError(err: unknown): boolean { const type = jmapErrorType(err); - return type === 'forbidden' || type === 'accountNotFound'; + return type === 'forbidden' || type === 'accountNotFound' || type === 'accountNotSupportedByMethod'; } /** Remember an access rejection for a shared account; returns true when it was one. */