Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / icsSurgery / vevent.ts
blob348a2451825a21ce39c047d420d993bddf650e31
1 import { addDays, fromUnixTime } from 'date-fns';
3 import type { EventComponentIdentifiers } from '@proton/shared/lib/calendar/icsSurgery/interface';
4 import { getClosestProtonColor } from '@proton/shared/lib/colors';
5 import { validateEmailAddress } from '@proton/shared/lib/helpers/email';
6 import truncate from '@proton/utils/truncate';
7 import unique from '@proton/utils/unique';
9 import type { RequireOnly, RequireSome } from '../../../lib/interfaces';
10 import { DAY } from '../../constants';
11 import { convertUTCDateTimeToZone, fromUTCDate, getSupportedTimezone } from '../../date/timezone';
12 import type {
13     IcalJSDateOrDateTimeProperty,
14     VcalAttendeeProperty,
15     VcalDateOrDateTimeProperty,
16     VcalDateTimeValue,
17     VcalDurationValue,
18     VcalFloatingDateTimeProperty,
19     VcalVeventComponent,
20 } from '../../interfaces/calendar';
21 import { dedupeAlarmsWithNormalizedTriggers } from '../alarms';
22 import { getAttendeeEmail, getSupportedAttendee, getSupportedOrganizer } from '../attendees';
23 import { ICAL_METHOD, MAX_CHARS_API, MAX_ICAL_SEQUENCE } from '../constants';
24 import { getIsDateOutOfBounds, getIsWellFormedDateOrDateTime, getSupportedUID } from '../helper';
25 import { getHasConsistentRrule, getHasOccurrences, getSupportedRrule } from '../recurrence/rrule';
26 import { durationToMilliseconds } from '../vcal';
27 import {
28     dateTimeToProperty,
29     dateToProperty,
30     getDateTimeProperty,
31     getDateTimePropertyInDifferentTimezone,
32     propertyToUTCDate,
33 } from '../vcalConverter';
34 import { getIsPropertyAllDay, getPropertyTzid } from '../vcalHelper';
35 import { EVENT_INVITATION_ERROR_TYPE, EventInvitationError, INVITATION_ERROR_TYPE } from './EventInvitationError';
36 import { IMPORT_EVENT_ERROR_TYPE, ImportEventError } from './ImportEventError';
37 import { getSupportedAlarms } from './valarm';
38 import { getSupportedStringValue } from './vcal';
40 export const getDtendPropertyFromDuration = (
41     dtstart: VcalDateOrDateTimeProperty,
42     duration: VcalDurationValue | number
43 ) => {
44     const startDateUTC = propertyToUTCDate(dtstart);
45     const durationInMs = typeof duration === 'number' ? duration : durationToMilliseconds(duration);
46     const timestamp = +startDateUTC + durationInMs;
47     const end = fromUTCDate(fromUnixTime(timestamp / 1000));
49     if (getIsPropertyAllDay(dtstart)) {
50         // The all-day event lasts one day, we don't need DTEND in this case
51         if (durationInMs <= DAY) {
52             return;
53         }
55         const shouldAddDay = !!(durationInMs <= 0 || end.hours || end.minutes || end.seconds);
56         const finalEnd = shouldAddDay
57             ? fromUTCDate(addDays(propertyToUTCDate({ value: { ...end, isUTC: true } }), 1))
58             : { ...end };
60         return dateToProperty(finalEnd);
61     }
63     if (durationInMs <= 0) {
64         // The part-day event has zero duration, we don't need DTEND in this case
65         return;
66     }
68     const tzid = getPropertyTzid(dtstart);
70     return getDateTimeProperty(convertUTCDateTimeToZone(end, tzid!), tzid!);
73 export const getSupportedDtstamp = (dtstamp: IcalJSDateOrDateTimeProperty | undefined, timestamp: number) => {
74     // as per RFC, the DTSTAMP value MUST be specified in the UTC time format. But that's not what we always receive from external providers
75     const value = dtstamp?.value;
76     const tzid = dtstamp?.parameters?.tzid;
78     if (!value) {
79         return dateTimeToProperty(fromUTCDate(new Date(timestamp)), true);
80     }
82     if (tzid) {
83         const supportedTzid = getSupportedTimezone(tzid);
84         if (!supportedTzid) {
85             // generate a new DTSTAMP
86             return dateTimeToProperty(fromUTCDate(new Date(timestamp)), true);
87         }
88         // we try to guess what the external provider meant
89         const guessedProperty = {
90             value: {
91                 year: value.year,
92                 month: value.month,
93                 day: value.day,
94                 hours: (value as VcalDateTimeValue)?.hours || 0,
95                 minutes: (value as VcalDateTimeValue)?.minutes || 0,
96                 seconds: (value as VcalDateTimeValue)?.seconds || 0,
97                 isUTC: (value as VcalDateTimeValue)?.isUTC === true,
98             },
99             parameters: {
100                 tzid: supportedTzid,
101             },
102         };
104         return dateTimeToProperty(fromUTCDate(propertyToUTCDate(guessedProperty)), true);
105     }
107     return dateTimeToProperty(fromUTCDate(propertyToUTCDate(dtstamp as VcalDateOrDateTimeProperty)), true);
110 export const withSupportedDtstamp = <T>(
111     properties: RequireOnly<VcalVeventComponent, 'uid' | 'component' | 'dtstart'> & T,
112     timestamp: number
113 ): VcalVeventComponent & T => {
114     return {
115         ...properties,
116         dtstamp: getSupportedDtstamp(properties.dtstamp, timestamp),
117     };
120 export const getSupportedDateOrDateTimeProperty = ({
121     property,
122     componentIdentifiers,
123     hasXWrTimezone,
124     calendarTzid,
125     isRecurring = false,
126     isInvite,
127     guessTzid,
128 }: {
129     property: VcalDateOrDateTimeProperty | VcalFloatingDateTimeProperty;
130     componentIdentifiers: EventComponentIdentifiers;
131     hasXWrTimezone: boolean;
132     calendarTzid?: string;
133     isRecurring?: boolean;
134     method?: ICAL_METHOD;
135     isInvite?: boolean;
136     guessTzid?: string;
137 }) => {
138     if (getIsPropertyAllDay(property)) {
139         return dateToProperty(property.value);
140     }
142     const partDayProperty = property;
144     // account for non-RFC-compliant Google Calendar exports
145     // namely localize Zulu date-times for non-recurring events with x-wr-timezone if present and accepted by us
146     if (partDayProperty.value.isUTC && !isRecurring && hasXWrTimezone && calendarTzid) {
147         const localizedDateTime = convertUTCDateTimeToZone(partDayProperty.value, calendarTzid);
148         return getDateTimeProperty(localizedDateTime, calendarTzid);
149     }
150     const partDayPropertyTzid = getPropertyTzid(partDayProperty);
152     // A floating date-time property
153     if (!partDayPropertyTzid) {
154         if (!hasXWrTimezone) {
155             if (guessTzid) {
156                 return getDateTimeProperty(partDayProperty.value, guessTzid);
157             }
158             if (isInvite) {
159                 throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
160                     componentIdentifiers,
161                     extendedType: EVENT_INVITATION_ERROR_TYPE.UNEXPECTED_FLOATING_TIME,
162                 });
163             }
164             // we should never reach here as guessTzid should be always defined for import
165             throw new ImportEventError({
166                 errorType: IMPORT_EVENT_ERROR_TYPE.UNEXPECTED_FLOATING_TIME,
167                 componentIdentifiers,
168             });
169         }
170         if (hasXWrTimezone && !calendarTzid) {
171             if (isInvite) {
172                 throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
173                     componentIdentifiers,
174                     extendedType: EVENT_INVITATION_ERROR_TYPE.X_WR_TIMEZONE_UNSUPPORTED,
175                 });
176             }
177             throw new ImportEventError({
178                 errorType: IMPORT_EVENT_ERROR_TYPE.X_WR_TIMEZONE_UNSUPPORTED,
179                 componentIdentifiers,
180             });
181         }
182         return getDateTimeProperty(partDayProperty.value, calendarTzid);
183     }
185     const supportedTzid = getSupportedTimezone(partDayPropertyTzid);
187     if (!supportedTzid) {
188         if (isInvite) {
189             throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
190                 componentIdentifiers,
191                 extendedType: EVENT_INVITATION_ERROR_TYPE.TZID_UNSUPPORTED,
192             });
193         }
194         throw new ImportEventError({
195             errorType: IMPORT_EVENT_ERROR_TYPE.TZID_UNSUPPORTED,
196             componentIdentifiers,
197         });
198     }
200     return getDateTimeProperty(partDayProperty.value, supportedTzid);
203 export const getLinkedDateTimeProperty = ({
204     property,
205     componentIdentifiers,
206     linkedIsAllDay,
207     linkedTzid,
208     isInvite,
209 }: {
210     property: VcalDateOrDateTimeProperty;
211     componentIdentifiers: EventComponentIdentifiers;
212     linkedIsAllDay: boolean;
213     linkedTzid?: string;
214     isInvite?: boolean;
215 }): VcalDateOrDateTimeProperty => {
216     if (linkedIsAllDay) {
217         return dateToProperty(property.value);
218     }
219     if (getIsPropertyAllDay(property)) {
220         if (isInvite) {
221             throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_INVALID, {
222                 componentIdentifiers,
223                 extendedType: EVENT_INVITATION_ERROR_TYPE.ALLDAY_INCONSISTENCY,
224             });
225         }
226         throw new ImportEventError({
227             errorType: IMPORT_EVENT_ERROR_TYPE.ALLDAY_INCONSISTENCY,
228             componentIdentifiers,
229         });
230     }
231     const supportedTzid = getPropertyTzid(property);
232     if (!supportedTzid || !linkedTzid) {
233         if (isInvite) {
234             throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
235                 componentIdentifiers,
236                 extendedType: EVENT_INVITATION_ERROR_TYPE.UNEXPECTED_FLOATING_TIME,
237             });
238         }
239         // should never be reached
240         throw new ImportEventError({
241             errorType: IMPORT_EVENT_ERROR_TYPE.UNEXPECTED_FLOATING_TIME,
242             componentIdentifiers,
243         });
244     }
245     if (linkedTzid !== supportedTzid) {
246         // the linked date-time property should have the same tzid as dtstart
247         return getDateTimePropertyInDifferentTimezone(property, linkedTzid, linkedIsAllDay);
248     }
249     return getDateTimeProperty(property.value, linkedTzid);
252 export const getSupportedSequenceValue = (sequence = 0) => {
253     /**
254      * According to the RFC (https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.4), the sequence property can
255      * have INTEGER values, and the valid range for an integer is that of a 32-byte integer: -2147483648 to 2147483647,
256      * cf. https://www.rfc-editor.org/rfc/rfc5545#section-3.3.8
257      *
258      * Our BE does not support negative values, and we should not save anything bigger than 2147483687. We transform
259      * negative values into 0 and take the modulo of bigger ones.
260      */
261     if (sequence < 0) {
262         return 0;
263     }
264     if (sequence >= MAX_ICAL_SEQUENCE) {
265         return sequence % MAX_ICAL_SEQUENCE;
266     }
267     return sequence;
270 export const withSupportedSequence = (vevent: VcalVeventComponent) => {
271     const supportedSequence = getSupportedSequenceValue(vevent.sequence?.value);
273     return {
274         ...vevent,
275         sequence: { value: supportedSequence },
276     };
280  * Perform ICS surgery on a VEVENT component
281  */
282 export const getSupportedEvent = ({
283     method = ICAL_METHOD.PUBLISH,
284     vcalVeventComponent,
285     hasXWrTimezone,
286     calendarTzid,
287     guessTzid,
288     componentIdentifiers,
289     isEventInvitation,
290     generatedHashUid = false,
291     canImportEventColor,
292 }: {
293     method?: ICAL_METHOD;
294     vcalVeventComponent: VcalVeventComponent;
295     hasXWrTimezone: boolean;
296     calendarTzid?: string;
297     guessTzid?: string;
298     componentIdentifiers: EventComponentIdentifiers;
299     isEventInvitation?: boolean;
300     generatedHashUid?: boolean;
301     canImportEventColor?: boolean;
302 }): VcalVeventComponent => {
303     const isPublish = method === ICAL_METHOD.PUBLISH;
304     const isInvitation = isEventInvitation && !isPublish;
305     try {
306         // common surgery
307         const {
308             component,
309             components,
310             uid,
311             dtstamp,
312             dtstart,
313             dtend,
314             rrule,
315             exdate,
316             description,
317             summary,
318             location,
319             sequence,
320             'recurrence-id': recurrenceId,
321             organizer,
322             attendee,
323             duration,
324             'x-pm-session-key': sharedSessionKey,
325             'x-pm-shared-event-id': sharedEventID,
326             'x-pm-proton-reply': protonReply,
327             'x-yahoo-yid': xYahooID,
328             'x-yahoo-user-status': xYahooUserStatus,
329             'x-pm-conference-id': xConferenceId,
330             'x-pm-conference-url': xConferenceUrl,
331             color,
332         } = vcalVeventComponent;
334         const [trimmedSummaryValue, trimmedDescriptionValue, trimmedLocationValue] = [
335             summary,
336             description,
337             location,
338         ].map(getSupportedStringValue);
339         const isRecurring = !!rrule || !!recurrenceId;
341         const validated: VcalVeventComponent = {
342             component,
343             uid: { value: getSupportedUID(uid.value) },
344             dtstamp: { ...dtstamp },
345             dtstart: { ...dtstart },
346             sequence: { value: getSupportedSequenceValue(sequence?.value) },
347         };
348         let ignoreRrule = false;
350         if (trimmedSummaryValue) {
351             validated.summary = {
352                 ...summary,
353                 value: truncate(trimmedSummaryValue, MAX_CHARS_API.TITLE),
354             };
355         }
356         if (trimmedDescriptionValue) {
357             validated.description = {
358                 ...description,
359                 value: truncate(trimmedDescriptionValue, MAX_CHARS_API.EVENT_DESCRIPTION),
360             };
361         }
362         if (trimmedLocationValue) {
363             validated.location = {
364                 ...location,
365                 value: truncate(trimmedLocationValue, MAX_CHARS_API.LOCATION),
366             };
367         }
369         validated.dtstart = getSupportedDateOrDateTimeProperty({
370             property: dtstart,
371             componentIdentifiers,
372             hasXWrTimezone,
373             calendarTzid,
374             isRecurring,
375             isInvite: isEventInvitation,
376             guessTzid,
377         });
379         const isAllDayStart = getIsPropertyAllDay(validated.dtstart);
380         const startTzid = getPropertyTzid(validated.dtstart);
381         if (!getIsWellFormedDateOrDateTime(validated.dtstart)) {
382             if (isEventInvitation) {
383                 throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_INVALID, {
384                     componentIdentifiers,
385                     extendedType: EVENT_INVITATION_ERROR_TYPE.DTSTART_MALFORMED,
386                 });
387             }
388             throw new ImportEventError({
389                 errorType: IMPORT_EVENT_ERROR_TYPE.DTSTART_MALFORMED,
390                 componentIdentifiers,
391             });
392         }
393         if (getIsDateOutOfBounds(validated.dtstart)) {
394             if (isEventInvitation) {
395                 throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
396                     componentIdentifiers,
397                     extendedType: EVENT_INVITATION_ERROR_TYPE.DTSTART_OUT_OF_BOUNDS,
398                 });
399             }
400             throw new ImportEventError({
401                 errorType: IMPORT_EVENT_ERROR_TYPE.DTSTART_OUT_OF_BOUNDS,
402                 componentIdentifiers,
403             });
404         }
405         if (dtend) {
406             const supportedDtend = getSupportedDateOrDateTimeProperty({
407                 property: dtend,
408                 componentIdentifiers,
409                 hasXWrTimezone,
410                 calendarTzid,
411                 isRecurring,
412                 isInvite: isEventInvitation,
413                 guessTzid,
414             });
415             if (!getIsWellFormedDateOrDateTime(supportedDtend)) {
416                 if (isEventInvitation) {
417                     throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_INVALID, {
418                         componentIdentifiers,
419                         extendedType: EVENT_INVITATION_ERROR_TYPE.DTEND_MALFORMED,
420                     });
421                 }
422                 throw new ImportEventError({
423                     errorType: IMPORT_EVENT_ERROR_TYPE.DTEND_MALFORMED,
424                     componentIdentifiers,
425                 });
426             }
427             const startDateUTC = propertyToUTCDate(validated.dtstart);
428             const endDateUTC = propertyToUTCDate(supportedDtend);
429             // allow a non-RFC-compliant all-day event with DTSTART = DTEND
430             const modifiedEndDateUTC =
431                 !getIsPropertyAllDay(dtend) || +startDateUTC === +endDateUTC ? endDateUTC : addDays(endDateUTC, -1);
432             const eventDuration = +modifiedEndDateUTC - +startDateUTC;
434             if (eventDuration > 0) {
435                 validated.dtend = supportedDtend;
436             }
437         } else if (duration) {
438             const dtendFromDuration = getDtendPropertyFromDuration(validated.dtstart, duration.value);
440             if (dtendFromDuration) {
441                 validated.dtend = dtendFromDuration;
442             }
443         }
445         if (validated.dtend && getIsDateOutOfBounds(validated.dtend)) {
446             if (isEventInvitation) {
447                 throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
448                     componentIdentifiers,
449                     extendedType: EVENT_INVITATION_ERROR_TYPE.DTEND_OUT_OF_BOUNDS,
450                 });
451             }
452             throw new ImportEventError({
453                 errorType: IMPORT_EVENT_ERROR_TYPE.DTEND_OUT_OF_BOUNDS,
454                 componentIdentifiers,
455             });
456         }
458         const isAllDayEnd = validated.dtend ? getIsPropertyAllDay(validated.dtend) : undefined;
460         if (isAllDayEnd !== undefined && +isAllDayStart ^ +isAllDayEnd) {
461             if (isEventInvitation) {
462                 throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_INVALID, {
463                     componentIdentifiers,
464                     extendedType: EVENT_INVITATION_ERROR_TYPE.ALLDAY_INCONSISTENCY,
465                 });
466             }
467             throw new ImportEventError({
468                 errorType: IMPORT_EVENT_ERROR_TYPE.ALLDAY_INCONSISTENCY,
469                 componentIdentifiers,
470             });
471         }
473         if (exdate) {
474             if (!rrule) {
475                 if (isEventInvitation) {
476                     throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_INVALID, {
477                         componentIdentifiers,
478                         extendedType: EVENT_INVITATION_ERROR_TYPE.RRULE_MALFORMED,
479                     });
480                 }
481                 throw new ImportEventError({
482                     errorType: IMPORT_EVENT_ERROR_TYPE.RRULE_MALFORMED,
483                     componentIdentifiers,
484                 });
485             }
486             const supportedExdate = exdate.map((property) =>
487                 getSupportedDateOrDateTimeProperty({
488                     property,
489                     componentIdentifiers,
490                     hasXWrTimezone,
491                     calendarTzid,
492                     isRecurring,
493                     isInvite: isEventInvitation,
494                     guessTzid,
495                 })
496             );
497             validated.exdate = supportedExdate.map((property) =>
498                 getLinkedDateTimeProperty({
499                     property,
500                     componentIdentifiers,
501                     linkedIsAllDay: isAllDayStart,
502                     linkedTzid: startTzid,
503                     isInvite: isEventInvitation,
504                 })
505             );
506         }
507         // Do not keep recurrence ids when we generated a hash UID, as the RECURRENCE-ID is meaningless then
508         if (recurrenceId && !generatedHashUid) {
509             if (rrule) {
510                 if (method === ICAL_METHOD.REPLY) {
511                     // the external provider forgot to remove the RRULE
512                     ignoreRrule = true;
513                 } else {
514                     if (isEventInvitation) {
515                         throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
516                             componentIdentifiers,
517                             extendedType: EVENT_INVITATION_ERROR_TYPE.SINGLE_EDIT_UNSUPPORTED,
518                         });
519                     }
520                     throw new ImportEventError({
521                         errorType: IMPORT_EVENT_ERROR_TYPE.SINGLE_EDIT_UNSUPPORTED,
522                         componentIdentifiers,
523                     });
524                 }
525             }
526             // RECURRENCE-ID cannot be linked with DTSTART of the parent event at this point since we do not have access to it
527             validated['recurrence-id'] = getSupportedDateOrDateTimeProperty({
528                 property: recurrenceId,
529                 componentIdentifiers,
530                 hasXWrTimezone,
531                 calendarTzid,
532                 isRecurring,
533                 isInvite: isEventInvitation,
534                 guessTzid,
535             });
536         }
538         if (rrule && !ignoreRrule) {
539             const supportedRrule = getSupportedRrule({ ...validated, rrule }, isInvitation, guessTzid);
540             if (!supportedRrule) {
541                 if (isEventInvitation) {
542                     throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
543                         componentIdentifiers,
544                         extendedType: EVENT_INVITATION_ERROR_TYPE.RRULE_UNSUPPORTED,
545                     });
546                 }
547                 throw new ImportEventError({
548                     errorType: IMPORT_EVENT_ERROR_TYPE.RRULE_UNSUPPORTED,
549                     componentIdentifiers,
550                 });
551             }
552             validated.rrule = supportedRrule;
553             if (!getHasConsistentRrule(validated)) {
554                 if (isEventInvitation) {
555                     throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_INVALID, {
556                         componentIdentifiers,
557                         extendedType: EVENT_INVITATION_ERROR_TYPE.RRULE_MALFORMED,
558                     });
559                 }
560                 throw new ImportEventError({
561                     errorType: IMPORT_EVENT_ERROR_TYPE.RRULE_MALFORMED,
562                     componentIdentifiers,
563                 });
564             }
565             if (!getHasOccurrences(validated)) {
566                 if (isEventInvitation) {
567                     throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_INVALID, {
568                         componentIdentifiers,
569                         extendedType: EVENT_INVITATION_ERROR_TYPE.NO_OCCURRENCES,
570                     });
571                 }
572                 throw new ImportEventError({
573                     errorType: IMPORT_EVENT_ERROR_TYPE.NO_OCCURRENCES,
574                     componentIdentifiers,
575                 });
576             }
577         }
579         // import-specific surgery
580         if (!isInvitation) {
581             if (!isEventInvitation && isPublish) {
582                 const alarms = components?.filter(({ component }) => component === 'valarm') || [];
583                 const supportedAlarms = getSupportedAlarms(alarms, dtstart);
584                 const dedupedAlarms = dedupeAlarmsWithNormalizedTriggers(supportedAlarms);
586                 if (dedupedAlarms.length) {
587                     validated.components = dedupedAlarms;
588                 }
589             }
591             if (canImportEventColor && color) {
592                 const closestColor = getClosestProtonColor(color.value);
594                 // If closest color is undefined, it means that the color format was invalid. In that case, we ignore the color.
595                 if (closestColor) {
596                     validated.color = { value: closestColor };
597                 }
598             }
599         }
601         // Zoom integration specific surgery
602         if (xConferenceId) {
603             // Needed to interpret non RFC-compliant Yahoo REPLY ics's
604             validated['x-pm-conference-id'] = { ...xConferenceId };
605         }
606         if (xConferenceUrl) {
607             // Needed to interpret non RFC-compliant Yahoo REPLY ics's
608             validated['x-pm-conference-url'] = { ...xConferenceUrl };
609         }
611         // invite-specific surgery
612         if (isInvitation) {
613             if (sharedSessionKey) {
614                 validated['x-pm-session-key'] = { ...sharedSessionKey };
615             }
616             if (sharedEventID) {
617                 validated['x-pm-shared-event-id'] = { ...sharedEventID };
618             }
619             if (protonReply) {
620                 validated['x-pm-proton-reply'] = { ...protonReply };
621             }
622             if (xYahooID) {
623                 // Needed to interpret non RFC-compliant Yahoo REPLY ics's
624                 validated['x-yahoo-yid'] = { ...xYahooID };
625             }
626             if (xYahooUserStatus) {
627                 // Needed to interpret non RFC-compliant Yahoo REPLY ics's
628                 validated['x-yahoo-user-status'] = { ...xYahooUserStatus };
629             }
630             if (organizer) {
631                 validated.organizer = getSupportedOrganizer(organizer);
632             } else {
633                 // The ORGANIZER field is mandatory in an invitation
634                 throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_INVALID, {
635                     componentIdentifiers,
636                     extendedType: EVENT_INVITATION_ERROR_TYPE.MISSING_ORGANIZER,
637                 });
638             }
640             if (attendee) {
641                 const attendeeEmails = attendee.map((att) => getAttendeeEmail(att));
642                 // Some attendees might be malformed, meaning that we receive an empty string as email
643                 // We want it to be possible to still add the event in such cases.
644                 // To do so, we start by cleaning the emails array, so that we get all correct emails.
645                 // Then we can search for duplicate emails, which will lead to an invitation unsupported error
646                 const cleanedAttendeeEmails = attendeeEmails.filter((attendee) => attendee !== '');
647                 if (unique(cleanedAttendeeEmails).length !== cleanedAttendeeEmails.length) {
648                     // Do not accept invitations with repeated emails as they will cause problems.
649                     // Usually external providers don't allow this to happen
650                     throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
651                         componentIdentifiers,
652                         extendedType: EVENT_INVITATION_ERROR_TYPE.DUPLICATE_ATTENDEES,
653                     });
654                 }
655                 // Do not add malformed attendee
656                 const supportedAttendee = attendee.reduce<RequireSome<VcalAttendeeProperty, 'parameters'>[]>(
657                     (acc, vcalAttendee) => {
658                         const attendeeEmail = getAttendeeEmail(vcalAttendee);
659                         if (validateEmailAddress(attendeeEmail)) {
660                             acc.push(getSupportedAttendee(vcalAttendee));
661                         }
662                         return acc;
663                     },
664                     []
665                 );
666                 validated.attendee = supportedAttendee;
667             }
668         }
670         return validated;
671     } catch (e: any) {
672         if (e instanceof ImportEventError || e instanceof EventInvitationError) {
673             throw e;
674         }
675         if (isEventInvitation) {
676             throw new EventInvitationError(INVITATION_ERROR_TYPE.INVITATION_UNSUPPORTED, {
677                 componentIdentifiers,
678                 extendedType: EVENT_INVITATION_ERROR_TYPE.EXTERNAL_ERROR,
679                 externalError: e,
680             });
681         }
682         throw new ImportEventError({
683             errorType: IMPORT_EVENT_ERROR_TYPE.VALIDATION_ERROR,
684             componentIdentifiers,
685         });
686     }