Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/ProtonMail/WebClients.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMargeBot <321-margebot@users.noreply.gitlab.protontech.ch>2022-10-28 10:21:09 +0300
committerEduardo Conde Pena <econdepe@proton.ch>2022-10-31 04:47:39 +0300
commit794ce2d62809b832179a76439781b3b478ad029c (patch)
tree8acfe124572b9f8f0c5e99cc01cce156498d4eee
parent31115406edaadef828f50d1a2f760faa82fb7ad7 (diff)
Merge branch 'ics-surgery-for-dtstamp' into 'main'proton-calendar@5.0.5.4release/proton-calendar@5.0.5.0
ICS surgery for DTSTAMP See merge request web/clients!3666 Changelog:
-rw-r--r--applications/mail/src/app/helpers/calendar/invite.test.ts60
-rw-r--r--applications/mail/src/app/helpers/calendar/invite.ts26
-rw-r--r--packages/shared/lib/calendar/icsSurgery/vevent.ts51
-rw-r--r--packages/shared/lib/calendar/import/import.ts6
-rw-r--r--packages/shared/lib/interfaces/calendar/VcalModel.ts8
-rw-r--r--packages/shared/test/calendar/icsSurgery/vevent.spec.ts98
-rw-r--r--packages/shared/test/calendar/import.spec.ts50
7 files changed, 276 insertions, 23 deletions
diff --git a/applications/mail/src/app/helpers/calendar/invite.test.ts b/applications/mail/src/app/helpers/calendar/invite.test.ts
index 60bc3bcde1..84c2ebe71a 100644
--- a/applications/mail/src/app/helpers/calendar/invite.test.ts
+++ b/applications/mail/src/app/helpers/calendar/invite.test.ts
@@ -326,6 +326,66 @@ END:VCALENDAR`;
});
});
+ test('should generate a DTSTAMP from the message if no DTSTAMP was present', async () => {
+ const invitation = `BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.13.6//EN
+BEGIN:VEVENT
+UID:test-event
+DTSTART;TZID=/mozilla.org/20050126_1/Europe/Brussels:20021231T203000
+DTEND;TZID=/mozilla.org/20050126_1/Europe/Brussels:20030101T003000
+LOCATION:1CP Conference Room 4350
+ATTENDEE;CUTYPE=INDIVIDUAL;EMAIL="testme@pm.me";PARTSTAT=NEED
+ S-ACTION;RSVP=TRUE:mailto:testme@pm.me
+ATTENDEE;CN="testKrt";CUTYPE=INDIVIDUAL;EMAIL="aGmailOne@gmail.co
+ m";PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:aGmailOne@gmail.com
+TRANSP:OPAQUE
+ORGANIZER;CN="testKrt":mailto:aGmailOne@gmail.com
+BEGIN:VALARM
+TRIGGER:-PT15H
+ACTION:DISPLAY
+END:VALARM
+BEGIN:VALARM
+TRIGGER:-PT1W2D
+ACTION:EMAIL
+END:VALARM
+END:VEVENT
+END:VCALENDAR`;
+ const parsedInvitation = parseVcalendar(invitation) as VcalVcalendar;
+ const message = { Time: Date.UTC(2022, 9, 10, 10, 0, 0) / 1000 } as Message;
+ expect(
+ await getSupportedEventInvitation({
+ vcalComponent: parsedInvitation,
+ message,
+ icsBinaryString: invitation,
+ icsFileName: 'test.ics',
+ primaryTimezone: 'America/Sao_Paulo',
+ })
+ ).toEqual({
+ method: 'PUBLISH',
+ vevent: expect.objectContaining({
+ component: 'vevent',
+ uid: { value: 'sha1-uid-1d92b0aa7fed011b07b53161798dfeb45cf4e186-original-uid-test-event' },
+ dtstamp: {
+ value: { year: 2022, month: 10, day: 10, hours: 10, minutes: 0, seconds: 0, isUTC: true },
+ },
+ dtstart: {
+ value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
+ parameters: { tzid: 'Europe/Brussels' },
+ },
+ dtend: {
+ value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
+ parameters: { tzid: 'Europe/Brussels' },
+ },
+ }),
+ originalVcalInvitation: parsedInvitation,
+ originalUniqueIdentifier: 'test-event',
+ hasMultipleVevents: false,
+ fileName: 'test.ics',
+ });
+ });
+
test('should not throw without version, untrimmed calscale and duration', async () => {
const invitation = `BEGIN:VCALENDAR
CALSCALE: Gregorian
diff --git a/applications/mail/src/app/helpers/calendar/invite.ts b/applications/mail/src/app/helpers/calendar/invite.ts
index e327d94767..dbabb7004d 100644
--- a/applications/mail/src/app/helpers/calendar/invite.ts
+++ b/applications/mail/src/app/helpers/calendar/invite.ts
@@ -12,13 +12,12 @@ import {
cloneEventInvitationErrorWithConfig,
} from '@proton/shared/lib/calendar/icsSurgery/EventInvitationError';
import { getSupportedCalscale } from '@proton/shared/lib/calendar/icsSurgery/vcal';
-import { getSupportedEvent } from '@proton/shared/lib/calendar/icsSurgery/vevent';
+import { getSupportedEvent, withSupportedDtstamp } 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,
@@ -42,7 +41,7 @@ import {
} 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 { getSupportedTimezone } from '@proton/shared/lib/date/timezone';
import { getIsAddressActive, getIsAddressDisabled } from '@proton/shared/lib/helpers/address';
import { canonicalizeEmailByGuess, canonicalizeInternalEmail } from '@proton/shared/lib/helpers/email';
import { splitExtension } from '@proton/shared/lib/helpers/file';
@@ -190,20 +189,6 @@ export const withOutsideUIDAndSequence = (vevent: VcalVeventComponent, vcal: Non
return result;
};
-const withMessageDtstamp = <T>(
- properties: VcalVeventComponent & T,
- { Time }: MessageWithOptionalBody
-): VcalVeventComponent & T => {
- if (properties.dtstamp) {
- return properties;
- }
- // use the received time of the mail as dtstamp
- return {
- ...properties,
- dtstamp: dateTimeToProperty(fromUTCDate(new Date(Time * SECOND)), true),
- };
-};
-
export const getHasMultipleVevents = (vcal?: VcalVcalendar) => {
const numberOfVevents = vcal?.components?.filter(unary(getIsEventComponent)).length || 0;
return numberOfVevents > 1;
@@ -624,7 +609,10 @@ export const getSupportedEventInvitation = async ({
if (!getHasDtStart(vevent)) {
throw new EventInvitationError(EVENT_INVITATION_ERROR_TYPE.INVITATION_INVALID, { method: supportedMethod });
}
- const completeVevent = withOutsideUIDAndSequence(vevent, vcalComponent);
+ const completeVevent = withOutsideUIDAndSequence(
+ withSupportedDtstamp(vevent, message.Time * SECOND),
+ vcalComponent
+ );
const hasMultipleVevents = getHasMultipleVevents(vcalComponent);
const isImport = supportedMethod === ICAL_METHOD.PUBLISH;
// To filter potentially equivalent invitation ics's, we have to generate a reliable
@@ -655,7 +643,7 @@ export const getSupportedEventInvitation = async ({
try {
const supportedEvent = getSupportedEvent({
method: supportedMethod,
- vcalVeventComponent: withMessageDtstamp(completeVevent, message),
+ vcalVeventComponent: completeVevent,
hasXWrTimezone,
calendarTzid,
guessTzid,
diff --git a/packages/shared/lib/calendar/icsSurgery/vevent.ts b/packages/shared/lib/calendar/icsSurgery/vevent.ts
index 39e557310c..28e84a2f41 100644
--- a/packages/shared/lib/calendar/icsSurgery/vevent.ts
+++ b/packages/shared/lib/calendar/icsSurgery/vevent.ts
@@ -3,10 +3,13 @@ import { addDays, fromUnixTime } from 'date-fns';
import truncate from '@proton/utils/truncate';
import unique from '@proton/utils/unique';
+import { RequireOnly } from '../../../lib/interfaces';
import { DAY } from '../../constants';
import { convertUTCDateTimeToZone, fromUTCDate, getSupportedTimezone } from '../../date/timezone';
import {
+ IcalJSDateOrDateTimeProperty,
VcalDateOrDateTimeProperty,
+ VcalDateTimeValue,
VcalDurationValue,
VcalFloatingDateTimeProperty,
VcalVeventComponent,
@@ -18,6 +21,7 @@ import { getIsDateOutOfBounds, getIsWellFormedDateOrDateTime, getSupportedUID }
import { getHasConsistentRrule, getHasOccurrences, getSupportedRrule } from '../rrule';
import { durationToMilliseconds } from '../vcal';
import {
+ dateTimeToProperty,
dateToProperty,
getDateTimeProperty,
getDateTimePropertyInDifferentTimezone,
@@ -62,6 +66,53 @@ export const getDtendPropertyFromDuration = (
return getDateTimeProperty(convertUTCDateTimeToZone(end, tzid!), tzid!);
};
+export const getSupportedDtstamp = (dtstamp: IcalJSDateOrDateTimeProperty | undefined, timestamp: number) => {
+ // 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
+ const value = dtstamp?.value;
+ const tzid = dtstamp?.parameters?.tzid;
+
+ if (!value) {
+ return dateTimeToProperty(fromUTCDate(new Date(timestamp)), true);
+ }
+
+ if (tzid) {
+ const supportedTzid = getSupportedTimezone(tzid);
+ if (!supportedTzid) {
+ // generate a new DTSTAMP
+ return dateTimeToProperty(fromUTCDate(new Date(timestamp)), true);
+ }
+ // we try to guess what the external provider meant
+ const guessedProperty = {
+ value: {
+ year: value.year,
+ month: value.month,
+ day: value.day,
+ hours: (value as VcalDateTimeValue)?.hours || 0,
+ minutes: (value as VcalDateTimeValue)?.minutes || 0,
+ seconds: (value as VcalDateTimeValue)?.seconds || 0,
+ isUTC: (value as VcalDateTimeValue)?.isUTC === true,
+ },
+ parameters: {
+ tzid: supportedTzid,
+ },
+ };
+
+ return dateTimeToProperty(fromUTCDate(propertyToUTCDate(guessedProperty)), true);
+ }
+
+ return dateTimeToProperty(fromUTCDate(propertyToUTCDate(dtstamp as VcalDateOrDateTimeProperty)), true);
+};
+
+export const withSupportedDtstamp = <T>(
+ properties: RequireOnly<VcalVeventComponent, 'uid' | 'component' | 'dtstart'> & T,
+ timestamp: number
+): VcalVeventComponent & T => {
+ return {
+ ...properties,
+ dtstamp: getSupportedDtstamp(properties.dtstamp, timestamp),
+ };
+};
+
export const getSupportedDateOrDateTimeProperty = ({
property,
component,
diff --git a/packages/shared/lib/calendar/import/import.ts b/packages/shared/lib/calendar/import/import.ts
index 47fd5eda03..5b756860a2 100644
--- a/packages/shared/lib/calendar/import/import.ts
+++ b/packages/shared/lib/calendar/import/import.ts
@@ -1,5 +1,6 @@
import { c } from 'ttag';
+import { serverTime } from '@proton/crypto';
import isTruthy from '@proton/utils/isTruthy';
import truncate from '@proton/utils/truncate';
import unique from '@proton/utils/unique';
@@ -23,7 +24,7 @@ import getComponentFromCalendarEvent from '../getComponentFromCalendarEvent';
import { generateVeventHashUID, getOriginalUID } from '../helper';
import { IMPORT_EVENT_ERROR_TYPE, ImportEventError } from '../icsSurgery/ImportEventError';
import { getSupportedCalscale } from '../icsSurgery/vcal';
-import { getLinkedDateTimeProperty, getSupportedEvent } from '../icsSurgery/vevent';
+import { getLinkedDateTimeProperty, getSupportedEvent, withSupportedDtstamp } from '../icsSurgery/vevent';
import { parseWithErrors, serialize } from '../vcal';
import {
getHasDtStart,
@@ -38,7 +39,6 @@ import {
getIsTodoComponent,
getPropertyTzid,
} from '../vcalHelper';
-import { withDtstamp } from '../veventHelper';
import { ImportFileError } from './ImportFileError';
const getParsedComponentHasError = (component: VcalCalendarComponentOrError): component is { error: Error } => {
@@ -167,7 +167,7 @@ export const extractSupportedEvent = async ({
if (!getHasDtStart(vcalComponent)) {
throw new ImportEventError(IMPORT_EVENT_ERROR_TYPE.DTSTART_MISSING, 'vevent', componentId);
}
- const validVevent = withDtstamp(vcalComponent);
+ const validVevent = withSupportedDtstamp(vcalComponent, +serverTime());
const generateHashUid = !validVevent.uid?.value || isInvitation;
if (generateHashUid) {
validVevent.uid = { value: await generateVeventHashUID(serialize(vcalComponent), vcalComponent?.uid?.value) };
diff --git a/packages/shared/lib/interfaces/calendar/VcalModel.ts b/packages/shared/lib/interfaces/calendar/VcalModel.ts
index b7d636fcb1..eaec844f7d 100644
--- a/packages/shared/lib/interfaces/calendar/VcalModel.ts
+++ b/packages/shared/lib/interfaces/calendar/VcalModel.ts
@@ -57,6 +57,14 @@ export interface VcalFloatingDateTimeProperty {
value: VcalDateTimeValue;
}
+export interface IcalJSDateOrDateTimeProperty {
+ parameters?: {
+ type?: 'date' | 'date-time';
+ tzid?: string;
+ };
+ value: VcalDateValue | VcalDateTimeValue;
+}
+
export type VcalDateOrDateTimeProperty = VcalDateProperty | VcalDateTimeProperty;
export type VcalRruleFreqValue = 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY' | undefined | string;
diff --git a/packages/shared/test/calendar/icsSurgery/vevent.spec.ts b/packages/shared/test/calendar/icsSurgery/vevent.spec.ts
index 7ce17e1068..083329c0cf 100644
--- a/packages/shared/test/calendar/icsSurgery/vevent.spec.ts
+++ b/packages/shared/test/calendar/icsSurgery/vevent.spec.ts
@@ -1,4 +1,100 @@
-import { getDtendPropertyFromDuration } from '../../../lib/calendar/icsSurgery/vevent';
+import { getDtendPropertyFromDuration, getSupportedDtstamp } from '../../../lib/calendar/icsSurgery/vevent';
+
+describe('getSupportedDtstamp()', () => {
+ it('leaves untouched a proper DTSTAMP', () => {
+ expect(
+ getSupportedDtstamp(
+ {
+ value: { year: 2020, month: 1, day: 31, hours: 15, minutes: 11, seconds: 11, isUTC: true },
+ },
+ 1666017619812
+ )
+ ).toEqual({ value: { year: 2020, month: 1, day: 31, hours: 15, minutes: 11, seconds: 11, isUTC: true } });
+ });
+
+ it('leaves untouched a DTSTAMP with Zulu marker and time zone', () => {
+ expect(
+ getSupportedDtstamp(
+ {
+ value: { year: 2020, month: 1, day: 31, hours: 15, minutes: 11, seconds: 11, isUTC: true },
+ parameters: { tzid: 'Asia/Seoul' },
+ },
+ 1666017619812
+ )
+ ).toEqual({ value: { year: 2020, month: 1, day: 31, hours: 15, minutes: 11, seconds: 11, isUTC: true } });
+ });
+
+ it('converts a time-zoned DTSTAMP', () => {
+ expect(
+ getSupportedDtstamp(
+ {
+ value: { year: 2020, month: 1, day: 31, hours: 15, minutes: 11, seconds: 11, isUTC: false },
+ parameters: { tzid: 'America/Montevideo' },
+ },
+ 1666017619812
+ )
+ ).toEqual({ value: { year: 2020, month: 1, day: 31, hours: 18, minutes: 11, seconds: 11, isUTC: true } });
+ });
+
+ it('converts a time-zoned DTSTAMP with a TZID that needs conversion', () => {
+ expect(
+ getSupportedDtstamp(
+ {
+ value: { year: 2020, month: 1, day: 31, hours: 15, minutes: 11, seconds: 11, isUTC: false },
+ parameters: { tzid: '/mozilla.org/20050126_1/Asia/Pyongyang' },
+ },
+ 1666017619812
+ )
+ ).toEqual({ value: { year: 2020, month: 1, day: 31, hours: 6, minutes: 11, seconds: 11, isUTC: true } });
+ });
+
+ it('converts a floating DTSTAMP', () => {
+ expect(
+ getSupportedDtstamp(
+ {
+ value: { year: 2020, month: 1, day: 31, hours: 15, minutes: 11, seconds: 11, isUTC: false },
+ },
+ 1666017619812
+ )
+ ).toEqual({ value: { year: 2020, month: 1, day: 31, hours: 15, minutes: 11, seconds: 11, isUTC: true } });
+ });
+
+ it('converts an all-day DTSTAMP', () => {
+ expect(
+ getSupportedDtstamp(
+ {
+ value: { year: 2020, month: 1, day: 31 },
+ parameters: { type: 'date' },
+ },
+ 1666017619812
+ )
+ ).toEqual({ value: { year: 2020, month: 1, day: 31, hours: 0, minutes: 0, seconds: 0, isUTC: true } });
+ });
+
+ it('converts an all-day DTSTAMP with TZID', () => {
+ expect(
+ getSupportedDtstamp(
+ {
+ value: { year: 2020, month: 1, day: 31 },
+ parameters: { type: 'date', tzid: 'America/Montevideo' },
+ },
+ 1666017619812
+ )
+ ).toEqual({ value: { year: 2020, month: 1, day: 31, hours: 3, minutes: 0, seconds: 0, isUTC: true } });
+ });
+
+ it('defaults to the given timestamp if a time zone is present but not supported', () => {
+ expect(
+ getSupportedDtstamp(
+ {
+ value: { year: 2020, month: 1, day: 31 },
+ parameters: { type: 'date', tzid: 'Europe/My_home' },
+ },
+ 1666017619812
+ )
+ ).toEqual({ value: { year: 2022, month: 10, day: 17, hours: 14, minutes: 40, seconds: 19, isUTC: true } });
+ });
+});
describe('getDtendPropertyFromDuration()', () => {
it('returns the appropriate dtend when given a duration and start', () => {
diff --git a/packages/shared/test/calendar/import.spec.ts b/packages/shared/test/calendar/import.spec.ts
index 823d75a9c2..50b1721625 100644
--- a/packages/shared/test/calendar/import.spec.ts
+++ b/packages/shared/test/calendar/import.spec.ts
@@ -1007,6 +1007,56 @@ END:VEVENT`;
});
});
+ it('should fix bad DTSTAMPs', async () => {
+ const vevent = `BEGIN:VEVENT
+DTSTART;TZID=America/New_York:20221012T171500
+DTEND;TZID=America/New_York:20221012T182500
+DTSTAMP;TZID=America/New_York:20221007T151646
+UID:11353R6@voltigeursbourget.com
+SEQUENCE:0
+END:VEVENT`;
+ const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
+ const supportedEvent = await extractSupportedEvent({
+ method: ICAL_METHOD.PUBLISH,
+ vcalComponent: event,
+ hasXWrTimezone: false,
+ guessTzid: 'Europe/Zurich',
+ });
+ expect(supportedEvent).toEqual({
+ component: 'vevent',
+ uid: { value: '11353R6@voltigeursbourget.com' },
+ dtstamp: {
+ value: { year: 2022, month: 10, day: 7, hours: 19, minutes: 16, seconds: 46, isUTC: true },
+ },
+ dtstart: {
+ value: { year: 2022, month: 10, day: 12, hours: 17, minutes: 15, seconds: 0, isUTC: false },
+ parameters: { tzid: 'America/New_York' },
+ },
+ dtend: {
+ value: { year: 2022, month: 10, day: 12, hours: 18, minutes: 25, seconds: 0, isUTC: false },
+ parameters: { tzid: 'America/New_York' },
+ },
+ sequence: { value: 0 },
+ });
+ });
+
+ it('should generate DTSTAMP if not present', async () => {
+ const vevent = `BEGIN:VEVENT
+DTSTART;TZID=America/New_York:20221012T171500
+DTEND;TZID=America/New_York:20221012T182500
+UID:11353R6@voltigeursbourget.com
+SEQUENCE:0
+END:VEVENT`;
+ const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
+ const supportedEvent = await extractSupportedEvent({
+ method: ICAL_METHOD.PUBLISH,
+ vcalComponent: event,
+ hasXWrTimezone: false,
+ guessTzid: 'Europe/Zurich',
+ });
+ expect(Object.keys(supportedEvent)).toContain('dtstamp');
+ });
+
it('should not import alarms for invitations', async () => {
const vevent = `
BEGIN:VEVENT