1 import { c } from 'ttag';
3 import { CryptoProxy } from '@proton/crypto';
4 import { arrayToHexString, binaryStringToArray } from '@proton/crypto/lib/utils';
6 import { API_CODES } from '../constants';
7 import { encodeBase64URL, uint8ArrayToString } from '../helpers/encoding';
9 SyncMultipleApiResponses,
10 SyncMultipleApiSuccessResponses,
11 VcalDateOrDateTimeProperty,
13 } from '../interfaces/calendar';
14 import { ACTION_VIEWS, MAXIMUM_DATE_UTC, MAX_CHARS_API, MINIMUM_DATE_UTC } from './constants';
15 import { propertyToUTCDate } from './vcalConverter';
16 import { getIsPropertyAllDay } from './vcalHelper';
18 export const HASH_UID_PREFIX = 'sha1-uid-';
19 export const ORIGINAL_UID_PREFIX = 'original-uid-';
21 export const getIsSuccessSyncApiResponse = (
22 response: SyncMultipleApiResponses
23 ): response is SyncMultipleApiSuccessResponses => {
25 Response: { Code, Event },
27 return Code === API_CODES.SINGLE_SUCCESS && !!Event;
31 * Generates a calendar UID of the form 'RandomBase64String@proton.me'
32 * RandomBase64String has a length of 28 characters
34 export const generateProtonCalendarUID = () => {
35 // by convention we generate 21 bytes of random data
36 const randomBytes = crypto.getRandomValues(new Uint8Array(21));
37 const base64String = encodeBase64URL(uint8ArrayToString(randomBytes));
38 // and we encode them in base 64
39 return `${base64String}@proton.me`;
42 export const generateVeventHashUID = async (binaryString: string, uid = '', legacyFormat = false) => {
43 const hash = arrayToHexString(
44 await CryptoProxy.computeHash({ algorithm: 'unsafeSHA1', data: binaryStringToArray(binaryString) })
46 const hashUid = `${HASH_UID_PREFIX}${hash}`;
51 const uidLength = uid.length;
52 const availableLength = MAX_CHARS_API.UID - ORIGINAL_UID_PREFIX.length - hashUid.length - join.length;
53 const croppedUID = uid.substring(uidLength - availableLength, uidLength);
55 ? `${hashUid}${join}${ORIGINAL_UID_PREFIX}${croppedUID}`
56 : `${ORIGINAL_UID_PREFIX}${croppedUID}${join}${hashUid}`;
59 export const getOriginalUID = (uid = '') => {
63 const regexWithOriginalUid = new RegExp(`^${ORIGINAL_UID_PREFIX}(.+)-${HASH_UID_PREFIX}[abcdef\\d]{40}`);
64 const regexWithOriginalUidLegacyFormat = new RegExp(
65 `^${HASH_UID_PREFIX}[abcdef\\d]{40}-${ORIGINAL_UID_PREFIX}(.+)`
67 const [, match] = uid.match(regexWithOriginalUid) || uid.match(regexWithOriginalUidLegacyFormat) || [];
71 const regexWithoutOriginalUid = new RegExp(`^${HASH_UID_PREFIX}[abcdef\\d]{40}$`);
72 if (regexWithoutOriginalUid.test(uid)) {
78 export const getHasLegacyHashUID = (uid = '') => {
82 return new RegExp(`^${HASH_UID_PREFIX}[abcdef\\d]{40}-${ORIGINAL_UID_PREFIX}`).test(uid);
85 export const getSupportedUID = (uid: string) => {
86 const uidLength = uid.length;
87 return uid.substring(uidLength - MAX_CHARS_API.UID, uidLength);
90 const getIsWellFormedDateTime = (property: VcalDateTimeProperty) => {
91 return property.value.isUTC || !!property.parameters!.tzid;
94 export const getIsWellFormedDateOrDateTime = (property: VcalDateOrDateTimeProperty) => {
95 return getIsPropertyAllDay(property) || getIsWellFormedDateTime(property);
98 export const getIsDateOutOfBounds = (property: VcalDateOrDateTimeProperty) => {
99 const dateUTC: Date = propertyToUTCDate(property);
100 return +dateUTC < +MINIMUM_DATE_UTC || +dateUTC > +MAXIMUM_DATE_UTC;
104 * Try to guess from the event uid if an event was generated by Proton. In pple there are two possibilities
105 * * Old uids of the form 'proton-calendar-350095ea-4368-26f0-4fc9-60a56015b02e' and derived ones from "this and future" editions
106 * * New uids of the form 'RandomBase64String@proton.me' and derived ones from "this and future" editions
108 export const getIsProtonUID = (uid = '') => {
109 return uid.endsWith('@proton.me') || uid.startsWith('proton-calendar-');
113 * Try to naively guess the domain of a provider from the uid.
114 * This helper only works when the uid is of the form `${randomString}@${domain}`
116 export const getNaiveDomainFromUID = (uid = '') => {
117 const parts = uid.split('@');
118 const numberOfParts = parts.length;
120 if (numberOfParts <= 1) {
124 return parts[numberOfParts - 1];
127 export const getDisplayTitle = (title = '') => {
128 return title.trim() || c('Event title').t`(no title)`;
132 * Check whether an object has more keys than a set of keys.
134 export const hasMoreThan = (set: Set<string>, properties: { [key: string]: any } = {}) => {
135 return Object.keys(properties).some((key) => !set.has(key));
138 export const wrap = (res: string, prodId?: string) => {
139 // Wrap in CRLF according to the rfc
141 ? `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:${prodId}\r\n${res}\r\nEND:VCALENDAR`
142 : `BEGIN:VCALENDAR\r\nVERSION:2.0\r\n${res}\r\nEND:VCALENDAR`;
145 export const unwrap = (res: string) => {
146 if (res.slice(0, 15) !== 'BEGIN:VCALENDAR') {
149 const startIdx = res.indexOf('BEGIN:', 1);
150 if (startIdx === -1 || startIdx === 0) {
153 const endIdx = res.lastIndexOf('END:VCALENDAR');
154 return res.slice(startIdx, endIdx).trim();
157 export const getLinkToCalendarEvent = ({
164 recurrenceID?: number;
166 const params = new URLSearchParams();
167 params.set('Action', ACTION_VIEWS.VIEW);
168 params.set('EventID', eventID);
169 params.set('CalendarID', calendarID);
171 params.set('RecurrenceID', `${recurrenceID}`);
174 return `/event?${params.toString()}`;
177 export const naiveGetIsDecryptionError = (error: any) => {
178 // We sometimes need to detect if an error produced while reading an event is due to a failed decryption.
179 // We don't have a great way of doing this as the error comes from openpgp
180 const errorMessage = error?.message || '';
182 return errorMessage.toLowerCase().includes('decrypt');