1 import { fromUnixTime } from 'date-fns';
2 import { c } from 'ttag';
4 import { CryptoProxy } from '@proton/crypto';
5 import { withSupportedSequence } from '@proton/shared/lib/calendar/icsSurgery/vevent';
6 import isTruthy from '@proton/utils/isTruthy';
7 import partition from '@proton/utils/partition';
8 import unique from '@proton/utils/unique';
10 import { getEvent, queryEventsIDs } from '../../api/calendars';
11 import { getSilentApi } from '../../api/helpers/customConfig';
12 import { SECOND } from '../../constants';
13 import formatUTC from '../../date-fns-utc/format';
14 import type { WeekStartsOn } from '../../date-fns-utc/interface';
16 formatGMTOffsetAbbreviation,
18 fromUTCDateToLocalFakeUTCDate,
20 } from '../../date/timezone';
21 import { dateLocale } from '../../i18n';
22 import type { Address, Api, Key } from '../../interfaces';
29 } from '../../interfaces/calendar';
30 import { EXPORT_EVENT_ERROR_TYPES } from '../../interfaces/calendar';
31 import type { CalendarEventsIDsQuery } from '../../interfaces/calendar/Api';
32 import type { GetAddressKeys } from '../../interfaces/hooks/GetAddressKeys';
33 import type { GetCalendarKeys } from '../../interfaces/hooks/GetCalendarKeys';
34 import { getIsAutoAddedInvite } from '../apiModels';
35 import { withNormalizedAuthors } from '../author';
36 import { getIsOwnedCalendar } from '../calendar';
37 import { getCalendarEventDecryptionKeys } from '../crypto/keys/helpers';
38 import { readCalendarEvent, readSessionKeys } from '../deserialize';
39 import { getTimezonedFrequencyString } from '../recurrence/getFrequencyString';
40 import { fromRruleString } from '../vcal';
41 import { getDateProperty } from '../vcalConverter';
42 import { withMandatoryPublishFields } from '../veventHelper';
44 export const getHasCalendarEventMatchingSigningKeys = async (event: CalendarEvent, keys: Key[]) => {
45 const allEventSignatures = [...event.SharedEvents, ...event.CalendarEvents, ...event.AttendeesEvents].flatMap(
46 (event) => (event.Signature ? [event.Signature] : [])
49 const allSignaturesKeyInfo = await Promise.all(
50 allEventSignatures.map((armoredSignature) => CryptoProxy.getSignatureInfo({ armoredSignature }))
52 const allSigningKeyIDs = unique(allSignaturesKeyInfo.flatMap(({ signingKeyIDs }) => signingKeyIDs));
53 for (const { PrivateKey: armoredKey } of keys) {
54 const { keyIDs } = await CryptoProxy.getKeyInfo({ armoredKey });
55 const isSigningKey = keyIDs.some((keyID) => allSigningKeyIDs.includes(keyID));
65 export interface GetErrorProps {
67 errorType: EXPORT_EVENT_ERROR_TYPES;
68 weekStartsOn: WeekStartsOn;
72 export const getError = ({ event, errorType, weekStartsOn, defaultTzid }: GetErrorProps): ExportError => {
73 const { StartTime, RRule, FullDay } = event;
74 const startDate = new Date(StartTime * SECOND);
75 const fakeUTCStartDate = fromUTCDateToLocalFakeUTCDate(startDate, !!FullDay, defaultTzid);
76 const startDateString = formatUTC(fakeUTCStartDate, FullDay ? 'P' : 'Pp', { locale: dateLocale });
77 const { offset } = getTimezoneOffset(startDate, defaultTzid);
78 const offsetString = formatGMTOffsetAbbreviation(offset);
79 const timeString = `${startDateString}${FullDay ? '' : ` ${offsetString}`}`;
81 const rruleValueFromString = RRule ? fromRruleString(RRule) : undefined;
82 const utcStartDate = fromUnixTime(StartTime);
83 const dtstart = getDateProperty(fromUTCDate(utcStartDate));
85 if (rruleValueFromString) {
86 const rruleString = getTimezonedFrequencyString({ value: rruleValueFromString }, dtstart, {
87 currentTzid: defaultTzid,
92 return [c('Error when exporting event from calendar').t`Event from ${timeString}, ${rruleString}`, errorType];
95 return [c('Error when exporting event from calendar').t`Event @ ${timeString}`, errorType];
98 const getDecryptionErrorType = async (event: CalendarEvent, keys: Key[]) => {
100 const HasMatchingKeys = await getHasCalendarEventMatchingSigningKeys(event, keys);
101 if (HasMatchingKeys) {
102 return EXPORT_EVENT_ERROR_TYPES.PASSWORD_RESET;
104 return EXPORT_EVENT_ERROR_TYPES.DECRYPTION_ERROR;
106 return EXPORT_EVENT_ERROR_TYPES.DECRYPTION_ERROR;
110 const decryptEvent = async ({
120 event: CalendarEvent;
121 calendarEmail: string;
122 calendarSettings: CalendarSettings;
123 addresses: Address[];
124 getAddressKeys: GetAddressKeys;
125 getCalendarKeys: GetCalendarKeys;
126 weekStartsOn: WeekStartsOn;
129 const defaultParams = { event, defaultTzid, weekStartsOn };
130 const eventDecryptionKeys = await getCalendarEventDecryptionKeys({
131 calendarEvent: event,
137 const [sharedSessionKey, calendarSessionKey] = await readSessionKeys({
138 calendarEvent: event,
139 privateKeys: eventDecryptionKeys,
142 const { CalendarID, ID, SharedEvents, CalendarEvents, AttendeesEvents, Attendees, Notifications, FullDay } =
145 const { veventComponent } = await readCalendarEvent({
147 SharedEvents: withNormalizedAuthors(SharedEvents),
148 CalendarEvents: withNormalizedAuthors(CalendarEvents),
149 AttendeesEvents: withNormalizedAuthors(AttendeesEvents),
155 // do not export color
162 encryptingAddressID: getIsAutoAddedInvite(event) ? event.AddressID : undefined,
165 return withSupportedSequence(withMandatoryPublishFields(veventComponent, calendarEmail));
166 } catch (error: any) {
167 const inactiveKeys = addresses.flatMap(({ Keys }) => Keys.filter(({ Active }) => !Active));
170 errorType: await getDecryptionErrorType(event, inactiveKeys),
175 const tryDecryptEvent = async ({
185 calendar: VisualCalendar;
186 event: CalendarEvent;
187 calendarSettings: CalendarSettings;
188 addresses: Address[];
189 getAddressKeys: GetAddressKeys;
190 getCalendarKeys: GetCalendarKeys;
191 weekStartsOn: WeekStartsOn;
194 // ignore auto-added invites in shared calendars (they can't be decrypted and we don't display them in the UI)
195 if (!getIsOwnedCalendar(calendar) && getIsAutoAddedInvite(event)) {
198 return decryptEvent({
200 calendarEmail: calendar.Email,
210 const fetchAndTryDecryptEvent = async ({
223 calendar: VisualCalendar;
224 calendarSettings: CalendarSettings;
225 addresses: Address[];
226 getAddressKeys: GetAddressKeys;
227 getCalendarKeys: GetCalendarKeys;
228 weekStartsOn: WeekStartsOn;
231 const { Event: event } = await getSilentApi(api)<{ Event: CalendarEvent }>(getEvent(calendar.ID, eventID));
232 return tryDecryptEvent({
244 interface ProcessData {
245 calendar: VisualCalendar;
246 addresses: Address[];
247 getAddressKeys: GetAddressKeys;
248 getCalendarKeys: GetCalendarKeys;
252 calendarEventIDs: string[],
253 veventComponents: VcalVeventComponent[],
254 exportErrors: ExportError[]
256 totalToProcess: number;
257 calendarSettings: CalendarSettings;
258 weekStartsOn: WeekStartsOn;
262 export const processInBatches = async ({
274 }: ProcessData): Promise<[VcalVeventComponent[], ExportError[], number]> => {
275 const PAGE_SIZE = 100;
276 const batchesLength = Math.ceil(totalToProcess / PAGE_SIZE);
277 const processed: VcalVeventComponent[] = [];
278 const errors: ExportError[] = [];
279 const promises: Promise<void>[] = [];
280 let totalEventsFetched = 0;
284 for (let i = 0; i < batchesLength; i++) {
285 if (signal.aborted) {
286 return [[], [], totalToProcess];
289 const params: CalendarEventsIDsQuery = {
294 const IDs = (await api<{ IDs: string[] }>(queryEventsIDs(calendar.ID, params))).IDs;
296 if (signal.aborted) {
297 return [[], [], totalToProcess];
300 onProgress(IDs, [], []);
302 lastId = IDs[IDs.length - 1];
303 totalEventsFetched += IDs.length;
305 const promise = Promise.all(
307 fetchAndTryDecryptEvent({
320 .then((veventsOrErrors) => {
321 const veventsOrErrorsNonNull: (VcalVeventComponent | ExportError)[] = veventsOrErrors.filter(isTruthy);
322 const [veventComponents, exportErrors] = partition<VcalVeventComponent, ExportError>(
323 veventsOrErrorsNonNull,
324 (item): item is VcalVeventComponent => !Array.isArray(item)
327 processed.push(...veventComponents);
328 errors.push(...exportErrors);
329 onProgress([], veventComponents, exportErrors);
332 const exportErrors: ExportError[] = IDs.map(() => [
334 EXPORT_EVENT_ERROR_TYPES.DECRYPTION_ERROR,
336 errors.push(...exportErrors);
337 onProgress([], [], exportErrors);
340 promises.push(promise);
343 await Promise.all(promises);
345 return [processed, errors, totalEventsFetched];