fix: Disable ICS file generation when destination calendar is Outlook (#16247)

* Set the organizer in the organizer field

* Create generateIcsFile

* Add role enum and only disable for organizers

* Move emails to generateIcsFile

* Add new ics func to new emails

* Fix tests

* Type fix

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
Joe Au-YeungandCarina Wollendorfer authored and GitHub committed 2024-08-22 09:49:53 +00:00
1 parent 342f935aba
commit 399a23a52f
17 files changed
+191 -169

No files matched your search

+16 -4
View File
@@ -81,15 +81,24 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers<R> {
toHaveEmail(expectedEmail: ExpectedEmail, to: string): R;
toHaveEmail(expectedEmail: ExpectedEmail, to: string, subject?: string): R;
}
}
}
expect.extend({
toHaveEmail(emails: Fixtures["emails"], expectedEmail: ExpectedEmail, to: string) {
toHaveEmail(emails: Fixtures["emails"], expectedEmail: ExpectedEmail, to: string, subject?: string) {
const { isNot } = this;
const testEmail = emails.get().find((email) => email.to.includes(to));
const testEmail = emails.get().find((email) => {
const filterConditions = [email.to.includes(to)];
if (subject) {
filterConditions.push(email.subject === subject);
}
return filterConditions.every((condition) => condition);
});
const emailsToLog = emails
.get()
.map((email) => ({ to: email.to, html: email.html, ics: email.icalEvent }));
@@ -742,10 +751,12 @@ export function expectSuccessfulBookingRescheduledEmails({
export function expectAwaitingPaymentEmails({
emails,
booker,
subject,
}: {
emails: Fixtures["emails"];
organizer: { email: string; name: string };
booker: { email: string; name: string };
subject?: string;
}) {
expect(emails).toHaveEmail(
{
@@ -753,7 +764,8 @@ export function expectAwaitingPaymentEmails({
to: `${booker.name} <${booker.email}>`,
noIcs: true,
},
`${booker.email}`
`${booker.email}`,
subject
);
}
@@ -277,17 +277,16 @@ export default class Office365CalendarService implements Calendar {
dateTime: dayjs(event.endTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"),
timeZone: event.organizer.timeZone,
},
attendees: [
// Add the calEvent organizer
{
emailAddress: {
address: event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ??
event.organizer.email
: event.organizer.email,
name: event.organizer.name,
},
organizer: {
emailAddress: {
address: event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ??
event.organizer.email
: event.organizer.email,
name: event.organizer.name,
},
},
attendees: [
...event.attendees.map((attendee) => ({
emailAddress: {
address: attendee.email,
+51
View File
@@ -0,0 +1,51 @@
import type { EventStatus } from "ics";
import type { TFunction } from "next-i18next";
import type { CalendarEvent } from "@calcom/types/Calendar";
import generateIcsString from "./generateIcsString";
export enum GenerateIcsRole {
ATTENDEE = "attendee",
ORGANIZER = "organizer",
}
export default function generateIcsFile({
calEvent,
title,
subtitle,
role,
status,
t,
isRequestReschedule,
}: {
calEvent: CalendarEvent;
title: string;
subtitle: string;
role: GenerateIcsRole;
status: EventStatus;
t?: TFunction;
isRequestReschedule?: boolean;
}) {
// O365 deletes emails if the calendar event is selected. Currently no option to disable this on the web
if (
role !== GenerateIcsRole.ATTENDEE &&
calEvent.destinationCalendar &&
calEvent.destinationCalendar[0].integration === "office365_calendar"
)
return null;
return {
filename: "event.ics",
content: generateIcsString({
event: calEvent,
title,
subtitle,
role,
status,
t,
isRequestReschedule,
}),
method: "REQUEST",
};
}
+4 -2
View File
@@ -9,6 +9,8 @@ import { getWhen } from "@calcom/lib/CalEventParser";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { GenerateIcsRole } from "./generateIcsFile";
export enum BookingAction {
Create = "create",
Cancel = "cancel",
@@ -30,7 +32,7 @@ const generateIcsString = ({
title: string;
subtitle: string;
status: EventStatus;
role: "attendee" | "organizer";
role: GenerateIcsRole;
isRequestReschedule?: boolean;
t?: TFunction;
}) => {
@@ -47,7 +49,7 @@ const generateIcsString = ({
const getTextBody = (title: string, subtitle: string): string => {
let body: string;
if (isRequestReschedule && role === "attendee" && t) {
if (isRequestReschedule && role === GenerateIcsRole.ATTENDEE && t) {
body = `
${title}
${getWhen(event, t)}
@@ -1,21 +1,17 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "attendee",
status: "CONFIRMED",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: GenerateIcsRole.ATTENDEE,
status: "CONFIRMED",
}),
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
@@ -1,21 +1,17 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("event_request_cancelled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
status: "CANCELLED",
role: "attendee",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("event_request_cancelled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: GenerateIcsRole.ATTENDEE,
status: "CANCELLED",
}),
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
@@ -1,21 +1,17 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("event_location_changed"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "attendee",
status: "CONFIRMED",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("event_location_changed"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: GenerateIcsRole.ATTENDEE,
status: "CONFIRMED",
}),
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
@@ -1,21 +1,17 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("event_type_has_been_rescheduled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "attendee",
status: "CONFIRMED",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("event_type_has_been_rescheduled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: GenerateIcsRole.ATTENDEE,
status: "CONFIRMED",
}),
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email],
@@ -7,7 +7,7 @@ import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import BaseEmail from "./_base-email";
export default class AttendeeScheduledEmail extends BaseEmail {
@@ -33,19 +33,15 @@ export default class AttendeeScheduledEmail extends BaseEmail {
const clonedCalEvent = cloneDeep(this.calEvent);
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.calEvent.recurringEvent?.count
? this.t("your_event_has_been_scheduled_recurring")
: this.t("your_event_has_been_scheduled"),
role: "attendee",
subtitle: this.t("emailed_you_and_any_other_attendees"),
status: "CONFIRMED",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.calEvent.recurringEvent?.count
? this.t("your_event_has_been_scheduled_recurring")
: this.t("your_event_has_been_scheduled"),
role: GenerateIcsRole.ATTENDEE,
subtitle: this.t("emailed_you_and_any_other_attendees"),
status: "CONFIRMED",
}),
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email],
@@ -3,7 +3,7 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "..";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerScheduledEmail {
@@ -17,19 +17,15 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
const toAddresses = [this.calEvent.attendees[0].email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("request_reschedule_booking"),
subtitle: this.t("request_reschedule_subtitle", {
organizer: this.calEvent.organizer.name,
}),
role: "attendee",
status: "CANCELLED",
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("request_reschedule_booking"),
subtitle: this.t("request_reschedule_subtitle", {
organizer: this.calEvent.organizer.name,
}),
method: "REQUEST",
},
role: GenerateIcsRole.ATTENDEE,
status: "CANCELLED",
}),
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.t("requested_to_reschedule_subject_attendee", {
@@ -1,7 +1,7 @@
import { APP_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
@@ -9,17 +9,13 @@ export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: GenerateIcsRole.ORGANIZER,
status: "CONFIRMED",
}),
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
@@ -1,7 +1,7 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
@@ -9,17 +9,13 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("event_request_cancelled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
status: "CANCELLED",
role: "organizer",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("event_request_cancelled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
status: "CANCELLED",
role: GenerateIcsRole.ORGANIZER,
}),
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.t("event_cancelled_subject", {
@@ -1,7 +1,7 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
@@ -9,17 +9,13 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("event_location_changed"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("event_location_changed"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: GenerateIcsRole.ORGANIZER,
status: "CONFIRMED",
}),
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
@@ -7,7 +7,7 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "..";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRequestedToRescheduleEmail extends OrganizerScheduledEmail {
@@ -20,21 +20,17 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
const toAddresses = [this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("request_reschedule_title_organizer", {
attendee: this.calEvent.attendees[0].name,
}),
subtitle: this.t("request_reschedule_subtitle_organizer", {
attendee: this.calEvent.attendees[0].name,
}),
role: "organizer",
status: "CANCELLED",
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("request_reschedule_title_organizer", {
attendee: this.calEvent.attendees[0].name,
}),
method: "REQUEST",
},
subtitle: this.t("request_reschedule_subtitle_organizer", {
attendee: this.calEvent.attendees[0].name,
}),
role: GenerateIcsRole.ORGANIZER,
status: "CANCELLED",
}),
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.t("rescheduled_event_type_subject", {
@@ -1,7 +1,7 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
@@ -9,17 +9,13 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("event_type_has_been_rescheduled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.t("event_type_has_been_rescheduled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: GenerateIcsRole.ORGANIZER,
status: "CONFIRMED",
}),
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
@@ -8,7 +8,8 @@ import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import generateIcsFile from "../lib/generateIcsFile";
import { GenerateIcsRole } from "../lib/generateIcsFile";
import BaseEmail from "./_base-email";
export default class OrganizerScheduledEmail extends BaseEmail {
@@ -31,19 +32,15 @@ export default class OrganizerScheduledEmail extends BaseEmail {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.calEvent.recurringEvent?.count
? this.t("new_event_scheduled_recurring")
: this.t("new_event_scheduled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
icalEvent: generateIcsFile({
calEvent: this.calEvent,
title: this.calEvent.recurringEvent?.count
? this.t("new_event_scheduled_recurring")
: this.t("new_event_scheduled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: GenerateIcsRole.ORGANIZER,
status: "CONFIRMED",
}),
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
@@ -2654,7 +2654,12 @@ describe("handleNewBooking", () => {
expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails });
expectAwaitingPaymentEmails({ organizer, booker, emails });
expectAwaitingPaymentEmails({
organizer,
booker,
emails,
subject: "complete_your_booking_subject",
});
expectBookingPaymentIntiatedWebhookToHaveBeenFired({
booker,
organizer,