From 7c3c3b5f7bfc5ff14f26f901a39e530e78f5bda1 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:41:47 +0100 Subject: [PATCH] fix: handle synthetic ID errors when updating calendar events with fallback to destroy and recreate --- stores/calendar-store.ts | 65 +++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/stores/calendar-store.ts b/stores/calendar-store.ts index e43c1594..6f528e73 100644 --- a/stores/calendar-store.ts +++ b/stores/calendar-store.ts @@ -169,7 +169,30 @@ export const useCalendarStore = create()( } cleanUpdates.calendarIds = remapped; } - await client.updateCalendarEvent(realId, cleanUpdates, sendSchedulingMessages, targetAccountId); + try { + await client.updateCalendarEvent(realId, cleanUpdates, sendSchedulingMessages, targetAccountId); + } catch (updateError) { + // Stalwart doesn't support updating events created via CalDAV (e.g. by Thunderbird). + // These events have "synthetic" JMAP IDs. Work around by destroying and recreating. + const message = updateError instanceof Error ? updateError.message : ''; + if (message.toLowerCase().includes('synthetic') && storeEvent) { + debug.log('Event has synthetic ID, falling back to destroy+recreate'); + // Merge existing event with updates, strip server-computed fields + const { id: _id, originalId: _oi, originalCalendarIds: _oc, + accountId: _ai, accountName: _an, isShared: _is, + utcStart: _us, utcEnd: _ue, isDraft: _dr, isOrigin: _io, + created: _cr, updated: _up, ...baseEvent } = storeEvent; + const mergedEvent: Partial = { ...baseEvent, ...cleanUpdates }; + // Delete the old event without sending cancellations (we're recreating it) + await client.deleteCalendarEvent(realId, false, targetAccountId); + const created = await client.createCalendarEvent(mergedEvent, sendSchedulingMessages, targetAccountId); + set((state) => ({ + events: state.events.map(e => e.id === id ? created : e), + })); + return; + } + throw updateError; + } set((state) => ({ events: state.events.map(e => e.id === id ? { ...e, ...updates } : e), })); @@ -202,12 +225,40 @@ export const useCalendarStore = create()( if (replyTo) { patch.replyTo = replyTo; } - await client.updateCalendarEvent( - realId, - patch as unknown as Partial, - true, - targetAccountId - ); + try { + await client.updateCalendarEvent( + realId, + patch as unknown as Partial, + true, + targetAccountId + ); + } catch (updateError) { + // Fallback for CalDAV-created events with synthetic IDs + const message = updateError instanceof Error ? updateError.message : ''; + if (message.toLowerCase().includes('synthetic') && storeEvent) { + debug.log('RSVP: Event has synthetic ID, falling back to destroy+recreate'); + const { id: _id, originalId: _oi, originalCalendarIds: _oc, + accountId: _ai, accountName: _an, isShared: _is, + utcStart: _us, utcEnd: _ue, isDraft: _dr, isOrigin: _io, + created: _cr, updated: _up, ...baseEvent } = storeEvent; + const updatedParticipants = storeEvent.participants ? { + ...storeEvent.participants, + [participantId]: { ...storeEvent.participants[participantId], participationStatus: status }, + } : storeEvent.participants; + const mergedEvent: Partial = { + ...baseEvent, + participants: updatedParticipants as Record | null, + ...(replyTo ? { replyTo } : {}), + }; + await client.deleteCalendarEvent(realId, false, targetAccountId); + const created = await client.createCalendarEvent(mergedEvent, true, targetAccountId); + set((state) => ({ + events: state.events.map(e => e.id === eventId ? created : e), + })); + return; + } + throw updateError; + } set((state) => ({ events: state.events.map(e => { if (e.id !== eventId || !e.participants?.[participantId]) return e;