Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
excludedToolbarItems={
isSMSAction(step.action)
? ["blockType", "bold", "italic", "link"]
: isOrganization
? []
: ["link"]
}
plainText={isSMSAction(step.action)}
Expand Down
30 changes: 22 additions & 8 deletions packages/features/ee/workflows/api/scheduleEmailReminders.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/**
* @deprecated use smtp with tasker instead
*/
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";

import process from "node:process";
import dayjs from "@calcom/dayjs";
import generateIcsString from "@calcom/emails/lib/generateIcsString";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
Expand All @@ -17,7 +15,9 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import { SchedulingType, WorkflowActions, WorkflowTemplates } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import {
getAllRemindersToCancel,
getAllRemindersToDelete,
Expand All @@ -33,9 +33,9 @@ import {
} from "../lib/reminders/providers/sendgridProvider";
import type { VariablesType } from "../lib/reminders/templates/customTemplate";
import customTemplate from "../lib/reminders/templates/customTemplate";
import { replaceCloakedLinksInHtml } from "../lib/reminders/utils";
import emailRatingTemplate from "../lib/reminders/templates/emailRatingTemplate";
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
import { replaceCloakedLinksInHtml } from "../lib/reminders/utils";

export async function handler(req: NextRequest) {
const apiKey = req.headers.get("authorization") || req.nextUrl.searchParams.get("apiKey");
Expand Down Expand Up @@ -126,7 +126,7 @@ export async function handler(req: NextRequest) {
// For seated events, get the correct attendee based on seatReferenceId
let targetAttendee = reminder.booking?.attendees[0];
if (reminder.seatReferenceId) {
const bookingSeatRepository = new BookingSeatRepository(prisma);
const bookingSeatRepository = new BookingSeatRepository(prisma);
const seatAttendeeData = await bookingSeatRepository.getByReferenceUidWithAttendeeDetails(
reminder.seatReferenceId
);
Expand Down Expand Up @@ -355,10 +355,17 @@ export async function handler(req: NextRequest) {
title: booking.title || booking.eventType?.title || "",
};

// Organization accounts are allowed to use cloaked links (URL behind text)
// since they are paid accounts with lower spam/scam risk
const isOrganization = reminder.workflowStep?.workflow?.team?.isOrganization ?? false;
const processedEmailBody = isOrganization
? emailContent.emailBody
: replaceCloakedLinksInHtml(emailContent.emailBody);
Comment on lines +360 to +363
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This change contradicts the stated security design in the PR description. The description explicitly says this deprecated file should "default to sanitizing all links (safer behavior)" as the intentional security posture for edge cases. Adding the organization check here allows cloaked links for organizations, which is inconsistent with that design. If the stricter default is intended for deprecated paths, remove these organization checks and let all emails have links sanitized in this file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/ee/workflows/api/scheduleEmailReminders.ts, line 360:

<comment>This change contradicts the stated security design in the PR description. The description explicitly says this deprecated file should "default to sanitizing all links (safer behavior)" as the intentional security posture for edge cases. Adding the organization check here allows cloaked links for organizations, which is inconsistent with that design. If the stricter default is intended for deprecated paths, remove these organization checks and let all emails have links sanitized in this file.</comment>

<file context>
@@ -355,10 +355,17 @@ export async function handler(req: NextRequest) {
 
+          // Organization accounts are allowed to use cloaked links (URL behind text)
+          // since they are paid accounts with lower spam/scam risk
+          const isOrganization = reminder.workflowStep?.workflow?.team?.isOrganization ?? false;
+          const processedEmailBody = isOrganization
+            ? emailContent.emailBody
</file context>

Fix confidence (alpha): 8/10

Suggested change
const isOrganization = reminder.workflowStep?.workflow?.team?.isOrganization ?? false;
const processedEmailBody = isOrganization
? emailContent.emailBody
: replaceCloakedLinksInHtml(emailContent.emailBody);
const processedEmailBody = replaceCloakedLinksInHtml(emailContent.emailBody);


const mailData = {
subject: emailContent.emailSubject,
to: Array.isArray(sendTo) ? sendTo : [sendTo],
html: replaceCloakedLinksInHtml(emailContent.emailBody),
html: processedEmailBody,
attachments: reminder.workflowStep.includeCalendarEvent
? [
{
Expand Down Expand Up @@ -454,10 +461,17 @@ export async function handler(req: NextRequest) {
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
const batchId = isSendgridEnabled ? await getBatchId() : undefined;

// Organization accounts are allowed to use cloaked links (URL behind text)
// since they are paid accounts with lower spam/scam risk
const isOrganization = reminder.workflowStep?.workflow?.team?.isOrganization ?? false;
const processedEmailBody = isOrganization
? emailContent.emailBody
: replaceCloakedLinksInHtml(emailContent.emailBody);

const mailData = {
subject: emailContent.emailSubject,
to: [sendTo],
html: replaceCloakedLinksInHtml(emailContent.emailBody),
html: processedEmailBody,
sender: reminder.workflowStep?.sender,
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
replyTo:
Expand Down
11 changes: 9 additions & 2 deletions packages/features/ee/workflows/lib/getWorkflowReminders.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import type { EventType, User, WorkflowReminder, WorkflowStep } from "@calcom/prisma/client";
import type { Prisma } from "@calcom/prisma/client";
import type { EventType, Prisma, User, WorkflowReminder, WorkflowStep } from "@calcom/prisma/client";
import { WorkflowMethods } from "@calcom/prisma/enums";

type PartialWorkflowStep =
| (Partial<WorkflowStep> & {
workflow: {
userId?: number;
teamId?: number;
team?: {
isOrganization: boolean;
} | null;
};
})
| null;
Expand Down Expand Up @@ -148,6 +150,11 @@ export const select = {
select: {
userId: true,
teamId: true,
team: {
select: {
isOrganization: true,
},
},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ 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 { WorkflowMethods, type WorkflowTemplates, type WorkflowTriggerEvents } from "@calcom/prisma/enums";

import type { BookingInfo, ScheduleEmailReminderAction, FormSubmissionData } from "../types";
import type { BookingInfo, FormSubmissionData, ScheduleEmailReminderAction } from "../types";
import { sendOrScheduleWorkflowEmails } from "./providers/emailProvider";
import type { WorkflowContextData } from "./reminderScheduler";
import type { VariablesType } from "./templates/customTemplate";
Expand Down Expand Up @@ -38,6 +38,7 @@ type scheduleEmailReminderArgs = ScheduleReminderArgs & {
hideBranding?: boolean;
includeCalendarEvent?: boolean;
verifiedAt: Date | null;
isOrganization?: boolean;
};

type SendEmailReminderParams = {
Expand All @@ -64,7 +65,7 @@ type SendEmailReminderParams = {
const sendOrScheduleWorkflowEmailWithReminder = async (params: SendEmailReminderParams) => {
const { mailData, sendTo, scheduledDate, uid, workflowStepId } = params;

let reminderUid = undefined;
let reminderUid;
if (scheduledDate) {
const reminder = await prisma.workflowReminder.create({
data: {
Expand Down Expand Up @@ -115,6 +116,7 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e
hideBranding,
includeCalendarEvent,
action,
isOrganization,
} = args;

const uid = evt.uid as string;
Expand All @@ -140,6 +142,7 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e
template,
includeCalendarEvent,
triggerEvent,
isOrganization,
});

await sendOrScheduleWorkflowEmailWithReminder({
Expand Down Expand Up @@ -168,6 +171,7 @@ const scheduleEmailReminderForForm = async (
emailSubject = "",
emailBody = "",
hideBranding,
isOrganization,
} = args;

const emailContent = {
Expand Down Expand Up @@ -196,9 +200,15 @@ const scheduleEmailReminderForForm = async (
// Allows debugging generated email content without waiting for sendgrid to send emails
log.debug(`Sending Email for trigger ${triggerEvent}`, JSON.stringify(emailContent));

// Organization accounts are allowed to use cloaked links (URL behind text)
// since they are paid accounts with lower spam/scam risk
const processedEmailBody = isOrganization
? emailContent.emailBody
: replaceCloakedLinksInHtml(emailContent.emailBody);

const mailData = {
subject: emailContent.emailSubject,
html: replaceCloakedLinksInHtml(emailContent.emailBody),
html: processedEmailBody,
sender,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class EmailWorkflowService {
}

const workflow = workflowReminder.workflowStep.workflow;
const isOrganization = workflow.team?.isOrganization ?? false;

let emailAttendeeSendToOverride: string | null = null;
if (workflowReminder.seatReferenceId) {
Expand Down Expand Up @@ -110,6 +111,7 @@ export class EmailWorkflowService {
action: workflowReminder.workflowStep.action as ScheduleEmailReminderAction,
template: workflowReminder.workflowStep.template,
includeCalendarEvent: workflowReminder.workflowStep.includeCalendarEvent,
isOrganization,
});

const results = await Promise.allSettled(
Expand Down Expand Up @@ -250,6 +252,7 @@ export class EmailWorkflowService {
template,
includeCalendarEvent,
triggerEvent,
isOrganization,
}: {
evt: BookingInfo;
sendTo: string[];
Expand All @@ -262,6 +265,7 @@ export class EmailWorkflowService {
template?: WorkflowTemplates;
includeCalendarEvent?: boolean;
triggerEvent: WorkflowTriggerEvents;
isOrganization?: boolean;
}) {
const log = logger.getSubLogger({
prefix: [`[generateEmailPayloadForEvtWorkflow]: bookingUid: ${evt?.uid}`],
Expand Down Expand Up @@ -536,9 +540,15 @@ export class EmailWorkflowService {
const customReplyToEmail =
evt?.eventType?.customReplyToEmail || (evt as CalendarEvent).customReplyToEmail;

// Organization accounts are allowed to use cloaked links (URL behind text)
// since they are paid accounts with lower spam/scam risk
const processedEmailBody = isOrganization
? emailContent.emailBody
: replaceCloakedLinksInHtml(emailContent.emailBody);

return {
subject: emailContent.emailSubject,
html: replaceCloakedLinksInHtml(emailContent.emailBody),
html: processedEmailBody,
...(!evt.hideOrganizerEmail && {
replyTo: customReplyToEmail || evt.organizer.email,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,37 @@ export class WorkflowReminderRepository {
where: {
id,
},
include: {
select: {
seatReferenceId: true,
workflowStep: {
include: {
workflow: true,
select: {
id: true,
verifiedAt: true,
action: true,
template: true,
includeCalendarEvent: true,
reminderBody: true,
sendTo: true,
emailSubject: true,
sender: true,
numberVerificationPending: true,
numberRequired: true,
workflow: {
select: {
id: true,
name: true,
trigger: true,
time: true,
timeUnit: true,
userId: true,
teamId: true,
team: {
select: {
isOrganization: true,
},
},
},
},
},
},
},
Expand Down
Loading