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:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
04c9453d5c
commit
fc518bfdc1
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user