From 4aaef1ebf1bf667a71c0b8e3023ae61e44232f1a Mon Sep 17 00:00:00 2001 From: Matthieu MALVACHE Date: Mon, 16 Mar 2026 12:41:10 +0100 Subject: [PATCH] fix: resolve calendar crash on undefined duration and sieve filter save error parseDuration() now handles undefined event.duration gracefully instead of crashing on .match(). CalendarEvent.duration type updated to reflect that JMAP servers may omit this field. Sieve filter activation replaced isActive (server-set per RFC 9661) with onSuccessActivateScript. Create/update calls merged with activation into single JMAP roundtrips. Closes #31, closes #21 --- components/calendar/event-card.tsx | 3 +- lib/jmap/client.ts | 76 +++++++++++++++--------------- lib/jmap/types.ts | 2 +- stores/filter-store.ts | 6 +-- 4 files changed, 44 insertions(+), 43 deletions(-) diff --git a/components/calendar/event-card.tsx b/components/calendar/event-card.tsx index 7948da6..d365dec 100644 --- a/components/calendar/event-card.tsx +++ b/components/calendar/event-card.tsx @@ -28,7 +28,8 @@ function getEventColor(event: CalendarEvent, calendar?: Calendar): string { return sanitizeColor(event.color, sanitizeColor(calendar?.color)); } -function parseDuration(duration: string): number { +function parseDuration(duration: string | undefined): number { + if (!duration) return 0; let totalMinutes = 0; const weekMatch = duration.match(/(\d+)W/); const hourMatch = duration.match(/(\d+)H/); diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index f96e976..ee04300 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -1433,17 +1433,22 @@ export class JMAPClient { throw new Error('Invalid upload response: blobId not found'); } - async createSieveScript(name: string, content: string): Promise { + async createSieveScript(name: string, content: string, activate?: boolean): Promise { const blobId = await this.uploadSieveBlob(content); const accountId = this.getSieveAccountId(); + const setArgs: Record = { + accountId, + create: { + "new-script": { name, blobId } + }, + }; + if (activate) { + setArgs.onSuccessActivateScript = "#new-script"; + } + const response = await this.request([ - ["SieveScript/set", { - accountId, - create: { - "new-script": { name, blobId } - } - }, "0"] + ["SieveScript/set", setArgs, "0"] ], this.sieveUsing()); if (response.methodResponses?.[0]?.[0] === "SieveScript/set") { @@ -1462,17 +1467,22 @@ export class JMAPClient { throw new Error("Failed to create sieve script"); } - async updateSieveScript(scriptId: string, content: string): Promise { + async updateSieveScript(scriptId: string, content: string, activate?: boolean): Promise { const blobId = await this.uploadSieveBlob(content); const accountId = this.getSieveAccountId(); + const setArgs: Record = { + accountId, + update: { + [scriptId]: { blobId } + }, + }; + if (activate) { + setArgs.onSuccessActivateScript = scriptId; + } + const response = await this.request([ - ["SieveScript/set", { - accountId, - update: { - [scriptId]: { blobId } - } - }, "0"] + ["SieveScript/set", setArgs, "0"] ], this.sieveUsing()); if (response.methodResponses?.[0]?.[0] === "SieveScript/set") { @@ -1513,44 +1523,36 @@ export class JMAPClient { const response = await this.request([ ["SieveScript/set", { accountId, - update: { - [scriptId]: { isActive: true } - } + onSuccessActivateScript: scriptId, }, "0"] ], this.sieveUsing()); - if (response.methodResponses?.[0]?.[0] === "SieveScript/set") { - const result = response.methodResponses[0][1]; - if (result.notUpdated?.[scriptId]) { - const error = result.notUpdated[scriptId]; - throw new Error(error.description || "Failed to activate sieve script"); - } - return; + const [methodName, result] = response.methodResponses?.[0] || []; + if (methodName === "error") { + throw new Error(result?.description || "Failed to activate sieve script"); + } + if (methodName !== "SieveScript/set") { + throw new Error("Failed to activate sieve script"); } - throw new Error("Failed to activate sieve script"); } - async deactivateSieveScript(scriptId: string): Promise { + async deactivateSieveScript(): Promise { const accountId = this.getSieveAccountId(); const response = await this.request([ ["SieveScript/set", { accountId, - update: { - [scriptId]: { isActive: false } - } + onSuccessActivateScript: null, }, "0"] ], this.sieveUsing()); - if (response.methodResponses?.[0]?.[0] === "SieveScript/set") { - const result = response.methodResponses[0][1]; - if (result.notUpdated?.[scriptId]) { - const error = result.notUpdated[scriptId]; - throw new Error(error.description || "Failed to deactivate sieve script"); - } - return; + const [methodName, result] = response.methodResponses?.[0] || []; + if (methodName === "error") { + throw new Error(result?.description || "Failed to deactivate sieve script"); + } + if (methodName !== "SieveScript/set") { + throw new Error("Failed to deactivate sieve script"); } - throw new Error("Failed to deactivate sieve script"); } async validateSieveScript(content: string): Promise<{ isValid: boolean; errors?: string[] }> { diff --git a/lib/jmap/types.ts b/lib/jmap/types.ts index 7cf0908..f2194aa 100644 --- a/lib/jmap/types.ts +++ b/lib/jmap/types.ts @@ -312,7 +312,7 @@ export interface CalendarEvent { updated: string; sequence: number; start: string; - duration: string; + duration?: string; timeZone: string | null; showWithoutTime: boolean; status: 'tentative' | 'confirmed' | 'cancelled'; diff --git a/stores/filter-store.ts b/stores/filter-store.ts index a5bc40d..25f2aaa 100644 --- a/stores/filter-store.ts +++ b/stores/filter-store.ts @@ -94,11 +94,9 @@ export const useFilterStore = create()((set, get) => ({ } if (activeScriptId) { - await client.updateSieveScript(activeScriptId, content); - await client.activateSieveScript(activeScriptId); + await client.updateSieveScript(activeScriptId, content, true); } else { - const script = await client.createSieveScript('filters', content); - await client.activateSieveScript(script.id); + const script = await client.createSieveScript('filters', content, true); set({ activeScriptId: script.id }); }