2 * This file needs to be improved in terms of typing. They were rushed due to time constraints.
4 import ICAL from 'ical.js';
6 import { parseWithRecovery } from '@proton/shared/lib/calendar/icsSurgery/ics';
8 import { DAY, HOUR, MINUTE, SECOND, WEEK } from '../constants';
10 VcalCalendarComponent,
11 VcalCalendarComponentWithMaybeErrors,
12 VcalDateOrDateTimeValue,
17 VcalRrulePropertyValue,
20 VcalVcalendarWithMaybeErrors,
22 VcalVeventComponentWithMaybeErrors,
23 } from '../interfaces/calendar';
24 import { UNIQUE_PROPERTIES } from './vcalDefinition';
25 import { getIsVcalErrorComponent } from './vcalHelper';
27 const getIcalDateValue = (value: any, tzid: string | undefined, isDate: boolean) => {
28 const icalTimezone = value.isUTC ? ICAL.Timezone.utcTimezone : ICAL.Timezone.localTimezone;
33 hour: value.hours || 0,
34 minute: value.minutes || 0,
35 second: value.seconds || 0,
38 return ICAL.Time.fromData(icalData, icalTimezone);
41 const getIcalPeriodValue = (value: any, tzid: string | undefined) => {
42 return ICAL.Period.fromData({
43 // periods must be of date-time
44 start: value.start ? getIcalDateValue(value.start, tzid, false) : undefined,
45 end: value.end ? getIcalDateValue(value.end, tzid, false) : undefined,
46 duration: value.duration ? ICAL.Duration.fromData(value.duration) : undefined,
50 const getIcalDurationValue = (value?: any) => {
51 return ICAL.Duration.fromData(value);
54 const getIcalUntilValue = (value?: any) => {
58 return getIcalDateValue(value, '', typeof value.hours === 'undefined');
61 export const internalValueToIcalValue = (type: string, value: any, { tzid }: { tzid?: string } = {}) => {
62 if (Array.isArray(value)) {
65 if (typeof value === 'string') {
68 if (type === 'date' || type === 'date-time') {
69 return getIcalDateValue(value, tzid, type === 'date');
71 if (type === 'duration') {
72 return getIcalDurationValue(value);
74 if (type === 'period') {
75 return getIcalPeriodValue(value, tzid);
77 if (type === 'recur') {
79 return ICAL.Recur.fromData(value);
81 const until = getIcalUntilValue(value.until);
82 return ICAL.Recur.fromData({ ...value, until });
84 return value.toString();
87 const getInternalDateValue = (value: any): VcalDateValue => {
95 export const getInternalDateTimeValue = (value: any): VcalDateTimeValue => {
97 ...getInternalDateValue(value),
99 minutes: value.minute,
100 seconds: value.second,
101 isUTC: value.zone.tzid === 'UTC',
105 const getInternalDurationValue = (value: any): VcalDurationValue => {
110 minutes: value.minutes,
111 seconds: value.seconds,
112 isNegative: value.isNegative,
116 const getInternalUntil = (value?: any): VcalDateOrDateTimeValue | undefined => {
120 return value.icaltype === 'date' ? getInternalDateValue(value) : getInternalDateTimeValue(value);
123 const getInternalRecur = (value?: any): VcalRrulePropertyValue | undefined => {
130 // COUNT = 0 gets ignored in the above step
131 if (value.count === 0) {
134 const until = getInternalUntil(value.until);
136 result.until = until;
142 * Convert from ical.js format to an internal format
144 export const icalValueToInternalValue = (type: string, value: any) => {
145 if (Array.isArray(value)) {
148 if (typeof value === 'string' || type === 'integer') {
151 if (type === 'date') {
152 return getInternalDateValue(value);
154 if (type === 'date-time') {
155 return getInternalDateTimeValue(value);
157 if (type === 'duration') {
158 return getInternalDurationValue(value);
160 if (type === 'period') {
161 const result: any = {};
163 result.start = getInternalDateTimeValue(value.start);
166 result.end = getInternalDateTimeValue(value.end);
168 if (value.duration) {
169 result.duration = getInternalDurationValue(value.duration);
173 if (type === 'recur') {
174 return getInternalRecur(value);
176 return value.toString();
180 * Get an ical property.
182 const getProperty = (name: string, { value, parameters }: any) => {
183 const property = new ICAL.Property(name);
185 const { type: specificType, ...restParameters } = parameters || {};
188 property.resetType(specificType);
191 const type = specificType || property.type;
193 if (property.isMultiValue && Array.isArray(value)) {
194 property.setValues(value.map((val) => internalValueToIcalValue(type, val, restParameters)));
196 property.setValue(internalValueToIcalValue(type, value, restParameters));
199 Object.keys(restParameters).forEach((key) => {
200 property.setParameter(key, restParameters[key]);
206 const addInternalProperties = (component: any, properties: any) => {
207 Object.keys(properties).forEach((name) => {
208 const jsonProperty = properties[name];
210 if (Array.isArray(jsonProperty)) {
211 jsonProperty.forEach((property) => {
212 component.addProperty(getProperty(name, property));
217 component.addProperty(getProperty(name, jsonProperty));
222 const fromInternalComponent = (properties: any) => {
223 const { component: name, components, ...restProperties } = properties;
225 const component = addInternalProperties(new ICAL.Component(name), restProperties);
227 if (Array.isArray(components)) {
228 components.forEach((otherComponent) => {
229 component.addSubcomponent(fromInternalComponent(otherComponent));
236 export const serialize = (component: any) => {
237 return fromInternalComponent(component).toString();
240 const getParameters = (type: string, property: any) => {
241 const allParameters = property.toJSON() || [];
242 const parameters = allParameters[1];
243 const isDefaultType = type === property.getDefaultType();
249 if (!isDefaultType) {
256 const checkIfDateOrDateTimeValid = (dateOrDateTimeString: string, isDateType = false) => {
257 if (/--/.test(dateOrDateTimeString)) {
258 // just to be consistent with error messages from ical.js
259 const message = isDateType ? 'could not extract integer from' : 'invalid date-time value';
260 throw new Error(message);
264 const fromIcalProperties = (properties = []) => {
265 if (properties.length === 0) {
268 return properties.reduce<{ [key: string]: any }>((acc, property: any) => {
269 const { name } = property;
274 const { type } = property;
275 if (['date-time', 'date'].includes(type)) {
276 checkIfDateOrDateTimeValid(property.toJSON()[3], type === 'date');
278 const values = property.getValues().map((value: any) => icalValueToInternalValue(type, value));
280 const parameters = getParameters(type, property);
281 const propertyAsObject = {
282 value: property.isMultiValue ? values : values[0],
283 ...(Object.keys(parameters).length && { parameters }),
286 if (UNIQUE_PROPERTIES.has(name)) {
287 acc[name] = propertyAsObject;
295 // Exdate can be both an array and multivalue, force it to only be an array
296 if (name === 'exdate') {
297 const normalizedValues = values.map((value: any) => ({ ...propertyAsObject, value }));
299 acc[name] = acc[name].concat(normalizedValues);
301 acc[name].push(propertyAsObject);
308 export const fromIcalComponent = (component: any) => {
309 const components = component.getAllSubcomponents().map(fromIcalComponent);
311 component: component.name,
312 ...(components.length && { components }),
313 ...fromIcalProperties(component ? component.getAllProperties() : undefined),
314 } as VcalCalendarComponent;
317 export const fromIcalComponentWithMaybeErrors = (
319 ): VcalCalendarComponentWithMaybeErrors | VcalErrorComponent => {
320 const components = component.getAllSubcomponents().map((subcomponent: any) => {
322 return fromIcalComponentWithMaybeErrors(subcomponent);
323 } catch (error: any) {
324 return { error, icalComponent: subcomponent };
328 component: component.name,
329 ...(components.length && { components }),
330 ...fromIcalProperties(component ? component.getAllProperties() : undefined),
331 } as VcalCalendarComponentWithMaybeErrors;
335 * Parse vCalendar String and return a component
337 export const parse = (vcal = ''): VcalCalendarComponent => {
339 return {} as VcalCalendarComponent;
341 return fromIcalComponent(new ICAL.Component(ICAL.parse(vcal))) as VcalCalendarComponent;
345 * Same as the parseWithRecovery function, but catching errors in individual components.
346 * This is useful in case we can parse some events but not all in a given ics
348 export const parseVcalendarWithRecoveryAndMaybeErrors = (
351 retryLineBreaks?: boolean;
352 retryEnclosing?: boolean;
353 retryDateTimes?: boolean;
354 retryOrganizer?: boolean;
356 ): VcalVcalendarWithMaybeErrors | VcalCalendarComponentWithMaybeErrors => {
358 const result = parseWithRecovery(vcal, retry) as VcalVcalendar;
359 // add mandatory but maybe missing properties
360 if (!result.prodid) {
361 result.prodid = { value: '' };
363 if (!result.version) {
364 result.version = { value: '2.0' };
368 return fromIcalComponentWithMaybeErrors(new ICAL.Component(ICAL.parse(vcal))) as
369 | VcalVcalendarWithMaybeErrors
370 | VcalCalendarComponentWithMaybeErrors;
374 export const getVeventWithoutErrors = (
375 veventWithMaybeErrors: VcalVeventComponentWithMaybeErrors
376 ): VcalVeventComponent => {
377 const filteredComponents: VcalValarmComponent[] | undefined = veventWithMaybeErrors.components?.filter(
378 (component): component is VcalValarmComponent => !getIsVcalErrorComponent(component)
382 ...veventWithMaybeErrors,
383 components: filteredComponents,
387 export const fromRruleString = (rrule = '') => {
388 return getInternalRecur(ICAL.Recur.fromString(rrule));
392 * Parse a trigger string (e.g. '-PT15M') and return an object indicating its duration
394 export const fromTriggerString = (trigger = '') => {
395 return getInternalDurationValue(ICAL.Duration.fromString(trigger));
398 export const toTriggerString = (value: VcalDurationValue): string => {
399 return getIcalDurationValue(value).toString();
403 * Transform a duration object into milliseconds
405 export const durationToMilliseconds = ({
414 const lapse = weeks * WEEK + days * DAY + hours * HOUR + minutes * MINUTE + seconds * SECOND + milliseconds;
415 return isNegative ? -lapse : lapse;
419 * Parse a trigger string (e.g. '-PT15M') and return its duration in milliseconds
421 export const getMillisecondsFromTriggerString = (trigger = '') => {
422 return durationToMilliseconds(fromTriggerString(trigger));