1 import type { PrivateKeyReference, PublicKeyReference, SessionKey } from '@proton/crypto';
3 import { getIsAddressActive, getIsAddressExternal } from '../helpers/address';
4 import { canonicalizeInternalEmail } from '../helpers/email';
5 import { base64StringToUint8Array } from '../helpers/encoding';
6 import type { Address, Nullable } from '../interfaces';
9 CalendarNotificationSettings,
12 VcalOrganizerProperty,
14 } from '../interfaces/calendar';
15 import type { SimpleMap } from '../interfaces/utils';
16 import { toSessionKey } from '../keys/sessionKey';
17 import { modelToValarmComponent } from './alarms/modelToValarm';
18 import { apiNotificationsToModel } from './alarms/notificationsToModel';
19 import { getAttendeeEmail, toInternalAttendee } from './attendees';
20 import { ICAL_ATTENDEE_STATUS } from './constants';
22 decryptAndVerifyCalendarEvent,
23 getAggregatedEventVerificationStatus,
24 getDecryptedSessionKey,
25 } from './crypto/decrypt';
26 import { unwrap } from './helper';
27 import { parseWithFoldingRecovery } from './icsSurgery/ics';
28 import { getAttendeePartstat, getIsEventComponent } from './vcalHelper';
30 export const readSessionKey = (
31 KeyPacket?: Nullable<string>,
32 privateKeys?: PrivateKeyReference | PrivateKeyReference[]
34 if (!KeyPacket || !privateKeys) {
37 return getDecryptedSessionKey(base64StringToUint8Array(KeyPacket), privateKeys);
41 * Read the session keys.
43 export const readSessionKeys = async ({
45 decryptedSharedKeyPacket,
48 calendarEvent: Pick<CalendarEvent, 'SharedKeyPacket' | 'AddressKeyPacket' | 'CalendarKeyPacket'>;
49 decryptedSharedKeyPacket?: string;
50 privateKeys?: PrivateKeyReference | PrivateKeyReference[];
52 const sharedsessionKeyPromise = decryptedSharedKeyPacket
53 ? Promise.resolve(toSessionKey(decryptedSharedKeyPacket))
54 : readSessionKey(calendarEvent.AddressKeyPacket || calendarEvent.SharedKeyPacket, privateKeys);
55 const calendarSessionKeyPromise = readSessionKey(calendarEvent.CalendarKeyPacket, privateKeys);
56 return Promise.all([sharedsessionKeyPromise, calendarSessionKeyPromise]);
59 const fromApiNotifications = ({
60 notifications: apiNotifications,
64 notifications: Nullable<CalendarNotificationSettings[]>;
66 calendarSettings: CalendarSettings;
68 const modelAlarms = apiNotificationsToModel({ notifications: apiNotifications, isAllDay, calendarSettings });
70 return modelAlarms.map((alarm) => modelToValarmComponent(alarm));
73 export const getSelfAddressData = ({
78 organizer?: VcalOrganizerProperty;
79 attendees?: VcalAttendeeProperty[];
80 addresses?: Address[];
83 // it is not an invitation
89 const internalAddresses = addresses.filter((address) => !getIsAddressExternal(address));
90 const ownCanonicalizedEmailsMap = internalAddresses.reduce<SimpleMap<string>>((acc, { Email }) => {
91 acc[Email] = canonicalizeInternalEmail(Email);
95 const organizerEmail = canonicalizeInternalEmail(getAttendeeEmail(organizer));
96 const organizerAddress = internalAddresses.find(({ Email }) => ownCanonicalizedEmailsMap[Email] === organizerEmail);
98 if (organizerAddress) {
102 selfAddress: organizerAddress,
106 const canonicalAttendeeEmails = attendees.map((attendee) => canonicalizeInternalEmail(getAttendeeEmail(attendee)));
108 // split active and inactive addresses
109 const { activeAddresses, inactiveAddresses } = internalAddresses.reduce<{
110 activeAddresses: Address[];
111 inactiveAddresses: Address[];
114 const addresses = getIsAddressActive(address) ? acc.activeAddresses : acc.inactiveAddresses;
115 addresses.push(address);
119 { activeAddresses: [], inactiveAddresses: [] }
122 // start checking active addresses
123 const { selfActiveAttendee, selfActiveAddress, selfActiveAttendeeIndex } = activeAddresses.reduce<{
124 selfActiveAttendee?: VcalAttendeeProperty;
125 selfActiveAttendeeIndex?: number;
126 selfActiveAddress?: Address;
127 answeredAttendeeFound: boolean;
130 if (acc.answeredAttendeeFound) {
133 const canonicalSelfEmail = ownCanonicalizedEmailsMap[address.Email];
134 const index = canonicalAttendeeEmails.findIndex((email) => email === canonicalSelfEmail);
138 const attendee = attendees[index];
139 const partstat = getAttendeePartstat(attendee);
140 const answeredAttendeeFound = partstat !== ICAL_ATTENDEE_STATUS.NEEDS_ACTION;
141 if (answeredAttendeeFound || !(acc.selfActiveAttendee && acc.selfActiveAddress)) {
143 selfActiveAttendee: attendee,
144 selfActiveAddress: address,
145 selfActiveAttendeeIndex: index,
146 answeredAttendeeFound,
151 { answeredAttendeeFound: false }
153 if (selfActiveAttendee && selfActiveAddress) {
157 selfAttendee: selfActiveAttendee,
158 selfAddress: selfActiveAddress,
159 selfAttendeeIndex: selfActiveAttendeeIndex,
162 // check inactive addresses
163 const { selfInactiveAttendee, selfInactiveAddress, selfInactiveAttendeeIndex } = inactiveAddresses.reduce<{
164 selfInactiveAttendee?: VcalAttendeeProperty;
165 selfInactiveAttendeeIndex?: number;
166 selfInactiveAddress?: Address;
167 answeredAttendeeFound: boolean;
170 if (acc.answeredAttendeeFound) {
173 const canonicalSelfEmail = ownCanonicalizedEmailsMap[address.Email];
174 const index = canonicalAttendeeEmails.findIndex((email) => email === canonicalSelfEmail);
178 const attendee = attendees[index];
179 const partstat = getAttendeePartstat(attendee);
180 const answeredAttendeeFound = partstat !== ICAL_ATTENDEE_STATUS.NEEDS_ACTION;
181 if (answeredAttendeeFound || !(acc.selfInactiveAttendee && acc.selfInactiveAddress)) {
183 selfInactiveAttendee: attendee,
184 selfInactiveAttendeeIndex: index,
185 selfInactiveAddress: address,
186 answeredAttendeeFound,
191 { answeredAttendeeFound: false }
195 isAttendee: !!selfInactiveAttendee,
196 selfAttendee: selfInactiveAttendee,
197 selfAddress: selfInactiveAddress,
198 selfAttendeeIndex: selfInactiveAttendeeIndex,
202 const readCalendarAlarms = (
203 { Notifications, FullDay }: Pick<CalendarEvent, 'Notifications' | 'FullDay'>,
204 calendarSettings: CalendarSettings
207 valarmComponents: fromApiNotifications({
208 notifications: Notifications || null,
212 hasDefaultNotifications: !Notifications,
217 * Read the parts of a calendar event into an internal vcal component.
219 interface ReadCalendarEventArguments {
232 publicKeysMap?: SimpleMap<PublicKeyReference | PublicKeyReference[]>;
233 sharedSessionKey?: SessionKey;
234 calendarSessionKey?: SessionKey;
235 calendarSettings: CalendarSettings;
236 addresses: Address[];
237 encryptingAddressID?: string;
240 export const readCalendarEvent = async ({
244 AttendeesEvents = [],
248 CalendarID: calendarID,
258 }: ReadCalendarEventArguments) => {
259 const decryptedEventsResults = await Promise.all([
260 Promise.all(SharedEvents.map((e) => decryptAndVerifyCalendarEvent(e, publicKeysMap, sharedSessionKey))),
261 Promise.all(CalendarEvents.map((e) => decryptAndVerifyCalendarEvent(e, publicKeysMap, calendarSessionKey))),
262 Promise.all(AttendeesEvents.map((e) => decryptAndVerifyCalendarEvent(e, publicKeysMap, sharedSessionKey))),
264 const [decryptedSharedEvents, decryptedCalendarEvents, decryptedAttendeesEvents] = decryptedEventsResults.map(
265 (decryptedEvents) => decryptedEvents.map(({ data }) => data)
267 const verificationStatusArray = decryptedEventsResults
268 .map((decryptedEvents) => decryptedEvents.map(({ verificationStatus }) => verificationStatus))
270 const verificationStatus = getAggregatedEventVerificationStatus(verificationStatusArray);
272 const vevent = [...decryptedSharedEvents, ...decryptedCalendarEvents].reduce<VcalVeventComponent>((acc, event) => {
276 const parsedComponent = parseWithFoldingRecovery(unwrap(event), { calendarID, eventID });
277 if (!getIsEventComponent(parsedComponent)) {
280 return { ...acc, ...parsedComponent };
281 }, {} as VcalVeventComponent);
283 const { valarmComponents, hasDefaultNotifications } = readCalendarAlarms(
284 { Notifications, FullDay },
288 const veventAttendees = decryptedAttendeesEvents.reduce<VcalAttendeeProperty[]>((acc, event) => {
292 const parsedComponent = parseWithFoldingRecovery(unwrap(event), { calendarID, eventID });
293 if (!getIsEventComponent(parsedComponent)) {
296 return acc.concat(toInternalAttendee(parsedComponent, Attendees));
299 if (valarmComponents.length) {
300 vevent.components = valarmComponents;
303 if (veventAttendees.length) {
304 vevent.attendee = veventAttendees;
308 vevent.color = { value: Color };
311 const selfAddressData = getSelfAddressData({
312 organizer: vevent.organizer,
313 attendees: veventAttendees,
316 const encryptionData = {
322 return { veventComponent: vevent, hasDefaultNotifications, verificationStatus, selfAddressData, encryptionData };