import { getAttendeeEmail } from '@proton/shared/lib/calendar/attendees' ; import { getDoesCalendarNeedUserAction , getIsCalendarDisabled } from '@proton/shared/lib/calendar/calendar' ; import { ICAL_EXTENSIONS , ICAL_METHOD , ICAL_METHODS_ATTENDEE } from '@proton/shared/lib/calendar/constants' ; import { getSelfAddressData } from '@proton/shared/lib/calendar/deserialize' ; import { generateVeventHashUID } from '@proton/shared/lib/calendar/helper' ; import { EVENT_INVITATION_ERROR_TYPE , EventInvitationError , } from '@proton/shared/lib/calendar/icsSurgery/EventInvitationError' ; import { getSupportedEvent } from '@proton/shared/lib/calendar/icsSurgery/vevent' ; import { findAttendee , getParticipant } from '@proton/shared/lib/calendar/integration/invite' ; import { getOccurrencesBetween } from '@proton/shared/lib/calendar/recurring' ; import { parseWithErrors , serialize } from '@proton/shared/lib/calendar/vcal' ; import { buildVcalOrganizer , dateTimeToProperty , getDtendProperty , propertyToLocalDate , propertyToUTCDate , } from '@proton/shared/lib/calendar/vcalConverter' ; import { getHasDtStart , getHasRecurrenceId , getIcalMethod , getIsCalendar , getIsEventComponent , getIsProtonReply , getIsRecurring , getIsTimezoneComponent , getIsValidMethod , getIsXOrIanaComponent , getIsYahooEvent , getPmSharedEventID , getPmSharedSessionKey , getSequence , } from '@proton/shared/lib/calendar/vcalHelper' ; import { getIsEventCancelled , withDtstamp } from '@proton/shared/lib/calendar/veventHelper' ; import { SECOND } from '@proton/shared/lib/constants' ; import { fromUTCDate , getSupportedTimezone } from '@proton/shared/lib/date/timezone' ; import { getIsAddressActive , getIsAddressDisabled } from '@proton/shared/lib/helpers/address' ; import { canonizeEmailByGuess , canonizeInternalEmail } from '@proton/shared/lib/helpers/email' ; import { splitExtension } from '@proton/shared/lib/helpers/file' ; import { unary } from '@proton/shared/lib/helpers/function' ; import { omit } from '@proton/shared/lib/helpers/object' ; import { Address } from '@proton/shared/lib/interfaces' ; import { Calendar , CalendarEventWithMetadata , CalendarWidgetData , Participant , PmInviteData , VcalDateOrDateTimeProperty , VcalNumberProperty , VcalStringProperty , VcalVeventComponent , VcalVcalendar , VcalVtimezoneComponent , VcalXOrIanaComponent , } from '@proton/shared/lib/interfaces/calendar' ; import { ContactEmail } from '@proton/shared/lib/interfaces/contacts' ; import { Attachment , Message } from '@proton/shared/lib/interfaces/mail/Message' ; import { RequireSome , Unwrap } from '@proton/shared/lib/interfaces/utils' ; import { getOriginalTo } from '@proton/shared/lib/mail/messages' ; import { getUnixTime } from 'date-fns' ; import { serverTime } from 'pmcrypto' ; import { MessageStateWithData } from '../../logic/messages/messagesTypes' ; import { FetchAllEventsByUID } from './inviteApi' ; export enum EVENT_TIME_STATUS { PAST , HAPPENING , FUTURE } export interface EventInvitation { originalVcalInvitation ? : VcalVcalendar ; originalUniqueIdentifier ? : string ; fileName ? : string ; vevent : VcalVeventComponent ; hasMultipleVevents ? : boolean ; calendarEvent ? : CalendarEventWithMetadata ; method ? : ICAL_METHOD ; vtimezone ? : VcalVtimezoneComponent ; xOrIanaComponents ? : VcalXOrIanaComponent [ ] ; organizer ? : Participant ; attendee ? : Participant ; participants ? : Participant [ ] ; } export enum UPDATE_ACTION { NONE , KEEP_PARTSTAT , RESET_PARTSTAT , UPDATE_PARTSTAT , CANCEL } export interface InvitationModel { isImport : boolean ; isOrganizerMode : boolean ; hasMultipleVevents : boolean ; timeStatus : EVENT_TIME_STATUS ; isPartyCrasher ? : boolean ; isAddressActive : boolean ; isAddressDisabled : boolean ; canCreateCalendar : boolean ; maxUserCalendarsDisabled : boolean ; mustReactivateCalendars : boolean ; hasNoCalendars : boolean ; isOutdated ? : boolean ; isFromFuture ? : boolean ; reinviteEventID ? : string ; updateAction ? : UPDATE_ACTION ; hideSummary ? : boolean ; hideLink ? : boolean ; calendarData ? : CalendarWidgetData ; singleEditData ? : CalendarEventWithMetadata [ ] ; pmData ? : PmInviteData ; invitationIcs ? : RequireSome < EventInvitation , 'method' > ; invitationApi ? : RequireSome < EventInvitation , 'calendarEvent' > ; parentInvitationApi ? : RequireSome < EventInvitation , 'calendarEvent' > ; error ? : EventInvitationError ; hasDecryptionError ? : boolean ; } interface NonRFCCompliantVcalendar extends VcalVcalendar { uid ? : VcalStringProperty ; sequence ? : VcalNumberProperty ; } export const getHasInvitationIcs = ( model ) : model is RequireSome < InvitationModel , 'invitationIcs' > => { return ! ! model . invitationIcs ; } ; export const getHasInvitationApi = ( model ) : model is RequireSome < InvitationModel , 'invitationApi' > => { return ! ! model . invitationApi ; } ; export const getInvitationHasMethod = ( invitation ) : invitation is RequireSome < EventInvitation , 'method' > => { return invitation . method !== undefined ; } ; export const getInvitationHasEventID = ( invitation ) : invitation is RequireSome < EventInvitation , 'calendarEvent' > => { return invitation . calendarEvent ?. ID !== undefined ; } ; export const getInvitationHasAttendee = ( invitation ) : invitation is RequireSome < EventInvitation , 'attendee' > => { return invitation . attendee !== undefined ; } ; export const getHasFullCalendarData = ( data ? ) : data is Required < CalendarWidgetData > => { const { memberID , addressKeys , calendarKeys , calendarSettings } = data || { } ; return ! ! ( memberID && addressKeys && calendarKeys && calendarSettings ) ; } ; export const filterAttachmentsForEvents = ( attachments ) : Attachment [ ] => attachments . filter ( ( { Name = '' } ) => ICAL_EXTENSIONS . includes ( splitExtension ( Name ) [ 1 ] ) ) ; export const withOutsideUIDAndSequence = ( vevent , vcal ) => { const { uid : veventUid , sequence : veventSequence } = vevent ; const { uid : vcalUid , sequence : vcalSequence } = vcal ; const result = { ... vevent } ; if ( ! veventUid && vcalUid ) { result . uid = { ... vcalUid } ; } if ( ! veventSequence && vcalSequence ) { result . sequence = { ... vcalSequence } ; } return result ; } ; const withMessageDtstamp = < T > ( properties , { Time } ) : VcalVeventComponent & T => { if ( properties . dtstamp ) { return properties ; } return { ... properties , dtstamp : dateTimeToProperty ( fromUTCDate ( new Date ( Time * SECOND ) ) , true ) , } ; } ; export const getHasMultipleVevents = ( vcal ? ) => { const numberOfVevents = vcal ?. components ?. filter ( unary ( getIsEventComponent ) ) . length || 0 ; return numberOfVevents > 1 ; } ; export const extractVevent = ( vcal ? ) : VcalVeventComponent | undefined => { const result = vcal ?. components ?. find ( getIsEventComponent ) ; return result ? { ... result } : undefined ; } ; export const extractUniqueVTimezone = ( vcal ? ) : VcalVtimezoneComponent | undefined => { const vtimezones = vcal ?. components ?. filter ( getIsTimezoneComponent ) ; if ( vtimezones ?. length === 1 ) { return vtimezones [ 0 ] ; } } ; export const extractXOrIanaComponents = ( vcal ? ) : VcalXOrIanaComponent [ ] | undefined => { const result = vcal ?. components ?. filter ( getIsXOrIanaComponent ) ; return result ? { ... result } : undefined ; } ; export const getIsOrganizerMode = ( event , emailTo ) => { if ( ! event . organizer ) { return false ; } const organizerEmail = getAttendeeEmail ( event . organizer ) ; return canonizeInternalEmail ( organizerEmail ) === canonizeInternalEmail ( emailTo ) ; } ; export const getSingleEditWidgetData = ( { otherEvents , otherParentEvents , } ) => { return ( otherParentEvents || otherEvents ) . filter ( ( { RecurrenceID } ) => ! ! RecurrenceID ) ; } ; export const getIsInvitationOutdated = ( { invitationIcs , invitationApi , isOrganizerMode , } ) => { const veventIcs = invitationIcs . vevent ; const veventApi = invitationApi ?. vevent ; const updateTime = invitationApi ?. attendee ?. updateTime ; if ( ! veventApi ) { return false ; } const timestampIcs = veventIcs . dtstamp ? getUnixTime ( propertyToUTCDate ( veventIcs . dtstamp ) ) : undefined ; const timestampApi = veventApi . dtstamp ? getUnixTime ( propertyToUTCDate ( veventApi . dtstamp ) ) : undefined ; const timestampDiff = timestampIcs !== undefined && timestampApi !== undefined ? timestampIcs - timestampApi : 0 ; const updateTimeDiff = timestampIcs !== undefined && updateTime !== undefined ? timestampIcs - updateTime : timestampDiff ; const sequenceDiff = getSequence ( veventIcs ) - getSequence ( veventApi ) ; if ( isOrganizerMode ) { return sequenceDiff < 0 || updateTimeDiff < 0 ; } return sequenceDiff < 0 || timestampDiff < 0 ; } ; export const getIsInvitationFromFuture = ( { invitationIcs , invitationApi , isOrganizerMode , } ) => { const veventIcs = invitationIcs . vevent ; const veventApi = invitationApi ?. vevent ; if ( ! veventApi ) { return false ; } const sequenceDiff = getSequence ( veventIcs ) - getSequence ( veventApi ) ; return isOrganizerMode && sequenceDiff > 0 ; } ; /**
 * A PM invite is either a new invitation or one for which the calendar event
 * is linked to the one sent in the ics
 */ export const getIsProtonInvite = ( { invitationIcs , invitationApi , pmData , } ) => { const { method } = invitationIcs ; const { sharedEventID , isProtonReply } = pmData || { } ; if ( [ ICAL_METHOD . REQUEST , ICAL_METHOD . CANCEL ] . includes ( method ) ) { if ( ! invitationApi ) { return ! ! sharedEventID ; } return sharedEventID === invitationApi . calendarEvent . SharedEventID ; } if ( method === ICAL_METHOD . REPLY ) { return ! ! isProtonReply ; } return false ; } ; /**
 * Detect case of being re-invited
 */ export const getIsReinvite = ( { invitationIcs , invitationApi , isOrganizerMode , } ) => { const { method } = invitationIcs ; const isOutdated = getIsInvitationOutdated ( { invitationIcs , invitationApi , isOrganizerMode } ) ; if ( isOrganizerMode || method !== ICAL_METHOD . REQUEST || ! invitationApi || isOutdated ) { return false ; } const { calendarEvent } = invitationApi ; return getIsEventCancelled ( calendarEvent ) ; } ; export const getIsNonSoughtEvent = ( event , vevent , supportedRecurrenceId ? ) => { if ( ! event . RecurrenceID ) { return false ; } if ( ! getHasRecurrenceId ( vevent ) ) { return true ; } return getUnixTime ( propertyToUTCDate ( supportedRecurrenceId || vevent [ 'recurrence-id' ] ) ) !== event . RecurrenceID ; } ; /**
 * Determines if a single edit can be created from a parent. So basically check if the recurrence-id
 * matches a parent occurrence
 */ export const getCanCreateSingleEdit = ( singleEditVevent , parentVevent ) => { if ( ! getIsRecurring ( parentVevent ) || ! getHasRecurrenceId ( singleEditVevent ) ) { return false ; } const utcStart = + propertyToUTCDate ( singleEditVevent [ 'recurrence-id' ] ) ; const occurrencesAtStart = getOccurrencesBetween ( parentVevent , utcStart , utcStart ) ; return occurrencesAtStart . length === 1 ; } ; /**
 * Determines if a event has already passed with respect to a timestamp in milliseconds
 */ export const getEventTimeStatus = ( vevent , now ) => { if ( vevent . rrule ?. value ) { return EVENT_TIME_STATUS . FUTURE ; } const nowTimestamp = getUnixTime ( now ) ; const startTimestamp = getUnixTime ( propertyToLocalDate ( vevent . dtstart ) ) ; const endTimestamp = getUnixTime ( propertyToLocalDate ( getDtendProperty ( vevent ) ) ) ; if ( nowTimestamp >= endTimestamp ) { return EVENT_TIME_STATUS . PAST ; } if ( nowTimestamp >= startTimestamp ) { return EVENT_TIME_STATUS . HAPPENING ; } return EVENT_TIME_STATUS . FUTURE ; } ; export const parseVcalendar = ( data ) : VcalVcalendar | undefined => { try { if ( ! data ) { return ; } const parsedVcalendar = parseWithErrors ( data ) as VcalVcalendar ; if ( ! getIsCalendar ( parsedVcalendar ) ) { return ; } return parsedVcalendar ; } catch ( e ) { throw new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . PARSING_ERROR ) ; } } ; interface ProcessedInvitation < T > { isImport : boolean ; isOrganizerMode : boolean ; timeStatus : EVENT_TIME_STATUS ; isAddressActive : boolean ; isAddressDisabled : boolean ; invitation : EventInvitation & T ; } export const processEventInvitation = < T > ( invitation , message , contactEmails , ownAddresses ) : ProcessedInvitation < T > => { const { vevent , calendarEvent , method } = invitation ; const isImport = method === ICAL_METHOD . PUBLISH ; const timeStatus = getEventTimeStatus ( vevent , + serverTime ( ) ) ; const attendees = vevent . attendee || [ ] ; const { organizer } = vevent ; const originalTo = getOriginalTo ( message . data ) ; const originalFrom = message . data . Sender . Address ; const isOrganizerMode = isImport ? false : getIsOrganizerMode ( vevent , originalTo ) ; const { selfAddress , selfAttendee } = getSelfAddressData ( { isOrganizer : isOrganizerMode , organizer , attendees , addresses : ownAddresses , } ) ; const isAddressActive = selfAddress ? getIsAddressActive ( selfAddress ) : true ; const isAddressDisabled = selfAddress ? getIsAddressDisabled ( selfAddress ) : false ; const processed = { ... invitation } ; if ( attendees ) { processed . participants = attendees . map ( ( attendee ) => getParticipant ( { participant : attendee , contactEmails , selfAddress , selfAttendee , emailTo : originalTo , } ) ) ; } if ( organizer ) { processed . organizer = getParticipant ( { participant : organizer , selfAddress , contactEmails , emailTo : originalTo , } ) ; } if ( isOrganizerMode ) { const { attendee , index } = findAttendee ( originalFrom , attendees ) ; if ( attendee ) { processed . attendee = getParticipant ( { participant : attendee , selfAddress , contactEmails , emailTo : originalTo , index , calendarAttendees : calendarEvent ?. Attendees , xYahooUserStatus : vevent [ 'x-yahoo-user-status' ] ?. value , } ) ; } } else if ( selfAttendee ) { processed . attendee = getParticipant ( { participant : selfAttendee , selfAddress , selfAttendee , contactEmails , emailTo : originalTo , calendarAttendees : calendarEvent ?. Attendees , } ) ; } return { isImport , isOrganizerMode , timeStatus , isAddressActive , isAddressDisabled , invitation : processed , } ; } ; interface GetInitialInvitationModelArgs { invitationOrError : EventInvitation | EventInvitationError ; message : MessageStateWithData ; contactEmails : ContactEmail [ ] ; ownAddresses : Address [ ] ; calendar ? : Calendar ; hasNoCalendars : boolean ; canCreateCalendar : boolean ; maxUserCalendarsDisabled : boolean ; mustReactivateCalendars : boolean ; } export const getInitialInvitationModel = ( { invitationOrError , message , contactEmails , ownAddresses , calendar , hasNoCalendars , canCreateCalendar , maxUserCalendarsDisabled , mustReactivateCalendars , } ) => { const errorModel = { isImport : false , hasMultipleVevents : false , isOrganizerMode : false , isAddressDisabled : false , isAddressActive : true , canCreateCalendar , maxUserCalendarsDisabled , mustReactivateCalendars , hasNoCalendars , timeStatus : EVENT_TIME_STATUS . FUTURE , } ; if ( invitationOrError instanceof EventInvitationError ) { return { ... errorModel , error : invitationOrError } ; } if ( ! getInvitationHasMethod ( invitationOrError ) ) { throw new Error ( 'Initial invitation lacks ICAL method' ) ; } const { isOrganizerMode , isImport , timeStatus , isAddressActive , isAddressDisabled , invitation } = processEventInvitation < RequireSome < EventInvitation , 'method' > > ( invitationOrError , message , contactEmails , ownAddresses ) ; if ( invitation . method === ICAL_METHOD . REPLY && ! invitation . attendee ) { return { ... errorModel , error : new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . INVITATION_INVALID , { method : invitation . method , } ) , } ; } const result = { isOrganizerMode , isImport , hasMultipleVevents : ! ! invitation . hasMultipleVevents , timeStatus , isAddressActive , isAddressDisabled , canCreateCalendar , maxUserCalendarsDisabled , mustReactivateCalendars , hasNoCalendars , invitationIcs : invitation , isPartyCrasher : isOrganizerMode || isImport ? false : ! invitation . attendee , } ; if ( calendar ) { result . calendarData = { calendar , isCalendarDisabled : getIsCalendarDisabled ( calendar ) , calendarNeedsUserAction : getDoesCalendarNeedUserAction ( calendar ) , } ; } if ( ! isImport && ! getIsValidMethod ( invitation . method , isOrganizerMode ) ) { result . error = new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . INVALID_METHOD , { method : invitation . method , } ) ; } const isProtonReply = getIsProtonReply ( invitation . vevent ) ; const sharedEventID = getPmSharedEventID ( invitation . vevent ) ; const sharedSessionKey = getPmSharedSessionKey ( invitation . vevent ) ; if ( isProtonReply !== undefined || sharedEventID || sharedSessionKey ) { const pmData = { } ; if ( isProtonReply !== undefined ) { pmData . isProtonReply = isProtonReply ; } if ( sharedEventID ) { pmData . sharedEventID = sharedEventID ; } if ( sharedSessionKey ) { pmData . sharedSessionKey = sharedSessionKey ; } result . pmData = pmData ; } const isYahooEvent = getIsYahooEvent ( invitation . vevent ) ; if ( isYahooEvent ) { invitation . vevent = withDtstamp ( omit ( invitation . vevent , [ 'dtstamp' ] ) , message . data . Time * SECOND ) ; } return result ; } ; export const getSupportedEventInvitation = async ( { vcalComponent , message , icsBinaryString , icsFileName , } ) : Promise < EventInvitation | undefined > => { const { calscale , 'x-wr-timezone' : xWrTimezone , method } = vcalComponent ; const supportedMethod = getIcalMethod ( method ) ; if ( ! supportedMethod ) { throw new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . INVALID_METHOD ) ; } if ( calscale && calscale . value . toLowerCase ( ) !== 'gregorian' ) { throw new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . INVITATION_UNSUPPORTED , { method : supportedMethod } ) ; } if ( ! vcalComponent . components ?. length ) { throw new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . NO_COMPONENT , { method : supportedMethod } ) ; } const vevent = extractVevent ( vcalComponent ) ; const vtimezone = extractUniqueVTimezone ( vcalComponent ) ; if ( ! vevent ) { throw new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . NO_VEVENT , { method : supportedMethod } ) ; } if ( ! getHasDtStart ( vevent ) ) { throw new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . INVITATION_INVALID , { method : supportedMethod } ) ; } const completeVevent = withOutsideUIDAndSequence ( vevent , vcalComponent ) ; const hasMultipleVevents = getHasMultipleVevents ( vcalComponent ) ; const isImport = supportedMethod === ICAL_METHOD . PUBLISH ; const originalUID = completeVevent . uid ?. value ; const originalUniqueIdentifier = hasMultipleVevents || ! originalUID ? await generateVeventHashUID ( serialize ( vcalComponent ) ) : originalUID ; if ( isImport ) { const sha1Uid = await generateVeventHashUID ( icsBinaryString , completeVevent . uid ?. value ) ; completeVevent . uid = { value : sha1Uid } ; } else if ( ! completeVevent . organizer ) { const guessOrganizerEmail = ICAL_METHODS_ATTENDEE . includes ( supportedMethod ) ? getOriginalTo ( message ) : message . Sender . Address ; completeVevent . organizer = buildVcalOrganizer ( guessOrganizerEmail ) ; } const hasXWrTimezone = ! ! xWrTimezone ?. value ; const calendarTzid = xWrTimezone ? getSupportedTimezone ( xWrTimezone . value ) : undefined ; const invitationTzid = vtimezone ?. tzid . value ; const guessTzid = invitationTzid ? getSupportedTimezone ( invitationTzid ) : undefined ; try { const supportedEvent = getSupportedEvent ( { method : supportedMethod , vcalVeventComponent : withMessageDtstamp ( completeVevent , message ) , hasXWrTimezone , calendarTzid , guessTzid , isEventInvitation : true , generatedHashUid : isImport , } ) ; return { method : supportedMethod , vevent : supportedEvent , vtimezone , originalVcalInvitation : vcalComponent , originalUniqueIdentifier , hasMultipleVevents , fileName : icsFileName , } ; } catch ( error ) { if ( error instanceof EventInvitationError ) { throw error ; } throw new EventInvitationError ( EVENT_INVITATION_ERROR_TYPE . INVITATION_UNSUPPORTED , { externalError : error , method : supportedMethod , } ) ; } } ; export const getDoNotDisplayButtons = ( model ) => { const { isImport , hasMultipleVevents , isOrganizerMode , isPartyCrasher , invitationIcs : { method , vevent : veventIcs } , invitationApi , calendarData , isOutdated , isAddressActive , } = model ; if ( isOrganizerMode ) { return ! isPartyCrasher || ! ! veventIcs [ 'recurrence-id' ] ; } if ( isImport && ( invitationApi || hasMultipleVevents ) ) { return true ; } return ( method === ICAL_METHOD . CANCEL || ! ! isOutdated || ! isAddressActive || ! ! calendarData ?. isCalendarDisabled || isPartyCrasher ) ; } ; export const getDisableButtons = ( model ) => { const { calendarData , isAddressActive , isImport } = model ; const alwaysDisable = ! calendarData ?. calendar || calendarData . isCalendarDisabled || calendarData . calendarNeedsUserAction ; return isImport ? alwaysDisable : alwaysDisable || ! isAddressActive ; } ; export const getParticipantsList = ( attendees ? , organizer ? ) => { const list = attendees ? [ ... attendees ] : [ ] ; if ( organizer ) { const canonicalOrganizerEmail = canonizeEmailByGuess ( organizer . emailAddress ) ; const organizerIndex = list . findIndex ( ( { emailAddress } ) => canonizeEmailByGuess ( emailAddress ) === canonicalOrganizerEmail ) ; if ( organizerIndex !== - 1 ) { list . splice ( organizerIndex , 1 ) ; } list . unshift ( organizer ) ; } return list ; } ;