diff --git a/apps/web/lib/emails/email-manager.ts b/apps/web/lib/emails/email-manager.ts index ad145c30f5..195398f1a8 100644 --- a/apps/web/lib/emails/email-manager.ts +++ b/apps/web/lib/emails/email-manager.ts @@ -1,6 +1,7 @@ import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email"; import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email"; import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email"; +import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email"; import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email"; import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-email"; import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email"; @@ -11,7 +12,7 @@ import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-reque import OrganizerRescheduledEmail from "@lib/emails/templates/organizer-rescheduled-email"; import OrganizerScheduledEmail from "@lib/emails/templates/organizer-scheduled-email"; import TeamInviteEmail, { TeamInvite } from "@lib/emails/templates/team-invite-email"; -import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar"; +import { CalendarEvent, Person } from "@lib/integrations/calendar/interfaces/Calendar"; export const sendScheduledEmails = async (calEvent: CalendarEvent) => { const emailsToSend: Promise[] = []; @@ -84,6 +85,17 @@ export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => { }); }; +export const sendAttendeeRequestEmail = async (calEvent: CalendarEvent, attendee: Person) => { + await new Promise((resolve, reject) => { + try { + const attendeeRequestEmail = new AttendeeRequestEmail(calEvent, attendee); + resolve(attendeeRequestEmail.sendEmail()); + } catch (e) { + reject(console.error("AttendRequestEmail.sendEmail failed", e)); + } + }); +}; + export const sendDeclinedEmails = async (calEvent: CalendarEvent) => { const emailsToSend: Promise[] = []; diff --git a/apps/web/lib/emails/templates/attendee-request-email.ts b/apps/web/lib/emails/templates/attendee-request-email.ts new file mode 100644 index 0000000000..9c279ecf99 --- /dev/null +++ b/apps/web/lib/emails/templates/attendee-request-email.ts @@ -0,0 +1,134 @@ +import dayjs from "dayjs"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import timezone from "dayjs/plugin/timezone"; +import toArray from "dayjs/plugin/toArray"; +import utc from "dayjs/plugin/utc"; + +import AttendeeScheduledEmail from "./attendee-scheduled-email"; +import { + emailHead, + emailSchedulingBodyHeader, + emailBodyLogo, + emailScheduledBodyHeaderContent, + emailSchedulingBodyDivider, +} from "./common"; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(localizedFormat); +dayjs.extend(toArray); + +export default class AttendeeRequestEmail extends AttendeeScheduledEmail { + protected getNodeMailerPayload(): Record { + const toAddresses = [this.calEvent.attendees[0].email]; + if (this.calEvent.team) { + this.calEvent.team.members.forEach((member) => { + const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); + if (memberAttendee) { + toAddresses.push(memberAttendee.email); + } + }); + } + + return { + from: `Cal.com <${this.getMailerOptions().from}>`, + to: toAddresses.join(","), + subject: `${this.calEvent.organizer.language.translate("booking_submitted_subject", { + eventType: this.calEvent.type, + name: this.calEvent.attendees[0].name, + date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( + "h:mma" + )}, ${this.calEvent.organizer.language.translate( + this.getInviteeStart().format("dddd").toLowerCase() + )}, ${this.calEvent.organizer.language.translate( + this.getInviteeStart().format("MMMM").toLowerCase() + )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, + })}`, + html: this.getHtmlBody(), + text: this.getTextBody(), + }; + } + + protected getTextBody(): string { + return ` +${this.calEvent.attendees[0].language.translate("booking_submitted", { + name: this.calEvent.attendees[0].name, +})} +${this.calEvent.attendees[0].language.translate("user_needs_to_confirm_or_reject_booking", { + user: this.calEvent.attendees[0].name, +})} +${this.getWhat()} +${this.getWhen()} +${this.getLocation()} +${this.getAdditionalNotes()} +`.replace(/(<([^>]+)>)/gi, ""); + } + + protected getHtmlBody(): string { + const headerContent = this.calEvent.attendees[0].language.translate("booking_submitted_subject", { + eventType: this.calEvent.type, + name: this.calEvent.attendees[0].name, + date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( + "h:mma" + )}, ${this.calEvent.attendees[0].language.translate( + this.getInviteeStart().format("dddd").toLowerCase() + )}, ${this.calEvent.attendees[0].language.translate( + this.getInviteeStart().format("MMMM").toLowerCase() + )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, + }); + + return ` + + + ${emailHead(headerContent)} + + +
+ ${emailSchedulingBodyHeader("calendarCircle")} + ${emailScheduledBodyHeaderContent( + this.calEvent.organizer.language.translate("booking_submitted"), + this.calEvent.organizer.language.translate("user_needs_to_confirm_or_reject_booking", { + user: this.calEvent.attendees[0].name, + }) + )} + ${emailSchedulingBodyDivider()} + +
+ + + + + + +
+ +
+ + + + + + +
+
+ ${this.getWhat()} + ${this.getWhen()} + ${this.getWho()} + ${this.getLocation()} + ${this.getAdditionalNotes()} +
+
+
+ +
+
+ ${emailSchedulingBodyDivider()} + + ${emailBodyLogo()} + +
+ + + `; + } +} diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index f6c8f59846..68f6eb13ad 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -15,6 +15,7 @@ import { sendScheduledEmails, sendRescheduledEmails, sendOrganizerRequestEmail, + sendAttendeeRequestEmail, } from "@lib/emails/email-manager"; import { ensureArray } from "@lib/ensureArray"; import { getErrorFromUnknown } from "@lib/errors"; @@ -586,6 +587,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (eventType.requiresConfirmation && !rescheduleUid) { await sendOrganizerRequestEmail(evt); + await sendAttendeeRequestEmail(evt, attendeesList[0]); } if (typeof eventType.price === "number" && eventType.price > 0) { diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 6814e243ec..6774c69ff2 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -60,6 +60,7 @@ "password_reset_instructions": "If you didn't request this, you can safely ignore this email and your password will not be changed.", "event_awaiting_approval_subject": "Awaiting Approval: {{eventType}} with {{name}} at {{date}}", "event_still_awaiting_approval": "An event is still waiting for your approval", + "booking_submitted_subject": "Booking Submitted: {{eventType}} with {{name}} at {{date}}", "your_meeting_has_been_booked": "Your meeting has been booked", "event_type_has_been_rescheduled_on_time_date": "Your {{eventType}} with {{name}} has been rescheduled to {{time}} ({{timeZone}}) on {{date}}.", "event_has_been_rescheduled": "Updated - Your event has been rescheduled",