fix: enhance calendar event handling to distinguish between events and tasks
This commit is contained in:
+39
-2
@@ -95,6 +95,24 @@ const EMAIL_LIST_PROPERTIES = [
|
|||||||
"hasAttachment",
|
"hasAttachment",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect whether a calendar object returned by the server is actually a
|
||||||
|
* task (VTODO) rather than an event (VEVENT). CalDAV clients like
|
||||||
|
* Thunderbird create VTODOs that Stalwart exposes through the
|
||||||
|
* CalendarEvent endpoints without a reliable `@type` discriminator.
|
||||||
|
*/
|
||||||
|
function isTaskObject(obj: { '@type'?: string; progress?: unknown; due?: unknown; percentComplete?: unknown }): boolean {
|
||||||
|
const type = obj['@type'];
|
||||||
|
if (typeof type === 'string' && type.toLowerCase() === 'task') return true;
|
||||||
|
// CalDAV-created tasks may lack @type='Task' — detect by task-specific fields
|
||||||
|
if (type !== 'Event' && (
|
||||||
|
('progress' in obj && typeof obj.progress === 'string') ||
|
||||||
|
('due' in obj && obj.due != null) ||
|
||||||
|
('percentComplete' in obj)
|
||||||
|
)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const CALENDAR_EVENT_PROPERTIES = [
|
const CALENDAR_EVENT_PROPERTIES = [
|
||||||
'id',
|
'id',
|
||||||
'@type',
|
'@type',
|
||||||
@@ -3190,6 +3208,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
if (response.methodResponses?.[1]?.[0] === "CalendarEvent/get") {
|
if (response.methodResponses?.[1]?.[0] === "CalendarEvent/get") {
|
||||||
return ((response.methodResponses[1][1].list || []) as CalendarEvent[])
|
return ((response.methodResponses[1][1].list || []) as CalendarEvent[])
|
||||||
|
.filter((event) => !isTaskObject(event))
|
||||||
.map((event) => normalizeCalendarEventLike(event));
|
.map((event) => normalizeCalendarEventLike(event));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -3268,6 +3287,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
if (response.methodResponses?.[1]?.[0] === "CalendarEvent/get") {
|
if (response.methodResponses?.[1]?.[0] === "CalendarEvent/get") {
|
||||||
return ((response.methodResponses[1][1].list || []) as CalendarEvent[])
|
return ((response.methodResponses[1][1].list || []) as CalendarEvent[])
|
||||||
|
.filter((event) => !isTaskObject(event))
|
||||||
.map((event) => normalizeCalendarEventLike(event));
|
.map((event) => normalizeCalendarEventLike(event));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -3638,6 +3658,18 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
const queryIds = queryResponse?.[1]?.ids || [];
|
const queryIds = queryResponse?.[1]?.ids || [];
|
||||||
debug.log('CalendarTask/fetch query returned', queryIds.length, 'ids:', queryIds);
|
debug.log('CalendarTask/fetch query returned', queryIds.length, 'ids:', queryIds);
|
||||||
debug.log('CalendarTask/fetch get returned', list.length, 'objects');
|
debug.log('CalendarTask/fetch get returned', list.length, 'objects');
|
||||||
|
|
||||||
|
// If the types filter returned 0 results, the server may have silently
|
||||||
|
// ignored it (e.g. Stalwart with CalDAV-created VTODOs). Fall back to
|
||||||
|
// a full scan so we can detect tasks by their properties.
|
||||||
|
if (queryIds.length === 0) {
|
||||||
|
debug.warn('CalendarTask/fetch types filter returned 0 results, falling back to full scan');
|
||||||
|
const tasks = await this.getCalendarTasksFallback(calendarIds, targetAccountId);
|
||||||
|
debug.log('CalendarTask/fetch fallback returned', tasks.length, 'tasks');
|
||||||
|
debug.groupEnd();
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
list.forEach((task, i) => {
|
list.forEach((task, i) => {
|
||||||
debug.log(`CalendarTask/fetch [${i}]`, {
|
debug.log(`CalendarTask/fetch [${i}]`, {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
@@ -3703,8 +3735,13 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
allObjects.forEach((obj) => {
|
allObjects.forEach((obj) => {
|
||||||
const type = obj['@type'];
|
const type = obj['@type'];
|
||||||
const isExplicitTask = typeof type === 'string' && type.toLowerCase() === 'task';
|
const isExplicitTask = typeof type === 'string' && type.toLowerCase() === 'task';
|
||||||
// CalDAV-created tasks (e.g. Thunderbird) may have progress but no @type
|
// CalDAV-created tasks (e.g. Thunderbird) may lack @type or have @type
|
||||||
const isCalDavTask = type !== 'Event' && 'progress' in obj && typeof obj.progress === 'string';
|
// set to something other than 'Event'. Detect them by the presence of
|
||||||
|
// task-specific fields: progress, due, or percentComplete.
|
||||||
|
const hasTaskFields = ('progress' in obj && typeof obj.progress === 'string')
|
||||||
|
|| ('due' in obj && obj.due != null)
|
||||||
|
|| ('percentComplete' in obj);
|
||||||
|
const isCalDavTask = type !== 'Event' && hasTaskFields;
|
||||||
|
|
||||||
debug.log('CalendarTask/fallback scan', {
|
debug.log('CalendarTask/fallback scan', {
|
||||||
id: obj.id,
|
id: obj.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user