diff --git a/apps/web/modules/ee/workflows/components/WorkflowStepContainer.tsx b/apps/web/modules/ee/workflows/components/WorkflowStepContainer.tsx index 574d186379..6d05654d5c 100644 --- a/apps/web/modules/ee/workflows/components/WorkflowStepContainer.tsx +++ b/apps/web/modules/ee/workflows/components/WorkflowStepContainer.tsx @@ -1648,6 +1648,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { excludedToolbarItems={ isSMSAction(step.action) ? ["blockType", "bold", "italic", "link"] + : isOrganization + ? [] : ["link"] } plainText={isSMSAction(step.action)} diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index d414cd3365..be6f892ea5 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -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: diff --git a/packages/features/ee/workflows/lib/getWorkflowReminders.ts b/packages/features/ee/workflows/lib/getWorkflowReminders.ts index 99ca56db09..b277b067d5 100644 --- a/packages/features/ee/workflows/lib/getWorkflowReminders.ts +++ b/packages/features/ee/workflows/lib/getWorkflowReminders.ts @@ -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, + }, + }, }, }, }, diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 1b539f73e3..e4253cc0af 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -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, }; diff --git a/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts b/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts index df528d2c8f..624acb43c9 100644 --- a/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts +++ b/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts @@ -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, }), diff --git a/packages/features/ee/workflows/repositories/WorkflowReminderRepository.ts b/packages/features/ee/workflows/repositories/WorkflowReminderRepository.ts index c69a058c61..beab47c2f0 100644 --- a/packages/features/ee/workflows/repositories/WorkflowReminderRepository.ts +++ b/packages/features/ee/workflows/repositories/WorkflowReminderRepository.ts @@ -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, + }, + }, + }, + }, }, }, },