fix: prevent CalDAV duplicate invitations with RFC-compliant SCHEDULE-AGENT (#22434)
* fix: prevent CalDAV duplicate invitations with RFC-compliant SCHEDULE-AGENT Fixes #9485 Adds SCHEDULE-AGENT=CLIENT to ATTENDEE lines in CalDAV createEvent and updateEvent operations per RFC 6638 Section 7.1. This tells CalDAV servers (Fastmail, NextCloud, etc.) that the client handles scheduling, preventing duplicate invitation emails. Implementation includes: - RFC 5545 compliant line folding (max 75 octets per line) - UTF-8 aware byte counting for international names - Proper handling of :mailto: format in ATTENDEE lines - Skips lines that already have SCHEDULE-AGENT parameter * fix: address Cubic review - unfold lines and case-insensitive matching - Add unfoldLines() to handle RFC 5545 folded lines before processing - Use case-insensitive regex (gim flag) per RFC 5545 spec - Simplify regex to properly capture params and value separately * fix: only unfold/refold ATTENDEE lines, preserve other lines - Match ATTENDEE lines including folded continuations - Unfold only the matched ATTENDEE line - Re-fold after adding SCHEDULE-AGENT=CLIENT - Other iCal lines remain untouched (no RFC 5545 violation) * fix: check SCHEDULE-AGENT only in params, not value Only check for existing SCHEDULE-AGENT in the params portion, not the value (which contains email/CN). This prevents false positives when email contains "schedule-agent" substring. * fix: handle quoted colons and exact SCHEDULE-AGENT matching 1. Colon parsing: Match :mailto:/:http:/:urn: patterns, or fallback to finding colon not inside quotes 2. SCHEDULE-AGENT check: Use regex to match exact parameter name (preceded by ; or at start), not substring match * add tests * Update .env.example --------- Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
co-authored by
Anik Dhabal Babu
Anik Dhabal Babu
parent
be13bdf18e
commit
45ea59ceb8
@@ -0,0 +1,400 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { createEvent as createIcsEvent } from "ics";
|
||||
import { createCalendarObject, updateCalendarObject } from "tsdav";
|
||||
|
||||
vi.mock("ics", () => ({
|
||||
createEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("tsdav", () => ({
|
||||
createAccount: vi.fn(),
|
||||
fetchCalendars: vi.fn(),
|
||||
fetchCalendarObjects: vi.fn(),
|
||||
createCalendarObject: vi.fn().mockResolvedValue({ ok: true }),
|
||||
updateCalendarObject: vi.fn().mockResolvedValue({ status: 200 }),
|
||||
deleteCalendarObject: vi.fn(),
|
||||
getBasicAuthHeaders: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/logger", () => ({
|
||||
default: {
|
||||
getSubLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/crypto", () => ({
|
||||
symmetricDecrypt: vi.fn().mockImplementation((text) => {
|
||||
if (typeof text === "object") {
|
||||
return JSON.stringify(text);
|
||||
}
|
||||
return text;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./CalEventParser", () => ({
|
||||
getLocation: vi.fn().mockReturnValue("Test Location"),
|
||||
getRichDescription: vi.fn().mockReturnValue("Test Description"),
|
||||
}));
|
||||
|
||||
import type { CalendarServiceEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import BaseCalendarService from "./CalendarService";
|
||||
|
||||
const createMockEvent = (overrides: Partial<CalendarServiceEvent> = {}): CalendarServiceEvent => ({
|
||||
type: "caldav",
|
||||
title: "Test Event",
|
||||
startTime: "2023-01-01T10:00:00Z",
|
||||
endTime: "2023-01-01T11:00:00Z",
|
||||
organizer: {
|
||||
name: "Test",
|
||||
email: "test@example.com",
|
||||
timeZone: "UTC",
|
||||
language: { translate: ((key: string) => key) as never, locale: "en" },
|
||||
},
|
||||
attendees: [],
|
||||
calendarDescription: "Test Description",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
class TestCalendarService extends BaseCalendarService {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: 1,
|
||||
type: "caldav_calendar",
|
||||
delegationCredentialId: null,
|
||||
user: { email: "test@example.com" },
|
||||
userId: 1,
|
||||
teamId: null,
|
||||
appId: "caldav",
|
||||
invalid: false,
|
||||
key: {
|
||||
username: "test",
|
||||
password: "test",
|
||||
url: "https://caldav.example.com",
|
||||
},
|
||||
},
|
||||
"caldav",
|
||||
"https://caldav.example.com"
|
||||
);
|
||||
}
|
||||
|
||||
async listCalendars() {
|
||||
return [
|
||||
{
|
||||
externalId: "https://caldav.example.com/calendar/",
|
||||
name: "Test Calendar",
|
||||
primary: true,
|
||||
readOnly: false,
|
||||
email: "test@example.com",
|
||||
integrationName: "caldav",
|
||||
credentialId: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async testUpdateEvent(uid: string, event: CalendarServiceEvent, externalCalendarId: string) {
|
||||
return this.updateEvent(uid, event);
|
||||
}
|
||||
}
|
||||
|
||||
describe("CalendarService - SCHEDULE-AGENT injection", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("injectScheduleAgent", () => {
|
||||
it("should inject SCHEDULE-AGENT=CLIENT into ATTENDEE lines", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nMETHOD:PUBLISH\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
expect(unfolded).toContain("ATTENDEE;CN=Guest;SCHEDULE-AGENT=CLIENT:mailto:guest@example.com");
|
||||
expect(unfolded).not.toContain("METHOD:PUBLISH");
|
||||
});
|
||||
|
||||
it("should inject SCHEDULE-AGENT=CLIENT into ORGANIZER lines", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nORGANIZER;CN=Test:mailto:test@example.com\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
expect(unfolded).toContain("ORGANIZER;CN=Test;SCHEDULE-AGENT=CLIENT:mailto:test@example.com");
|
||||
expect(unfolded).toContain("ATTENDEE;CN=Guest;SCHEDULE-AGENT=CLIENT:mailto:guest@example.com");
|
||||
});
|
||||
|
||||
it("should not duplicate SCHEDULE-AGENT if already present", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;SCHEDULE-AGENT=SERVER;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
|
||||
const scheduleAgentCount = (iCalString.match(/SCHEDULE-AGENT/g) || []).length;
|
||||
expect(scheduleAgentCount).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle quoted colons in CN parameter", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN="John: CEO":mailto:john@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
expect(unfolded).toContain('ATTENDEE;CN="John: CEO";SCHEDULE-AGENT=CLIENT:mailto:john@example.com');
|
||||
});
|
||||
|
||||
it("should remove METHOD:PUBLISH per RFC 4791", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nMETHOD:PUBLISH\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
|
||||
expect(iCalString).not.toContain("METHOD:PUBLISH");
|
||||
});
|
||||
|
||||
it("should handle case-insensitive ATTENDEE matching", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nattendee;cn=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("RFC 5545 Line Folding", () => {
|
||||
it("should fold long lines at 75 octets", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const longName = "A".repeat(60);
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN=${longName}:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const lines = iCalString.split("\r\n");
|
||||
|
||||
lines.forEach((line) => {
|
||||
const byteLength = new TextEncoder().encode(line).length;
|
||||
expect(byteLength).toBeLessThanOrEqual(75);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle UTF-8 multi-byte characters correctly", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const unicodeName = "\u4E2D".repeat(30);
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nORGANIZER;CN=${unicodeName}:mailto:test@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const lines = iCalString.split("\r\n");
|
||||
|
||||
lines.forEach((line) => {
|
||||
const byteLength = new TextEncoder().encode(line).length;
|
||||
expect(byteLength).toBeLessThanOrEqual(75);
|
||||
});
|
||||
|
||||
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
|
||||
expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT");
|
||||
});
|
||||
|
||||
it("should handle folded input lines correctly", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN=Very Long Name That Will Be\r\n Folded:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateEvent", () => {
|
||||
it("should inject SCHEDULE-AGENT=CLIENT on update", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
(service as unknown as Record<string, unknown>).getEventsByUID = vi.fn().mockResolvedValue([
|
||||
{
|
||||
uid: "test-uid",
|
||||
url: "https://caldav.example.com/calendar/test.ics",
|
||||
etag: '"etag123"',
|
||||
},
|
||||
]);
|
||||
|
||||
const event = createMockEvent({ uid: "test-uid" });
|
||||
|
||||
await service.testUpdateEvent("test-uid", event, "https://caldav.example.com/calendar/");
|
||||
|
||||
const calledArg = vi.mocked(updateCalendarObject).mock.calls[0][0];
|
||||
const data = calledArg.calendarObject.data;
|
||||
const unfolded = data.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle ATTENDEE with multiple parameters", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
expect(unfolded).toContain("PARTSTAT=NEEDS-ACTION");
|
||||
expect(unfolded).toContain("RSVP=TRUE");
|
||||
expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT");
|
||||
});
|
||||
|
||||
it("should handle ATTENDEE with http URI", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN=Guest:http://example.com/user\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
expect(unfolded).toContain("ATTENDEE;CN=Guest;SCHEDULE-AGENT=CLIENT:http://example.com/user");
|
||||
});
|
||||
|
||||
it("should preserve other iCal properties unchanged", async () => {
|
||||
const service = new TestCalendarService();
|
||||
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Cal.com//NONSGML//EN\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nDTSTART:20230101T100000Z\r\nDTEND:20230101T110000Z\r\nEND:VCALENDAR`;
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: mockIcsOutput,
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await service.createEvent(event, 1);
|
||||
|
||||
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
|
||||
const iCalString = calledArg.iCalString;
|
||||
|
||||
expect(iCalString).toContain("VERSION:2.0");
|
||||
expect(iCalString).toContain("PRODID:-//Cal.com//NONSGML//EN");
|
||||
expect(iCalString).toContain("DTSTART:20230101T100000Z");
|
||||
expect(iCalString).toContain("DTEND:20230101T110000Z");
|
||||
});
|
||||
|
||||
it("should handle empty iCalString gracefully", async () => {
|
||||
const service = new TestCalendarService();
|
||||
vi.mocked(createIcsEvent).mockReturnValue({
|
||||
error: null as unknown as Error,
|
||||
value: "",
|
||||
});
|
||||
|
||||
const event = createMockEvent();
|
||||
|
||||
await expect(service.createEvent(event, 1)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -98,6 +98,80 @@ const getDuration = (start: string, end: string): DurationObject => ({
|
||||
minutes: dayjs(end).diff(dayjs(start), "minute"),
|
||||
});
|
||||
|
||||
/** Folds lines per RFC 5545 (max 75 octets, UTF-8 aware) */
|
||||
const foldLine = (line: string): string => {
|
||||
const encoder = new TextEncoder();
|
||||
if (encoder.encode(line).length <= 75) return line;
|
||||
|
||||
const result: string[] = [];
|
||||
let segment = "";
|
||||
let byteCount = 0;
|
||||
|
||||
for (const char of line) {
|
||||
const charBytes = encoder.encode(char).length;
|
||||
const limit = result.length === 0 ? 75 : 74;
|
||||
|
||||
if (byteCount + charBytes > limit) {
|
||||
result.push(segment);
|
||||
segment = char;
|
||||
byteCount = charBytes;
|
||||
} else {
|
||||
segment += char;
|
||||
byteCount += charBytes;
|
||||
}
|
||||
}
|
||||
if (segment) result.push(segment);
|
||||
return result.join("\r\n ");
|
||||
};
|
||||
|
||||
/** Finds the value separator colon, skipping colons inside quoted strings */
|
||||
const findValueColon = (str: string): number => {
|
||||
let inQuotes = false;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str[i] === '"') inQuotes = !inQuotes;
|
||||
if (str[i] === ":" && !inQuotes) return i;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
/** Checks if SCHEDULE-AGENT parameter exists in the params portion */
|
||||
const hasScheduleAgent = (params: string): boolean =>
|
||||
/;SCHEDULE-AGENT=/i.test(params) || /^SCHEDULE-AGENT=/i.test(params);
|
||||
|
||||
/**
|
||||
* Injects SCHEDULE-AGENT=CLIENT into ORGANIZER and ATTENDEE properties.
|
||||
* Prevents CalDAV servers from sending duplicate invitation emails (RFC 6638).
|
||||
*/
|
||||
const injectScheduleAgent = (iCalString: string): string => {
|
||||
// Remove METHOD:PUBLISH per RFC 4791 Section 4.1
|
||||
let result = iCalString.replace(/METHOD:[^\r\n]+[\r\n]+/g, "");
|
||||
|
||||
// Process ORGANIZER and ATTENDEE lines (handles folded lines)
|
||||
result = result.replace(
|
||||
/^(ORGANIZER|ATTENDEE)((?:[^\r\n]|\r?\n[ \t])*)(\r?\n(?![ \t]))/gim,
|
||||
(match, property, rest, lineEnding) => {
|
||||
const unfolded = rest.replace(/\r?\n[ \t]/g, "");
|
||||
|
||||
// Try to find colon via :mailto:/:http: pattern first
|
||||
const valueMatch = unfolded.match(/:(mailto:|http:|https:|urn:)/i);
|
||||
const colonIndex = valueMatch?.index ?? findValueColon(unfolded);
|
||||
|
||||
if (colonIndex === -1) return match;
|
||||
|
||||
const params = unfolded.slice(0, colonIndex);
|
||||
const value = unfolded.slice(colonIndex);
|
||||
|
||||
if (hasScheduleAgent(params)) {
|
||||
return foldLine(property + unfolded) + lineEnding;
|
||||
}
|
||||
|
||||
return foldLine(property + params + ";SCHEDULE-AGENT=CLIENT" + value) + lineEnding;
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const mapAttendees = (attendees: AttendeeInCalendarEvent[] | TeamMember[]): Attendee[] =>
|
||||
attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
||||
|
||||
@@ -187,8 +261,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||
url: calendar.externalId,
|
||||
},
|
||||
filename: `${uid}.ics`,
|
||||
// according to https://datatracker.ietf.org/doc/html/rfc4791#section-4.1, Calendar object resources contained in calendar collections MUST NOT specify the iCalendar METHOD property.
|
||||
iCalString: iCalString.replace(/METHOD:[^\r\n]+\r\n/g, ""),
|
||||
iCalString: injectScheduleAgent(iCalString),
|
||||
headers: this.headers,
|
||||
})
|
||||
)
|
||||
@@ -255,8 +328,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||
return updateCalendarObject({
|
||||
calendarObject: {
|
||||
url: calendarEvent.url,
|
||||
// ensures compliance with standard iCal string (known as iCal2.0 by some) required by various providers
|
||||
data: iCalString?.replace(/METHOD:[^\r\n]+\r\n/g, ""),
|
||||
data: injectScheduleAgent(iCalString ?? ""),
|
||||
etag: calendarEvent?.etag,
|
||||
},
|
||||
headers: this.headers,
|
||||
|
||||
Reference in New Issue
Block a user