From 0c1f182b6b65bb656e6c6a5909dd85574aabf021 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:54:37 +0100 Subject: [PATCH] fix: detect tasks created by external CalDAV clients (Thunderbird) Tasks created in Thunderbird via CalDAV were not visible because getCalendarTasks() used a strict @type === 'Task' check. Stalwart may not set @type when converting VTODO from CalDAV to JMAP. - Use case-insensitive @type matching for server variations - Add fallback heuristic: detect tasks by presence of 'progress' property (exclusive to JSCalendar Task, never on Event objects) - Normalize @type to 'Task' on detected tasks for consistent downstream handling - Refresh task store on CalendarEvent state changes so tasks created externally appear without manual page refresh Fixes #84 --- lib/jmap/client.ts | 21 ++++++++++++++++++--- stores/email-store.ts | 6 ++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index dfe363d9..c2395464 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -3178,9 +3178,24 @@ export class JMAPClient implements IJMAPClient { async getCalendarTasks(calendarIds?: string[], targetAccountId?: string): Promise { try { const events = await this.getCalendarEvents(calendarIds, targetAccountId); - return events.filter((e): e is CalendarTask & CalendarEvent => - (e as unknown as CalendarTask)['@type'] === 'Task' - ) as unknown as CalendarTask[]; + return events.filter((e) => { + const obj = e as unknown as Record; + const type = obj['@type']; + // Explicit @type check (case-insensitive to handle server variations) + if (typeof type === 'string' && type.toLowerCase() === 'task') return true; + // Fallback: detect tasks created via CalDAV (e.g. Thunderbird) where @type + // may be missing. The "progress" property is exclusive to JSCalendar Task + // objects and never appears on Event objects. + if (type !== 'Event' && 'progress' in obj && typeof obj.progress === 'string') return true; + return false; + }).map((e) => { + const task = e as unknown as CalendarTask; + // Normalize @type for tasks detected by fallback heuristic + if (task['@type'] !== 'Task') { + (task as unknown as Record)['@type'] = 'Task'; + } + return task; + }); } catch (error) { console.error('Failed to get calendar tasks:', error); return []; diff --git a/stores/email-store.ts b/stores/email-store.ts index 1fa2bd69..675e126b 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -1102,6 +1102,12 @@ export const useEmailStore = create((set, get) => ({ if (dateRange && selectedCalendarIds.length > 0) { calendarStore.fetchEvents(client, dateRange.start, dateRange.end); } + // Refresh tasks when calendar events change (e.g. task created via CalDAV) + const { useTaskStore } = await import('./task-store'); + const taskStore = useTaskStore.getState(); + if (taskStore.tasks.length > 0 || calendarStore.viewMode === 'tasks') { + taskStore.fetchTasks(client); + } } }