2022-07-14 00:10:45 +00:00
import client from "@sendgrid/client" ;
2023-04-13 19:03:08 +00:00
import type { MailData } from "@sendgrid/helpers/classes/mail" ;
2022-07-14 00:10:45 +00:00
import sgMail from "@sendgrid/mail" ;
2023-08-29 11:56:26 +00:00
import { createEvent } from "ics" ;
import type { ParticipationStatus } from "ics" ;
import type { DateArray } from "ics" ;
import { RRule } from "rrule" ;
import { v4 as uuidv4 } from "uuid" ;
2022-07-14 00:10:45 +00:00
import dayjs from "@calcom/dayjs" ;
2023-08-29 11:56:26 +00:00
import { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils" ;
2023-04-18 10:08:09 +00:00
import logger from "@calcom/lib/logger" ;
2022-07-14 00:10:45 +00:00
import prisma from "@calcom/prisma" ;
2023-05-02 11:44:05 +00:00
import type { TimeUnit } from "@calcom/prisma/enums" ;
2023-08-02 09:35:48 +00:00
import {
WorkflowActions ,
WorkflowMethods ,
WorkflowTemplates ,
WorkflowTriggerEvents ,
} from "@calcom/prisma/enums" ;
2022-12-18 02:04:06 +00:00
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils" ;
2022-07-28 19:58:26 +00:00
2023-08-04 13:53:58 +00:00
import type { AttendeeInBookingInfo , BookingInfo , timeUnitLowerCase } from "./smsReminderManager" ;
2023-03-10 14:28:42 +00:00
import type { VariablesType } from "./templates/customTemplate" ;
import customTemplate from "./templates/customTemplate" ;
2022-07-28 19:58:26 +00:00
import emailReminderTemplate from "./templates/emailReminderTemplate" ;
2022-07-14 00:10:45 +00:00
let sendgridAPIKey , senderEmail : string ;
2023-04-18 10:08:09 +00:00
const log = logger . getChildLogger ( { prefix : [ "[emailReminderManager]" ] } ) ;
2022-07-14 00:10:45 +00:00
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 ) ;
}
2023-08-02 09:35:48 +00:00
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 ;
}
2023-08-29 11:56:26 +00:00
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 ;
}
2023-08-04 13:53:58 +00:00
type ScheduleEmailReminderAction = Extract <
WorkflowActions ,
"EMAIL_HOST" | "EMAIL_ATTENDEE" | "EMAIL_ADDRESS"
> ;
2022-07-14 00:10:45 +00:00
export const scheduleEmailReminder = async (
evt : BookingInfo ,
triggerEvent : WorkflowTriggerEvents ,
2023-08-04 13:53:58 +00:00
action : ScheduleEmailReminderAction ,
2022-10-07 18:18:28 +00:00
timeSpan : {
2022-07-14 00:10:45 +00:00
time : number | null ;
timeUnit : TimeUnit | null ;
} ,
2023-04-13 19:03:08 +00:00
sendTo : MailData [ "to" ] ,
2022-07-14 00:10:45 +00:00
emailSubject : string ,
emailBody : string ,
workflowStepId : number ,
2023-01-18 14:32:39 +00:00
template : WorkflowTemplates ,
2023-04-18 10:08:09 +00:00
sender : string ,
2023-08-01 14:13:28 +00:00
hideBranding? : boolean ,
2023-08-29 11:56:26 +00:00
seatReferenceUid? : string ,
includeCalendarEvent? : boolean
2022-07-14 00:10:45 +00:00
) = > {
2022-12-16 21:11:08 +00:00
if ( action === WorkflowActions . EMAIL_ADDRESS ) return ;
2022-08-04 08:52:05 +00:00
const { startTime , endTime } = evt ;
2022-07-14 00:10:45 +00:00
const uid = evt . uid as string ;
const currentDate = dayjs ( ) ;
2022-10-07 18:18:28 +00:00
const timeUnit : timeUnitLowerCase | undefined = timeSpan . timeUnit ? . toLocaleLowerCase ( ) as timeUnitLowerCase ;
2022-07-14 00:10:45 +00:00
2022-10-07 18:18:28 +00:00
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 ;
}
2023-04-18 10:08:09 +00:00
2022-07-20 19:48:40 +00:00
if ( ! process . env . SENDGRID_API_KEY || ! process . env . SENDGRID_EMAIL ) {
console . error ( "Sendgrid credentials are missing from the .env file" ) ;
}
2022-07-14 00:10:45 +00:00
2023-07-21 15:36:39 +00:00
const sandboxMode = process . env . NEXT_PUBLIC_IS_E2E ? true : false ;
2023-08-04 13:53:58 +00:00
let attendeeEmailToBeUsedInMail : string | null = null ;
let attendeeToBeUsedInMail : AttendeeInBookingInfo | null = null ;
2022-10-10 13:40:20 +00:00
let name = "" ;
let attendeeName = "" ;
let timeZone = "" ;
switch ( action ) {
case WorkflowActions . EMAIL_HOST :
2023-08-04 13:53:58 +00:00
attendeeToBeUsedInMail = evt . attendees [ 0 ] ;
2022-10-10 13:40:20 +00:00
name = evt . organizer . name ;
2023-08-04 13:53:58 +00:00
attendeeName = attendeeToBeUsedInMail . name ;
2022-10-10 13:40:20 +00:00
timeZone = evt . organizer . timeZone ;
break ;
case WorkflowActions . EMAIL_ATTENDEE :
2023-08-04 13:53:58 +00:00
//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 ;
2022-10-10 13:40:20 +00:00
attendeeName = evt . organizer . name ;
2023-08-04 13:53:58 +00:00
timeZone = attendeeToBeUsedInMail . timeZone ;
2022-10-10 13:40:20 +00:00
break ;
}
2022-07-14 00:10:45 +00:00
2022-07-21 18:56:20 +00:00
let emailContent = {
emailSubject ,
2023-04-18 10:08:09 +00:00
emailBody : ` <body style="white-space: pre-wrap;"> ${ emailBody } </body> ` ,
2022-07-21 18:56:20 +00:00
} ;
2023-04-18 10:08:09 +00:00
if ( emailBody ) {
const variables : VariablesType = {
eventName : evt.title || "" ,
organizerName : evt.organizer.name ,
2023-08-04 13:53:58 +00:00
attendeeName : attendeeToBeUsedInMail.name ,
attendeeFirstName : attendeeToBeUsedInMail.firstName ,
attendeeLastName : attendeeToBeUsedInMail.lastName ,
attendeeEmail : attendeeToBeUsedInMail.email ,
2023-04-18 10:08:09 +00:00
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 =
2023-08-04 13:53:58 +00:00
action === WorkflowActions . EMAIL_ATTENDEE
? attendeeToBeUsedInMail . language ? . locale
2023-04-18 10:08:09 +00:00
: evt . organizer . language . locale ;
2023-07-19 14:30:37 +00:00
const emailSubjectTemplate = customTemplate ( emailSubject , variables , locale , evt . organizer . timeFormat ) ;
2023-04-18 10:08:09 +00:00
emailContent . emailSubject = emailSubjectTemplate . text ;
2023-07-19 14:30:37 +00:00
emailContent . emailBody = customTemplate (
emailBody ,
variables ,
locale ,
evt . organizer . timeFormat ,
hideBranding
) . html ;
2023-04-18 10:08:09 +00:00
} else if ( template === WorkflowTemplates . REMINDER ) {
emailContent = emailReminderTemplate (
false ,
action ,
2023-07-19 14:30:37 +00:00
evt . organizer . timeFormat ,
2023-04-18 10:08:09 +00:00
startTime ,
endTime ,
evt . title ,
timeZone ,
attendeeName ,
name
) ;
2022-07-14 00:10:45 +00:00
}
2023-04-18 10:08:09 +00:00
// Allows debugging generated email content without waiting for sendgrid to send emails
log . debug ( ` Sending Email for trigger ${ triggerEvent } ` , JSON . stringify ( emailContent ) ) ;
2023-08-02 09:35:48 +00:00
const batchId = await getBatchId ( ) ;
2023-08-29 11:56:26 +00:00
function sendEmail ( data : Partial < MailData > , triggerEvent? : WorkflowTriggerEvents ) {
2023-08-02 09:35:48 +00:00
if ( ! process . env . SENDGRID_API_KEY ) {
console . info ( "No sendgrid API key provided, skipping email" ) ;
return Promise . resolve ( ) ;
}
2023-08-29 11:56:26 +00:00
const status : ParticipationStatus =
triggerEvent === WorkflowTriggerEvents . AFTER_EVENT
? "COMPLETED"
: triggerEvent === WorkflowTriggerEvents . EVENT_CANCELLED
? "DECLINED"
: "ACCEPTED" ;
2023-08-02 09:35:48 +00:00
return sgMail . send ( {
to : data.to ,
from : {
email : senderEmail ,
name : sender ,
} ,
subject : emailContent.emailSubject ,
html : emailContent.emailBody ,
batchId ,
replyTo : evt.organizer.email ,
mailSettings : {
sandboxMode : {
enable : sandboxMode ,
} ,
} ,
2023-08-29 11:56:26 +00:00
attachments : includeCalendarEvent
? [
{
content : Buffer.from ( getiCalEventAsString ( evt , status ) || "" ) . toString ( "base64" ) ,
filename : "event.ics" ,
type : "text/calendar; method=REQUEST" ,
disposition : "attachment" ,
contentId : uuidv4 ( ) ,
} ,
]
: undefined ,
2023-08-22 19:29:06 +00:00
sendAt : data.sendAt ,
2023-08-02 09:35:48 +00:00
} ) ;
}
2023-04-18 10:08:09 +00:00
2022-07-14 00:10:45 +00:00
if (
triggerEvent === WorkflowTriggerEvents . NEW_EVENT ||
2022-08-31 23:09:34 +00:00
triggerEvent === WorkflowTriggerEvents . EVENT_CANCELLED ||
triggerEvent === WorkflowTriggerEvents . RESCHEDULE_EVENT
2022-07-14 00:10:45 +00:00
) {
try {
2023-08-02 09:35:48 +00:00
if ( ! sendTo ) throw new Error ( "No email addresses provided" ) ;
const addressees = Array . isArray ( sendTo ) ? sendTo : [ sendTo ] ;
2023-08-29 11:56:26 +00:00
const promises = addressees . map ( ( email ) = > sendEmail ( { to : email } , triggerEvent ) ) ;
2023-08-02 09:35:48 +00:00
// TODO: Maybe don't await for this?
await Promise . all ( promises ) ;
2022-07-14 00:10:45 +00:00
} catch ( error ) {
console . log ( "Error sending Email" ) ;
}
2022-10-07 18:18:28 +00:00
} else if (
( triggerEvent === WorkflowTriggerEvents . BEFORE_EVENT ||
triggerEvent === WorkflowTriggerEvents . AFTER_EVENT ) &&
scheduledDate
) {
2022-07-14 00:10:45 +00:00
// Sendgrid to schedule emails
// Can only schedule at least 60 minutes and at most 72 hours in advance
if (
currentDate . isBefore ( scheduledDate . subtract ( 1 , "hour" ) ) &&
! scheduledDate . isAfter ( currentDate . add ( 72 , "hour" ) )
) {
try {
2023-08-02 09:35:48 +00:00
// If sendEmail failed then workflowReminer will not be created, failing E2E tests
2023-08-29 11:56:26 +00:00
await sendEmail (
{
to : sendTo ,
sendAt : scheduledDate.unix ( ) ,
} ,
triggerEvent
) ;
2022-07-14 00:10:45 +00:00
await prisma . workflowReminder . create ( {
data : {
bookingUid : uid ,
workflowStepId : workflowStepId ,
method : WorkflowMethods.EMAIL ,
scheduledDate : scheduledDate.toDate ( ) ,
scheduled : true ,
2023-08-02 09:35:48 +00:00
referenceId : batchId ,
2023-08-01 14:13:28 +00:00
seatReferenceId : seatReferenceUid ,
2022-07-14 00:10:45 +00:00
} ,
} ) ;
} catch ( error ) {
console . log ( ` Error scheduling email with error ${ error } ` ) ;
}
} else if ( scheduledDate . isAfter ( currentDate . add ( 72 , "hour" ) ) ) {
// Write to DB and send to CRON if scheduled reminder date is past 72 hours
await prisma . workflowReminder . create ( {
data : {
bookingUid : uid ,
workflowStepId : workflowStepId ,
method : WorkflowMethods.EMAIL ,
scheduledDate : scheduledDate.toDate ( ) ,
scheduled : false ,
2023-08-01 14:13:28 +00:00
seatReferenceId : seatReferenceUid ,
2022-07-14 00:10:45 +00:00
} ,
} ) ;
}
}
} ;
2023-03-10 14:28:42 +00:00
export const deleteScheduledEmailReminder = async ( reminderId : number , referenceId : string | null ) = > {
2022-07-14 00:10:45 +00:00
try {
2023-02-20 17:40:08 +00:00
if ( ! referenceId ) {
await prisma . workflowReminder . delete ( {
where : {
id : reminderId ,
} ,
} ) ;
return ;
}
2023-02-13 12:39:06 +00:00
2023-02-20 17:40:08 +00:00
await prisma . workflowReminder . update ( {
where : {
id : reminderId ,
} ,
data : {
cancelled : true ,
} ,
2023-02-13 12:39:06 +00:00
} ) ;
2022-07-14 00:10:45 +00:00
} catch ( error ) {
console . log ( ` Error canceling reminder with error ${ error } ` ) ;
}
} ;