import client from "@sendgrid/client"; import type { MailData } from "@sendgrid/helpers/classes/mail"; import sgMail from "@sendgrid/mail"; import { createEvent } from "ics"; import type { ParticipationStatus } from "ics"; import type { DateArray } from "ics"; import { RRule } from "rrule"; import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; import { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { TimeUnit } from "@calcom/prisma/enums"; import { WorkflowActions, WorkflowMethods, WorkflowTemplates, WorkflowTriggerEvents, } from "@calcom/prisma/enums"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import type { AttendeeInBookingInfo, BookingInfo, timeUnitLowerCase } from "./smsReminderManager"; import type { VariablesType } from "./templates/customTemplate"; import customTemplate from "./templates/customTemplate"; import emailReminderTemplate from "./templates/emailReminderTemplate"; let sendgridAPIKey, senderEmail: string; const log = logger.getChildLogger({ prefix: ["[emailReminderManager]"] }); if (process.env.SENDGRID_API_KEY) { sendgridAPIKey = process.env.SENDGRID_API_KEY as string; senderEmail = process.env.SENDGRID_EMAIL as string; sgMail.setApiKey(sendgridAPIKey); client.setApiKey(sendgridAPIKey); } async function getBatchId() { if (!process.env.SENDGRID_API_KEY) { console.info("No sendgrid API key provided, returning DUMMY_BATCH_ID"); return "DUMMY_BATCH_ID"; } const batchIdResponse = await client.request({ url: "/v3/mail/batch", method: "POST", }); return batchIdResponse[1].batch_id as string; } function getiCalEventAsString(evt: BookingInfo, status?: ParticipationStatus) { const uid = uuidv4(); let recurrenceRule: string | undefined = undefined; if (evt.eventType.recurringEvent?.count) { recurrenceRule = new RRule(evt.eventType.recurringEvent).toString().replace("RRULE:", ""); } const icsEvent = createEvent({ uid, startInputType: "utc", start: dayjs(evt.startTime) .utc() .toArray() .slice(0, 6) .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, duration: { minutes: dayjs(evt.endTime).diff(dayjs(evt.startTime), "minute") }, title: evt.title, description: evt.additionalNotes || "", location: evt.location || "", organizer: { email: evt.organizer.email || "", name: evt.organizer.name }, attendees: [ { name: preprocessNameFieldDataWithVariant("fullName", evt.attendees[0].name) as string, email: evt.attendees[0].email, partstat: status, role: "REQ-PARTICIPANT", rsvp: true, }, ], method: "REQUEST", ...{ recurrenceRule }, status: "CONFIRMED", }); if (icsEvent.error) { throw icsEvent.error; } return icsEvent.value; } type ScheduleEmailReminderAction = Extract< WorkflowActions, "EMAIL_HOST" | "EMAIL_ATTENDEE" | "EMAIL_ADDRESS" >; export const scheduleEmailReminder = async ( evt: BookingInfo, triggerEvent: WorkflowTriggerEvents, action: ScheduleEmailReminderAction, timeSpan: { time: number | null; timeUnit: TimeUnit | null; }, sendTo: MailData["to"], emailSubject: string, emailBody: string, workflowStepId: number, template: WorkflowTemplates, sender: string, hideBranding?: boolean, seatReferenceUid?: string, includeCalendarEvent?: boolean ) => { if (action === WorkflowActions.EMAIL_ADDRESS) return; const { startTime, endTime } = evt; const uid = evt.uid as string; const currentDate = dayjs(); const timeUnit: timeUnitLowerCase | undefined = timeSpan.timeUnit?.toLocaleLowerCase() as timeUnitLowerCase; let scheduledDate = null; if (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT) { scheduledDate = timeSpan.time && timeUnit ? dayjs(startTime).subtract(timeSpan.time, timeUnit) : null; } else if (triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) { scheduledDate = timeSpan.time && timeUnit ? dayjs(endTime).add(timeSpan.time, timeUnit) : null; } if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) { console.error("Sendgrid credentials are missing from the .env file"); } const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false; let attendeeEmailToBeUsedInMail: string | null = null; let attendeeToBeUsedInMail: AttendeeInBookingInfo | null = null; let name = ""; let attendeeName = ""; let timeZone = ""; switch (action) { case WorkflowActions.EMAIL_HOST: attendeeToBeUsedInMail = evt.attendees[0]; name = evt.organizer.name; attendeeName = attendeeToBeUsedInMail.name; timeZone = evt.organizer.timeZone; break; case WorkflowActions.EMAIL_ATTENDEE: //These type checks are required as sendTo is of type MailData["to"] which in turn is of string | {name?:string, email: string} | string | {name?:string, email: string}[0] // and the email is being sent to the first attendee of event by default instead of the sendTo // so check if first attendee can be extracted from sendTo -> attendeeEmailToBeUsedInMail if (typeof sendTo === "string") { attendeeEmailToBeUsedInMail = sendTo; } else if (Array.isArray(sendTo)) { // If it's an array, take the first entry (if it exists) and extract name and email (if object); otherwise, just put the email (if string) const emailData = sendTo[0]; if (typeof emailData === "object" && emailData !== null) { const { name, email } = emailData; attendeeEmailToBeUsedInMail = email; } else if (typeof emailData === "string") { attendeeEmailToBeUsedInMail = emailData; } } else if (typeof sendTo === "object" && sendTo !== null) { const { name, email } = sendTo; attendeeEmailToBeUsedInMail = email; } // check if first attendee of sendTo is present in the attendees list, if not take the evt attendee const attendeeEmailToBeUsedInMailFromEvt = evt.attendees.find( (attendee) => attendee.email === attendeeEmailToBeUsedInMail ); attendeeToBeUsedInMail = attendeeEmailToBeUsedInMailFromEvt ? attendeeEmailToBeUsedInMailFromEvt : evt.attendees[0]; name = attendeeToBeUsedInMail.name; attendeeName = evt.organizer.name; timeZone = attendeeToBeUsedInMail.timeZone; break; } let emailContent = { emailSubject, emailBody: `
${emailBody}`, }; if (emailBody) { const variables: VariablesType = { eventName: evt.title || "", organizerName: evt.organizer.name, attendeeName: attendeeToBeUsedInMail.name, attendeeFirstName: attendeeToBeUsedInMail.firstName, attendeeLastName: attendeeToBeUsedInMail.lastName, attendeeEmail: attendeeToBeUsedInMail.email, eventDate: dayjs(startTime).tz(timeZone), eventEndTime: dayjs(endTime).tz(timeZone), timeZone: timeZone, location: evt.location, additionalNotes: evt.additionalNotes, responses: evt.responses, meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl, cancelLink: `/booking/${evt.uid}?cancel=true`, rescheduleLink: `/${evt.organizer.username}/${evt.eventType.slug}?rescheduleUid=${evt.uid}`, }; const locale = action === WorkflowActions.EMAIL_ATTENDEE ? attendeeToBeUsedInMail.language?.locale : evt.organizer.language.locale; const emailSubjectTemplate = customTemplate(emailSubject, variables, locale, evt.organizer.timeFormat); emailContent.emailSubject = emailSubjectTemplate.text; emailContent.emailBody = customTemplate( emailBody, variables, locale, evt.organizer.timeFormat, hideBranding ).html; } else if (template === WorkflowTemplates.REMINDER) { emailContent = emailReminderTemplate( false, action, evt.organizer.timeFormat, startTime, endTime, evt.title, timeZone, attendeeName, name ); } // Allows debugging generated email content without waiting for sendgrid to send emails log.debug(`Sending Email for trigger ${triggerEvent}`, JSON.stringify(emailContent)); const batchId = await getBatchId(); function sendEmail(data: Partial