handle localization and property parameters in CalDAV iCal parsing (#16519)

Co-authored-by: guillim <guigloo@msn.com>
This commit is contained in:
neo773
2025-12-17 16:17:16 +05:30
committed by GitHub
parent 2717afce59
commit 6de424bc32
3 changed files with 160 additions and 4 deletions
@@ -13,6 +13,7 @@ import {
syncCollection,
} from 'tsdav';
import { icalDataExtractPropertyValue } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/utils/icalDataExtractPropertyValue';
import { CalDavGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-get-events.service';
import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
import {
@@ -269,18 +270,26 @@ export class CalDAVClient {
const event = events[0] as ical.VEvent;
const participants = this.extractParticipantsFromEvent(event);
const title = icalDataExtractPropertyValue(
event.summary,
'Untitled Event',
);
const description = icalDataExtractPropertyValue(event.description);
const location = icalDataExtractPropertyValue(event.location);
const conferenceLinkUrl = icalDataExtractPropertyValue(event.url);
return {
id: objectUrl,
title: event.summary || 'Untitled Event',
title,
iCalUid: event.uid || '',
description: event.description || '',
description,
startsAt: event.start.toISOString(),
endsAt: event.end.toISOString(),
location: event.location || '',
location,
isFullDay: this.isFullDayEvent(rawData),
isCanceled: event.status === 'CANCELLED',
conferenceLinkLabel: '',
conferenceLinkUrl: event.url,
conferenceLinkUrl,
externalCreatedAt:
event.created?.toISOString() || new Date().toISOString(),
externalUpdatedAt:
@@ -0,0 +1,97 @@
import { icalDataExtractPropertyValue } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/utils/icalDataExtractPropertyValue';
describe('icalDataExtractPropertyValue', () => {
describe('properties with parameters (RFC 5545 Section 3.2)', () => {
it('should extract value from property object with val and params', () => {
const property = {
val: 'Meeting Title',
params: { LANGUAGE: 'en-US' },
};
const result = icalDataExtractPropertyValue(property);
expect(result).toBe('Meeting Title');
});
it('should handle property with val but no params', () => {
const property = {
val: 'Conference Room A',
};
const result = icalDataExtractPropertyValue(property);
expect(result).toBe('Conference Room A');
});
it('should convert non-string val to string', () => {
const property = {
val: 12345,
params: { TYPE: 'INTEGER' },
} as any;
const result = icalDataExtractPropertyValue(property);
expect(result).toBe('12345');
});
it('should handle property with empty string val', () => {
const property = {
val: '',
params: { LANGUAGE: 'de-DE' },
};
const result = icalDataExtractPropertyValue(property, 'default');
expect(result).toBe('');
});
});
describe('multiple values in a single property (RFC 5545 Section 3.1.2)', () => {
it('should join multiple string values with comma and space', () => {
const property = ['Value 1', 'Value 2', 'Value 3'] as any;
const result = icalDataExtractPropertyValue(property);
expect(result).toBe('Value 1, Value 2, Value 3');
});
it('should handle array of property objects with val', () => {
const property = [
{ val: 'First Value', params: { LANGUAGE: 'en' } },
{ val: 'Second Value', params: { LANGUAGE: 'fr' } },
] as any;
const result = icalDataExtractPropertyValue(property);
expect(result).toBe('First Value, Second Value');
});
it('should filter out empty values from array', () => {
const property = ['Value 1', '', 'Value 3', { val: '' }] as any;
const result = icalDataExtractPropertyValue(property);
expect(result).toBe('Value 1, Value 3');
});
it('should return default value when array contains only empty values', () => {
const property = ['', { val: '' }, null] as any;
const result = icalDataExtractPropertyValue(property, 'No values');
expect(result).toBe('No values');
});
it('should handle mixed array of strings and objects', () => {
const property = [
'Plain String',
{ val: 'Object Value', params: {} },
'Another String',
] as any;
const result = icalDataExtractPropertyValue(property);
expect(result).toBe('Plain String, Object Value, Another String');
});
});
});
@@ -0,0 +1,50 @@
import { isNonEmptyString, isString } from '@sniptt/guards';
import { isDefined } from 'class-validator';
/**
* Extracts the string value from an iCal property that may have parameters.
* Per RFC 5545, properties can have parameters like LANGUAGE=de-DE, which causes
* node-ical to return an object with `val` and `params` instead of a plain string.
*
* RFC 5545 Section 3.1.2 also allows multiple values in a single property.
*
* @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2 (Property Parameters)
* @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.1.2 (Multiple Values)
*/
export const icalDataExtractPropertyValue = (
property:
| string
| { val?: string; params?: Record<string, unknown> }
| undefined,
defaultValue = '',
): string => {
if (!isDefined(property)) {
return defaultValue;
}
if (isNonEmptyString(property)) {
return property;
}
if (isDefined(property) && typeof property === 'object') {
if ('val' in property && isDefined(property.val)) {
return isString(property.val) ? property.val : String(property.val);
}
if (Array.isArray(property)) {
const values = property
.map((item) => {
if (isNonEmptyString(item)) return item;
if (isDefined(item) && typeof item === 'object' && item?.val)
return String(item.val);
return '';
})
.filter(Boolean);
return values.length > 0 ? values.join(', ') : defaultValue;
}
}
return defaultValue;
};