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