feat: allow link cloaking for Organization accounts in workflow emails (#26528)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Peer Richelsen
2026-01-16 08:33:38 +00:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 04c9453d5c
commit fc518bfdc1
6 changed files with 88 additions and 18 deletions
@@ -1648,6 +1648,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
excludedToolbarItems={
isSMSAction(step.action)
? ["blockType", "bold", "italic", "link"]
: isOrganization
? []
: ["link"]
}
plainText={isSMSAction(step.action)}
@@ -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";
@@ -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,
@@ -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");
@@ -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
);
@@ -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);
const mailData = {
subject: emailContent.emailSubject,
to: Array.isArray(sendTo) ? sendTo : [sendTo],
html: replaceCloakedLinksInHtml(emailContent.emailBody),
html: processedEmailBody,
attachments: reminder.workflowStep.includeCalendarEvent
? [
{
@@ -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:
@@ -1,7 +1,6 @@
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 =
@@ -9,6 +8,9 @@ type PartialWorkflowStep =
workflow: {
userId?: number;
teamId?: number;
team?: {
isOrganization: boolean;
} | null;
};
})
| null;
@@ -148,6 +150,11 @@ export const select = {
select: {
userId: true,
teamId: true,
team: {
select: {
isOrganization: true,
},
},
},
},
},
@@ -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";
@@ -38,6 +38,7 @@ type scheduleEmailReminderArgs = ScheduleReminderArgs & {
hideBranding?: boolean;
includeCalendarEvent?: boolean;
verifiedAt: Date | null;
isOrganization?: boolean;
};
type SendEmailReminderParams = {
@@ -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: {
@@ -115,6 +116,7 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e
hideBranding,
includeCalendarEvent,
action,
isOrganization,
} = args;
const uid = evt.uid as string;
@@ -140,6 +142,7 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e
template,
includeCalendarEvent,
triggerEvent,
isOrganization,
});
await sendOrScheduleWorkflowEmailWithReminder({
@@ -168,6 +171,7 @@ const scheduleEmailReminderForForm = async (
emailSubject = "",
emailBody = "",
hideBranding,
isOrganization,
} = args;
const emailContent = {
@@ -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,
};
@@ -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) {
@@ -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(
@@ -250,6 +252,7 @@ export class EmailWorkflowService {
template,
includeCalendarEvent,
triggerEvent,
isOrganization,
}: {
evt: BookingInfo;
sendTo: string[];
@@ -262,6 +265,7 @@ export class EmailWorkflowService {
template?: WorkflowTemplates;
includeCalendarEvent?: boolean;
triggerEvent: WorkflowTriggerEvents;
isOrganization?: boolean;
}) {
const log = logger.getSubLogger({
prefix: [`[generateEmailPayloadForEvtWorkflow]: bookingUid: ${evt?.uid}`],
@@ -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,
}),
@@ -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,
},
},
},
},
},
},
},