Fix: stop re-probing shared accounts without calendar access

The calendar fan-out probes every shared/group account on suspicion,
because Stalwart does not always advertise calendar capability on group
accounts. A shared account that grants no calendar access at all rejects
that probe - and did so again on every calendar interaction: each range
change re-queried the account and logged a red console error
("You do not have access to account X") while working fine otherwise.

Remember the rejection instead: the thrown query error now carries the
JMAP error type, an access rejection for a probed secondary account is
logged once at debug level, and both fan-out loops (events and calendar
lists) skip the account for the rest of the session. Genuine failures on
the primary account keep the error log. Two regression tests cover the
probe-once behavior and the calendar-list skip.
This commit is contained in:
dealerweb
2026-07-23 14:55:34 +02:00
parent e442c55931
commit 010f082c73
2 changed files with 122 additions and 1 deletions
+25 -1
View File
@@ -4547,6 +4547,7 @@ export class JMAPClient implements IJMAPClient {
for (const accountId of accountIds) {
const isPrimary = accountId === primaryId;
if (!isPrimary && this.calendarAccessDenied.has(accountId)) continue;
const account = this.accounts[accountId];
try {
@@ -4753,6 +4754,11 @@ export class JMAPClient implements IJMAPClient {
.map((event) => normalizeCalendarEventLike(event));
}
// Shared accounts the server rejected calendar access for - probed once,
// then skipped for the rest of the session (see getCalendarCapableAccountIds
// for why the fan-out has to probe on suspicion).
private calendarAccessDenied = new Set<string>();
async queryAllCalendarEvents(
filter: CalendarEventFilter,
sort?: Array<{ property: string; isAscending: boolean }>,
@@ -4765,6 +4771,7 @@ export class JMAPClient implements IJMAPClient {
for (const accountId of accountIds) {
const isPrimary = accountId === primaryId;
if (!isPrimary && this.calendarAccessDenied.has(accountId)) continue;
const account = this.accounts[accountId];
try {
@@ -4830,7 +4837,12 @@ export class JMAPClient implements IJMAPClient {
if (queryResponse.methodResponses?.[0]?.[0] === "error") {
const error = queryResponse.methodResponses[0][1];
throw new Error(error?.description || error?.type || "CalendarEvent/query failed");
// Keep the JMAP error type so the catch below can tell an expected
// access rejection apart from a genuine failure.
throw Object.assign(
new Error(error?.description || error?.type || "CalendarEvent/query failed"),
{ jmapErrorType: error?.type },
);
}
const ids: string[] = queryResponse.methodResponses?.[0]?.[1]?.ids || [];
@@ -4874,6 +4886,18 @@ export class JMAPClient implements IJMAPClient {
return filtered;
} catch (error) {
// The fan-out over shared accounts probes on suspicion (see
// getCalendarCapableAccountIds) and may hit accounts that grant no
// calendar access at all. Remember the rejection and go quiet instead
// of re-probing - and re-logging - on every range change.
const type = (error as { jmapErrorType?: string } | null)?.jmapErrorType;
const denied = type === 'forbidden' || type === 'accountNotFound' ||
/not have access/i.test(error instanceof Error ? error.message : '');
if (targetAccountId && denied) {
this.calendarAccessDenied.add(targetAccountId);
debug.log('calendar', `No calendar access to account ${targetAccountId} - skipping it from now on`);
return [];
}
console.error('Failed to query calendar events:', error);
return [];
}