Files
calendar/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts
T
Peer RichelsenGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ec90c1ae24 fix: display full URLs in workflow links (#26292)
- Exclude 'link' toolbar item from Editor for all workflow actions (not just SMS)
- Add replaceCloakedLinksInHtml utility to sanitize HTML and replace cloaked links with visible URLs
- Apply sanitization to email content before sending in all workflow email paths

This ensures recipients can see the actual destination of URLs, helping them identify potentially malicious links.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-30 01:53:04 -03:00

270 lines
7.1 KiB
TypeScript

import { BookingSeatRepository } from "@calcom/features/bookings/repositories/BookingSeatRepository";
import { EmailWorkflowService } from "@calcom/features/ee/workflows/lib/service/EmailWorkflowService";
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
import { WorkflowReminderRepository } from "@calcom/features/ee/workflows/repositories/WorkflowReminderRepository";
import tasker from "@calcom/features/tasker";
import logger from "@calcom/lib/logger";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import type { TimeUnit } from "@calcom/prisma/enums";
import { WorkflowMethods, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { BookingInfo, ScheduleEmailReminderAction, FormSubmissionData } from "../types";
import { sendOrScheduleWorkflowEmails } from "./providers/emailProvider";
import type { WorkflowContextData } from "./reminderScheduler";
import type { VariablesType } from "./templates/customTemplate";
import customTemplate, { transformRoutingFormResponsesToVariableFormat } from "./templates/customTemplate";
import { replaceCloakedLinksInHtml } from "./utils";
const log = logger.getSubLogger({ prefix: ["[emailReminderManager]"] });
export type ScheduleReminderArgs = {
triggerEvent: WorkflowTriggerEvents;
timeSpan: {
time: number | null;
timeUnit: TimeUnit | null;
};
template?: WorkflowTemplates;
sender?: string | null;
workflowStepId?: number;
seatReferenceUid?: string;
} & WorkflowContextData;
type scheduleEmailReminderArgs = ScheduleReminderArgs & {
sendTo: string[];
action: ScheduleEmailReminderAction;
emailSubject?: string;
emailBody?: string;
hideBranding?: boolean;
includeCalendarEvent?: boolean;
verifiedAt: Date | null;
};
type SendEmailReminderParams = {
mailData: {
subject: string;
html: string;
replyTo?: string;
attachments?: {
content: string;
filename: string;
contentType: string;
disposition: string;
}[];
sender?: string | null;
};
sendTo: string[];
triggerEvent: WorkflowTriggerEvents;
scheduledDate?: Date | null;
uid?: string;
workflowStepId?: number;
seatReferenceUid?: string;
};
const sendOrScheduleWorkflowEmailWithReminder = async (params: SendEmailReminderParams) => {
const { mailData, sendTo, scheduledDate, uid, workflowStepId } = params;
let reminderUid = undefined;
if (scheduledDate) {
const reminder = await prisma.workflowReminder.create({
data: {
bookingUid: uid,
workflowStepId,
method: WorkflowMethods.EMAIL,
scheduledDate,
scheduled: true,
},
});
reminderUid = reminder.uuid;
}
await sendOrScheduleWorkflowEmails({
...mailData,
to: sendTo,
sendAt: scheduledDate,
referenceUid: reminderUid ?? undefined,
});
};
export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => {
const { verifiedAt, workflowStepId } = args;
if (!verifiedAt) {
log.warn(`Workflow step ${workflowStepId} not yet verified`);
return;
}
if (args.evt) {
await scheduleEmailReminderForEvt(args);
} else {
await scheduleEmailReminderForForm(args);
}
};
const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { evt: BookingInfo }) => {
const {
evt,
triggerEvent,
timeSpan,
template,
sender,
workflowStepId,
seatReferenceUid,
sendTo,
emailSubject = "",
emailBody = "",
hideBranding,
includeCalendarEvent,
action,
} = args;
const uid = evt.uid as string;
const scheduledDate = WorkflowService.processWorkflowScheduledDate({
workflowTriggerEvent: triggerEvent,
time: timeSpan.time,
timeUnit: timeSpan.timeUnit,
evt,
});
const workflowReminderRepository = new WorkflowReminderRepository(prisma);
const bookingSeatRepository = new BookingSeatRepository(prisma);
const emailWorkflowService = new EmailWorkflowService(workflowReminderRepository, bookingSeatRepository);
const mailData = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt,
sendTo,
seatReferenceUid,
hideBranding,
emailSubject,
emailBody,
sender: sender || "",
action,
template,
includeCalendarEvent,
triggerEvent,
});
await sendOrScheduleWorkflowEmailWithReminder({
mailData,
sendTo,
triggerEvent,
scheduledDate,
uid,
workflowStepId,
seatReferenceUid,
});
};
// sends all immediately, no scheduling needed
const scheduleEmailReminderForForm = async (
args: scheduleEmailReminderArgs & {
formData: FormSubmissionData;
}
) => {
const {
formData,
triggerEvent,
sender,
workflowStepId,
sendTo,
emailSubject = "",
emailBody = "",
hideBranding,
} = args;
const emailContent = {
emailSubject,
emailBody: `<body style="white-space: pre-wrap;">${emailBody}</body>`,
};
if (emailBody) {
const timeFormat = getTimeFormatStringFromUserTimeFormat(formData.user.timeFormat);
const variables: VariablesType = {
responses: transformRoutingFormResponsesToVariableFormat(formData.responses),
};
const emailSubjectTemplate = customTemplate(emailSubject, variables, formData.user.locale, timeFormat);
emailContent.emailSubject = emailSubjectTemplate.text;
emailContent.emailBody = customTemplate(
emailBody,
variables,
formData.user.locale,
timeFormat,
hideBranding
).html;
}
// Allows debugging generated email content without waiting for sendgrid to send emails
log.debug(`Sending Email for trigger ${triggerEvent}`, JSON.stringify(emailContent));
const mailData = {
subject: emailContent.emailSubject,
html: replaceCloakedLinksInHtml(emailContent.emailBody),
sender,
};
await sendOrScheduleWorkflowEmailWithReminder({
mailData,
sendTo,
triggerEvent,
workflowStepId,
scheduledDate: null,
});
};
export const deleteScheduledEmailReminder = async (reminderId: number) => {
const workflowReminder = await prisma.workflowReminder.findUnique({
where: {
id: reminderId,
},
});
if (!workflowReminder) {
console.error("Workflow reminder not found");
return;
}
const { uuid, referenceId } = workflowReminder;
if (uuid) {
try {
const taskId = await tasker.cancelWithReference(uuid, "sendWorkflowEmails");
if (taskId) {
await prisma.workflowReminder.delete({
where: {
id: reminderId,
},
});
return;
}
} catch (error) {
log.error(`Error canceling/deleting reminder with tasker. Error: ${error}`);
}
}
/**
* @deprecated only needed for SendGrid, use SMTP with tasker instead
*/
try {
if (!referenceId) {
await prisma.workflowReminder.delete({
where: {
id: reminderId,
},
});
return;
}
await prisma.workflowReminder.update({
where: {
id: reminderId,
},
data: {
cancelled: true,
},
});
} catch (error) {
log.error(`Error canceling reminder with error ${error}`);
}
};