From 15d5333cf6d4da13c57c2aa00cb1b35a6986670e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Mon, 6 Jun 2022 11:49:56 -0600 Subject: [PATCH] Refactor emails to use JSX as templating engine (#2915) * Init Maizzle * Initial template JSX conversion and testing * WIP * WIP * WIP * WIP * WIP * Migrated AttendeeRescheduledEmail * WIP * WIP * DRY * Cleanup * Cleanup * Cleanup * Migrate feedback email * Migrates ForgotPasswordEmail * Migrates OrganizerCancelledEmail * Migrated OrganizerLocationChangeEmail * Formatting * Migrated AttendeeRequestRescheduledEmail * Migrates OrganizerPaymentRefundFailedEmail * Migrates OrganizerRequestEmail * Migrates OrganizerRequestReminderEmail * Fixes type-check * Moved email-manager to package * Import fixes * Removed duplicate email code from vital app * Removed duplicate email code from wipemycal * Build/type fixes * Fixes web email imports * Fixes build * Embed build fixes * Update AttendeeAwaitingPaymentEmail.tsx * Update default-cookies.ts * Revert "Embed build fixes" This reverts commit 8d693e99aca6dfe92d5cbb27ffa962545c3e9389. * Embed build fixes # Conflicts: # packages/embeds/embed-core/package.json * dep and email date fixes * Update attendee-scheduled-email.ts * Update package.json * Update [...nextauth].tsx * Update email.ts * Prevents /api/email on production builds Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/ee/lib/stripe/server.ts | 9 +- .../api/integrations/stripepayment/webhook.ts | 2 +- .../attendee-awaiting-payment-email.ts | 173 ------- .../templates/attendee-cancelled-email.ts | 144 ------ .../templates/attendee-declined-email.ts | 139 ------ .../attendee-location-change-email.ts | 165 ------- .../templates/attendee-request-email.ts | 145 ------ .../attendee-request-reschedule-email.ts | 212 -------- .../templates/attendee-rescheduled-email.ts | 170 ------- .../templates/attendee-scheduled-email.ts | 463 ------------------ .../lib/emails/templates/common/body-logo.ts | 44 -- .../templates/common/scheduling-body-head.ts | 71 --- .../lib/emails/templates/feedback-email.ts | 118 ----- .../emails/templates/forgot-password-email.ts | 189 ------- .../templates/organizer-cancelled-email.ts | 152 ------ .../organizer-location-change-email.ts | 162 ------ .../organizer-payment-refund-failed-email.ts | 207 -------- .../templates/organizer-request-email.ts | 178 ------- .../organizer-request-reminder-email.ts | 176 ------- .../organizer-request-reschedule-email.ts | 192 -------- .../templates/organizer-rescheduled-email.ts | 166 ------- .../templates/organizer-scheduled-email.ts | 450 ----------------- .../lib/emails/templates/team-invite-email.ts | 179 ------- apps/web/next.config.js | 1 + apps/web/pages/api/auth/[...nextauth].tsx | 2 +- apps/web/pages/api/auth/forgot-password.ts | 11 +- apps/web/pages/api/book/confirm.ts | 2 +- apps/web/pages/api/book/event.ts | 12 +- apps/web/pages/api/book/request-reschedule.ts | 2 +- apps/web/pages/api/cancel.ts | 2 +- apps/web/pages/api/cron/bookingReminder.ts | 3 +- apps/web/pages/api/email.ts | 58 +++ apps/web/pages/api/teams/[team]/invite.ts | 5 +- apps/web/server/routers/viewer.tsx | 2 +- apps/web/server/routers/viewer/bookings.tsx | 3 +- apps/web/server/routers/viewer/teams.tsx | 26 +- packages/app-store/vital/lib/emailManager.ts | 35 -- .../app-store/vital/lib/emailServerConfig.ts | 34 -- packages/app-store/vital/lib/reschedule.ts | 4 +- .../attendee-request-reschedule-email.ts | 210 -------- .../vital/lib/templates/base-template.ts | 417 ---------------- .../vital/lib/templates/common/body-logo.ts | 44 -- .../vital/lib/templates/common/head.ts | 91 ---- .../vital/lib/templates/common/index.ts | 6 - .../vital/lib/templates/common/link-icon.ts | 5 - .../common/scheduling-body-divider.ts | 31 -- .../common/scheduling-body-head-content.ts | 33 -- .../templates/common/scheduling-body-head.ts | 71 --- .../organizer-request-reschedule-email.ts | 190 ------- .../wipemycalother/lib/emailManager.ts | 35 -- .../wipemycalother/lib/emailServerConfig.ts | 34 -- .../wipemycalother/lib/reschedule.ts | 2 +- .../attendee-request-reschedule-email.ts | 210 -------- .../lib/templates/base-template.ts | 417 ---------------- .../lib/templates/common/head.ts | 91 ---- .../lib/templates/common/index.ts | 6 - .../lib/templates/common/link-icon.ts | 5 - .../common/scheduling-body-divider.ts | 31 -- .../common/scheduling-body-head-content.ts | 33 -- .../organizer-request-reschedule-email.ts | 190 ------- packages/emails/README.md | 24 + .../lib => packages}/emails/email-manager.ts | 38 +- packages/emails/index.ts | 2 + packages/emails/package.json | 15 + .../emails/src/components/BaseEmailHtml.tsx | 188 +++++++ packages/emails/src/components/BaseTable.tsx | 12 + .../emails/src/components/CallToAction.tsx | 30 ++ .../src/components/CallToActionTable.tsx | 24 + .../emails/src/components/CustomInputs.tsx | 18 + .../emails/src/components/EmailBodyLogo.tsx | 81 +++ .../src/components/EmailCommonDivider.tsx | 71 +++ packages/emails/src/components/EmailHead.tsx | 93 ++++ .../EmailScheduledBodyHeaderContent.tsx | 48 ++ .../components/EmailSchedulingBodyDivider.tsx | 28 ++ .../components/EmailSchedulingBodyHeader.tsx | 63 +++ packages/emails/src/components/Info.tsx | 30 ++ packages/emails/src/components/LinkIcon.tsx | 9 + .../emails/src/components/LocationInfo.tsx | 124 +++++ packages/emails/src/components/ManageLink.tsx | 30 ++ packages/emails/src/components/RawHtml.tsx | 5 + packages/emails/src/components/Row.tsx | 15 + packages/emails/src/components/WhenInfo.tsx | 57 +++ packages/emails/src/components/WhoInfo.tsx | 41 ++ packages/emails/src/components/index.ts | 11 + packages/emails/src/renderEmail.ts | 23 + .../AttendeeAwaitingPaymentEmail.tsx | 24 + .../src/templates/AttendeeCancelledEmail.tsx | 11 + .../src/templates/AttendeeDeclinedEmail.tsx | 11 + .../templates/AttendeeLocationChangeEmail.tsx | 10 + .../src/templates/AttendeeRequestEmail.tsx | 18 + .../AttendeeRequestRescheduledEmail.tsx | 21 + .../templates/AttendeeRescheduledEmail.tsx | 10 + .../src/templates/AttendeeScheduledEmail.tsx | 15 + .../src/templates/BaseScheduledEmail.tsx | 80 +++ .../emails/src/templates/FeedbackEmail.tsx | 17 + .../src/templates/ForgotPasswordEmail.tsx | 43 ++ .../src/templates/OrganizerCancelledEmail.tsx | 11 + .../OrganizerLocationChangeEmail.tsx | 11 + .../OrganizerPaymentRefundFailedEmail.tsx | 67 +++ .../src/templates/OrganizerRequestEmail.tsx | 24 + .../OrganizerRequestReminderEmail.tsx | 5 + .../OrganizerRequestRescheduledEmail.tsx | 17 + .../templates/OrganizerRescheduledEmail.tsx | 10 + .../src/templates/OrganizerScheduledEmail.tsx | 22 + .../emails/src/templates/TeamInviteEmail.tsx | 40 ++ packages/emails/src/templates/index.ts | 19 + packages/emails/tailwind.config.js | 143 ++++++ .../emails/templates/_base-email.ts | 22 + .../attendee-awaiting-payment-email.ts | 23 + .../templates/attendee-cancelled-email.ts | 23 + .../templates/attendee-declined-email.ts | 25 + .../attendee-location-change-email.ts | 27 + .../templates/attendee-request-email.ts | 39 ++ .../attendee-request-reschedule-email.ts | 105 ++++ .../templates/attendee-rescheduled-email.ts | 27 + .../templates/attendee-scheduled-email.ts | 125 +++++ .../templates/common/body-logo.ts | 2 +- .../emails/templates/common/head.ts | 0 .../emails/templates/common/index.ts | 0 .../emails/templates/common/link-icon.ts | 2 +- .../common/scheduling-body-divider.ts | 0 .../common/scheduling-body-head-content.ts | 0 .../templates/common/scheduling-body-head.ts | 2 +- .../emails/templates/confirm-email.html | 202 ++++---- packages/emails/templates/feedback-email.ts | 36 ++ .../emails/templates/forgot-password-email.ts | 49 ++ .../templates/organizer-cancelled-email.ts | 32 ++ .../organizer-location-change-email.ts | 36 ++ .../organizer-payment-refund-failed-email.ts | 43 ++ .../templates/organizer-request-email.ts | 42 ++ .../organizer-request-reminder-email.ts | 32 ++ .../organizer-request-reschedule-email.ts | 91 ++++ .../templates/organizer-rescheduled-email.ts | 42 ++ .../templates/organizer-scheduled-email.ts | 132 +++++ .../emails/templates/team-invite-email.ts | 35 ++ packages/lib/CalEventParser.ts | 52 +- packages/types/Calendar.d.ts | 6 + 137 files changed, 2919 insertions(+), 6302 deletions(-) delete mode 100644 apps/web/lib/emails/templates/attendee-awaiting-payment-email.ts delete mode 100644 apps/web/lib/emails/templates/attendee-cancelled-email.ts delete mode 100644 apps/web/lib/emails/templates/attendee-declined-email.ts delete mode 100644 apps/web/lib/emails/templates/attendee-location-change-email.ts delete mode 100644 apps/web/lib/emails/templates/attendee-request-email.ts delete mode 100644 apps/web/lib/emails/templates/attendee-request-reschedule-email.ts delete mode 100644 apps/web/lib/emails/templates/attendee-rescheduled-email.ts delete mode 100644 apps/web/lib/emails/templates/attendee-scheduled-email.ts delete mode 100644 apps/web/lib/emails/templates/common/body-logo.ts delete mode 100644 apps/web/lib/emails/templates/common/scheduling-body-head.ts delete mode 100644 apps/web/lib/emails/templates/feedback-email.ts delete mode 100644 apps/web/lib/emails/templates/forgot-password-email.ts delete mode 100644 apps/web/lib/emails/templates/organizer-cancelled-email.ts delete mode 100644 apps/web/lib/emails/templates/organizer-location-change-email.ts delete mode 100644 apps/web/lib/emails/templates/organizer-payment-refund-failed-email.ts delete mode 100644 apps/web/lib/emails/templates/organizer-request-email.ts delete mode 100644 apps/web/lib/emails/templates/organizer-request-reminder-email.ts delete mode 100644 apps/web/lib/emails/templates/organizer-request-reschedule-email.ts delete mode 100644 apps/web/lib/emails/templates/organizer-rescheduled-email.ts delete mode 100644 apps/web/lib/emails/templates/organizer-scheduled-email.ts delete mode 100644 apps/web/lib/emails/templates/team-invite-email.ts create mode 100644 apps/web/pages/api/email.ts delete mode 100644 packages/app-store/vital/lib/emailManager.ts delete mode 100644 packages/app-store/vital/lib/emailServerConfig.ts delete mode 100644 packages/app-store/vital/lib/templates/attendee-request-reschedule-email.ts delete mode 100644 packages/app-store/vital/lib/templates/base-template.ts delete mode 100644 packages/app-store/vital/lib/templates/common/body-logo.ts delete mode 100644 packages/app-store/vital/lib/templates/common/head.ts delete mode 100644 packages/app-store/vital/lib/templates/common/index.ts delete mode 100644 packages/app-store/vital/lib/templates/common/link-icon.ts delete mode 100644 packages/app-store/vital/lib/templates/common/scheduling-body-divider.ts delete mode 100644 packages/app-store/vital/lib/templates/common/scheduling-body-head-content.ts delete mode 100644 packages/app-store/vital/lib/templates/common/scheduling-body-head.ts delete mode 100644 packages/app-store/vital/lib/templates/organizer-request-reschedule-email.ts delete mode 100644 packages/app-store/wipemycalother/lib/emailManager.ts delete mode 100644 packages/app-store/wipemycalother/lib/emailServerConfig.ts delete mode 100644 packages/app-store/wipemycalother/lib/templates/attendee-request-reschedule-email.ts delete mode 100644 packages/app-store/wipemycalother/lib/templates/base-template.ts delete mode 100644 packages/app-store/wipemycalother/lib/templates/common/head.ts delete mode 100644 packages/app-store/wipemycalother/lib/templates/common/index.ts delete mode 100644 packages/app-store/wipemycalother/lib/templates/common/link-icon.ts delete mode 100644 packages/app-store/wipemycalother/lib/templates/common/scheduling-body-divider.ts delete mode 100644 packages/app-store/wipemycalother/lib/templates/common/scheduling-body-head-content.ts delete mode 100644 packages/app-store/wipemycalother/lib/templates/organizer-request-reschedule-email.ts create mode 100644 packages/emails/README.md rename {apps/web/lib => packages}/emails/email-manager.ts (83%) create mode 100644 packages/emails/index.ts create mode 100644 packages/emails/package.json create mode 100644 packages/emails/src/components/BaseEmailHtml.tsx create mode 100644 packages/emails/src/components/BaseTable.tsx create mode 100644 packages/emails/src/components/CallToAction.tsx create mode 100644 packages/emails/src/components/CallToActionTable.tsx create mode 100644 packages/emails/src/components/CustomInputs.tsx create mode 100644 packages/emails/src/components/EmailBodyLogo.tsx create mode 100644 packages/emails/src/components/EmailCommonDivider.tsx create mode 100644 packages/emails/src/components/EmailHead.tsx create mode 100644 packages/emails/src/components/EmailScheduledBodyHeaderContent.tsx create mode 100644 packages/emails/src/components/EmailSchedulingBodyDivider.tsx create mode 100644 packages/emails/src/components/EmailSchedulingBodyHeader.tsx create mode 100644 packages/emails/src/components/Info.tsx create mode 100644 packages/emails/src/components/LinkIcon.tsx create mode 100644 packages/emails/src/components/LocationInfo.tsx create mode 100644 packages/emails/src/components/ManageLink.tsx create mode 100644 packages/emails/src/components/RawHtml.tsx create mode 100644 packages/emails/src/components/Row.tsx create mode 100644 packages/emails/src/components/WhenInfo.tsx create mode 100644 packages/emails/src/components/WhoInfo.tsx create mode 100644 packages/emails/src/components/index.ts create mode 100644 packages/emails/src/renderEmail.ts create mode 100644 packages/emails/src/templates/AttendeeAwaitingPaymentEmail.tsx create mode 100644 packages/emails/src/templates/AttendeeCancelledEmail.tsx create mode 100644 packages/emails/src/templates/AttendeeDeclinedEmail.tsx create mode 100644 packages/emails/src/templates/AttendeeLocationChangeEmail.tsx create mode 100644 packages/emails/src/templates/AttendeeRequestEmail.tsx create mode 100644 packages/emails/src/templates/AttendeeRequestRescheduledEmail.tsx create mode 100644 packages/emails/src/templates/AttendeeRescheduledEmail.tsx create mode 100644 packages/emails/src/templates/AttendeeScheduledEmail.tsx create mode 100644 packages/emails/src/templates/BaseScheduledEmail.tsx create mode 100644 packages/emails/src/templates/FeedbackEmail.tsx create mode 100644 packages/emails/src/templates/ForgotPasswordEmail.tsx create mode 100644 packages/emails/src/templates/OrganizerCancelledEmail.tsx create mode 100644 packages/emails/src/templates/OrganizerLocationChangeEmail.tsx create mode 100644 packages/emails/src/templates/OrganizerPaymentRefundFailedEmail.tsx create mode 100644 packages/emails/src/templates/OrganizerRequestEmail.tsx create mode 100644 packages/emails/src/templates/OrganizerRequestReminderEmail.tsx create mode 100644 packages/emails/src/templates/OrganizerRequestRescheduledEmail.tsx create mode 100644 packages/emails/src/templates/OrganizerRescheduledEmail.tsx create mode 100644 packages/emails/src/templates/OrganizerScheduledEmail.tsx create mode 100644 packages/emails/src/templates/TeamInviteEmail.tsx create mode 100644 packages/emails/src/templates/index.ts create mode 100644 packages/emails/tailwind.config.js rename {apps/web/lib => packages}/emails/templates/_base-email.ts (64%) create mode 100644 packages/emails/templates/attendee-awaiting-payment-email.ts create mode 100644 packages/emails/templates/attendee-cancelled-email.ts create mode 100644 packages/emails/templates/attendee-declined-email.ts create mode 100644 packages/emails/templates/attendee-location-change-email.ts create mode 100644 packages/emails/templates/attendee-request-email.ts create mode 100644 packages/emails/templates/attendee-request-reschedule-email.ts create mode 100644 packages/emails/templates/attendee-rescheduled-email.ts create mode 100644 packages/emails/templates/attendee-scheduled-email.ts rename packages/{app-store/wipemycalother/lib => emails}/templates/common/body-logo.ts (97%) rename {apps/web/lib => packages}/emails/templates/common/head.ts (100%) rename {apps/web/lib => packages}/emails/templates/common/index.ts (100%) rename {apps/web/lib => packages}/emails/templates/common/link-icon.ts (69%) rename {apps/web/lib => packages}/emails/templates/common/scheduling-body-divider.ts (100%) rename {apps/web/lib => packages}/emails/templates/common/scheduling-body-head-content.ts (100%) rename packages/{app-store/wipemycalother/lib => emails}/templates/common/scheduling-body-head.ts (98%) rename {apps/web/lib => packages}/emails/templates/confirm-email.html (69%) create mode 100644 packages/emails/templates/feedback-email.ts create mode 100644 packages/emails/templates/forgot-password-email.ts create mode 100644 packages/emails/templates/organizer-cancelled-email.ts create mode 100644 packages/emails/templates/organizer-location-change-email.ts create mode 100644 packages/emails/templates/organizer-payment-refund-failed-email.ts create mode 100644 packages/emails/templates/organizer-request-email.ts create mode 100644 packages/emails/templates/organizer-request-reminder-email.ts create mode 100644 packages/emails/templates/organizer-request-reschedule-email.ts create mode 100644 packages/emails/templates/organizer-rescheduled-email.ts create mode 100644 packages/emails/templates/organizer-scheduled-email.ts create mode 100644 packages/emails/templates/team-invite-email.ts diff --git a/apps/web/ee/lib/stripe/server.ts b/apps/web/ee/lib/stripe/server.ts index 029c8f175e..8cc083ebb4 100644 --- a/apps/web/ee/lib/stripe/server.ts +++ b/apps/web/ee/lib/stripe/server.ts @@ -4,20 +4,13 @@ import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; +import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@calcom/emails"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import prisma from "@calcom/prisma"; import { createPaymentLink } from "@calcom/stripe/client"; import stripe, { PaymentData } from "@calcom/stripe/server"; import { CalendarEvent } from "@calcom/types/Calendar"; -import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager"; - -export type PaymentInfo = { - link?: string | null; - reason?: string | null; - id?: string | null; -}; - const stripeKeysSchema = z.object({ payment_fee_fixed: z.number(), payment_fee_percentage: z.number(), diff --git a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts index ff23521921..02a39ca8d9 100644 --- a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts +++ b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts @@ -4,6 +4,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; import EventManager from "@calcom/core/EventManager"; +import { sendScheduledEmails } from "@calcom/emails"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; @@ -12,7 +13,6 @@ import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; import { IS_PRODUCTION } from "@lib/config/constants"; import { HttpError as HttpCode } from "@lib/core/http/error"; -import { sendScheduledEmails } from "@lib/emails/email-manager"; import { getTranslation } from "@server/lib/i18n"; diff --git a/apps/web/lib/emails/templates/attendee-awaiting-payment-email.ts b/apps/web/lib/emails/templates/attendee-awaiting-payment-email.ts deleted file mode 100644 index 773aba66b5..0000000000 --- a/apps/web/lib/emails/templates/attendee-awaiting-payment-email.ts +++ /dev/null @@ -1,173 +0,0 @@ -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, - linkIcon, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { - return { - to: `${this.attendee.name} <${this.attendee.email}>`, - from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, - subject: `${this.attendee.language.translate("awaiting_payment_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.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.attendee.language.translate("meeting_awaiting_payment")} -${this.attendee.language.translate("emailed_you_and_any_other_attendees")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.attendee.language.translate("awaiting_payment_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("MMMM").toLowerCase() - )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.attendee.language.translate("meeting_awaiting_payment"), - this.attendee.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - - - - -
- - ${this.getManageLink()} -
-
-
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getManageLink(): string { - const manageText = this.attendee.language.translate("pay_now"); - - if (this.calEvent.paymentInfo) { - return ` - - -

- ${manageText} -

- - - `; - } - - return ""; - } -} diff --git a/apps/web/lib/emails/templates/attendee-cancelled-email.ts b/apps/web/lib/emails/templates/attendee-cancelled-email.ts deleted file mode 100644 index f0c10d827c..0000000000 --- a/apps/web/lib/emails/templates/attendee-cancelled-email.ts +++ /dev/null @@ -1,144 +0,0 @@ -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 AttendeeCancelledEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { - return { - to: `${this.attendee.name} <${this.attendee.email}>`, - from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, - subject: `${this.attendee.language.translate("event_cancelled_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.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.attendee.language.translate("event_request_cancelled")} -${this.attendee.language.translate("emailed_you_and_any_other_attendees")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -${this.calEvent.cancellationReason && this.getCancellationReason()} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.attendee.language.translate("event_cancelled_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("MMMM").toLowerCase() - )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - - -
- ${emailSchedulingBodyHeader("xCircle")} - ${emailScheduledBodyHeaderContent( - this.attendee.language.translate("event_request_cancelled"), - this.attendee.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} - ${this.calEvent.cancellationReason && this.getCancellationReason()} -
-
-
- -
-
- -
- - - - - - -
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getCancellationReason(): string { - return ` -

-
-

${this.attendee.language.translate("cancellation_reason")}

-

${this.calEvent.cancellationReason}

-
`; - } -} diff --git a/apps/web/lib/emails/templates/attendee-declined-email.ts b/apps/web/lib/emails/templates/attendee-declined-email.ts deleted file mode 100644 index 41fb6184d1..0000000000 --- a/apps/web/lib/emails/templates/attendee-declined-email.ts +++ /dev/null @@ -1,139 +0,0 @@ -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 AttendeeDeclinedEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { - return { - to: `${this.attendee.name} <${this.attendee.email}>`, - from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, - subject: `${this.attendee.language.translate("event_declined_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.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.attendee.language.translate( - this.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined" -)} -${this.attendee.language.translate("emailed_you_and_any_other_attendees")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -${this.getRejectionReason()} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.attendee.language.translate("event_declined_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("MMMM").toLowerCase() - )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - - -
- ${emailSchedulingBodyHeader("xCircle")} - ${emailScheduledBodyHeaderContent( - this.attendee.language.translate( - this.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined" - ), - this.attendee.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} - ${this.getRejectionReason()} -
-
-
- -
-
- -
- - - - - - -
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/attendee-location-change-email.ts b/apps/web/lib/emails/templates/attendee-location-change-email.ts deleted file mode 100644 index 611ebe269d..0000000000 --- a/apps/web/lib/emails/templates/attendee-location-change-email.ts +++ /dev/null @@ -1,165 +0,0 @@ -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 { getCancelLink } from "@calcom/lib/CalEventParser"; - -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 AttendeeLocationChangeEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - to: `${this.attendee.name} <${this.attendee.email}>`, - from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, - subject: `${this.attendee.language.translate("location_changed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("MMMM").toLowerCase() - )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - protected getTextBody(): string { - // Only the original attendee can make changes to the event - // Guests cannot - if (this.attendee === this.calEvent.attendees[0]) { - return ` - ${this.attendee.language.translate("event_location_changed")} - ${this.attendee.language.translate("emailed_you_and_any_other_attendees")} - ${this.getWhat()} - ${this.getWhen()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.attendee.language.translate("need_to_reschedule_or_cancel")} - ${getCancelLink(this.calEvent)} - `.replace(/(<([^>]+)>)/gi, ""); - } - - return ` -${this.attendee.language.translate("event_location_changed")} -${this.attendee.language.translate("emailed_you_and_any_other_attendees")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getAdditionalNotes()} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.attendee.language.translate("location_changed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("MMMM").toLowerCase() - )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.attendee.language.translate("event_location_changed"), - this.attendee.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/attendee-request-email.ts b/apps/web/lib/emails/templates/attendee-request-email.ts deleted file mode 100644 index c2301c71a5..0000000000 --- a/apps/web/lib/emails/templates/attendee-request-email.ts +++ /dev/null @@ -1,145 +0,0 @@ -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.organizer.name, -})} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -`.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( - this.recurringEvent?.count ? "booking_submitted_recurring" : "booking_submitted" - ), - this.calEvent.organizer.language.translate( - this.recurringEvent.count - ? "user_needs_to_confirm_or_reject_booking_recurring" - : "user_needs_to_confirm_or_reject_booking", - { - user: this.calEvent.organizer.name, - } - ) - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - - ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/attendee-request-reschedule-email.ts b/apps/web/lib/emails/templates/attendee-request-reschedule-email.ts deleted file mode 100644 index 58a5c9e553..0000000000 --- a/apps/web/lib/emails/templates/attendee-request-reschedule-email.ts +++ /dev/null @@ -1,212 +0,0 @@ -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 { createEvent, DateArray, Person } from "ics"; - -import { getCancelLink } from "@calcom/lib/CalEventParser"; -import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; - -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; -import OrganizerScheduledEmail from "./organizer-scheduled-email"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class AttendeeRequestRescheduledEmail extends OrganizerScheduledEmail { - private metadata: { rescheduleLink: string }; - constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }, recurringEvent: RecurringEvent) { - super(calEvent, recurringEvent); - this.metadata = metadata; - } - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.attendees[0].email]; - - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("requested_to_reschedule_subject_attendee", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - // @OVERRIDE - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CANCELLED", - method: "CANCEL", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - // @OVERRIDE - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}

-

- ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("request_reschedule_title_attendee")} -${this.calEvent.organizer.language.translate("request_reschedule_subtitle", { - organizer: this.calEvent.organizer.name, -})}, -${this.calEvent.cancellationReason && this.getReason()} -${this.getWhat()} -${this.getWhen()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("request_reschedule_title_attendee"), - this.calEvent.organizer.language.translate("request_reschedule_subtitle", { - organizer: this.calEvent.organizer.name, - }) - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
- -
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/attendee-rescheduled-email.ts b/apps/web/lib/emails/templates/attendee-rescheduled-email.ts deleted file mode 100644 index 1ad39fec14..0000000000 --- a/apps/web/lib/emails/templates/attendee-rescheduled-email.ts +++ /dev/null @@ -1,170 +0,0 @@ -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 { getCancelLink } from "@calcom/lib/CalEventParser"; - -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 AttendeeRescheduledEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - to: `${this.attendee.name} <${this.attendee.email}>`, - from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, - subject: `${this.attendee.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("MMMM").toLowerCase() - )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - protected getTextBody(): string { - // Only the original attendee can make changes to the event - // Guests cannot - if (this.attendee === this.calEvent.attendees[0]) { - return ` - ${this.attendee.language.translate("event_has_been_rescheduled")} - ${this.attendee.language.translate("emailed_you_and_any_other_attendees")} - ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} - ${this.attendee.language.translate("need_to_reschedule_or_cancel")} - ${getCancelLink(this.calEvent)} - `.replace(/(<([^>]+)>)/gi, ""); - } - - return ` -${this.attendee.language.translate("event_has_been_rescheduled")} -${this.attendee.language.translate("emailed_you_and_any_other_attendees")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.attendee.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.name, - date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("dddd").toLowerCase() - )}, ${this.attendee.language.translate( - this.getInviteeStart().format("MMMM").toLowerCase() - )} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.attendee.language.translate("event_has_been_rescheduled"), - this.attendee.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/attendee-scheduled-email.ts b/apps/web/lib/emails/templates/attendee-scheduled-email.ts deleted file mode 100644 index 68671b275d..0000000000 --- a/apps/web/lib/emails/templates/attendee-scheduled-email.ts +++ /dev/null @@ -1,463 +0,0 @@ -import dayjs, { 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 { createEvent, DateArray } from "ics"; -import rrule from "rrule"; - -import { getAppName } from "@calcom/app-store/utils"; -import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser"; -import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calendar"; - -import BaseEmail from "@lib/emails/templates/_base-email"; - -import { - emailBodyLogo, - emailHead, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, - emailSchedulingBodyHeader, - linkIcon, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class AttendeeScheduledEmail extends BaseEmail { - calEvent: CalendarEvent; - attendee: Person; - recurringEvent: RecurringEvent; - - constructor(calEvent: CalendarEvent, attendee: Person, recurringEvent: RecurringEvent) { - super(); - this.name = "SEND_BOOKING_CONFIRMATION"; - this.calEvent = calEvent; - this.attendee = attendee; - this.recurringEvent = recurringEvent; - } - - protected getiCalEventAsString(): string | undefined { - // Taking care of recurrence rule beforehand - let recurrenceRule: string | undefined = undefined; - if (this.recurringEvent?.count) { - recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", ""); - } - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.attendees[0].language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - ...{ recurrenceRule }, - status: "CONFIRMED", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - - protected getNodeMailerPayload(): Record { - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - to: `${this.attendee.name} <${this.attendee.email}>`, - from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, - subject: `${this.calEvent.attendees[0].language.translate("confirmed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.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")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getDescription(): string { - if (!this.calEvent.description) return ""; - return ` -

-
-

${this.calEvent.organizer.language.translate("description")}

-

${ - this.calEvent.description - }

-
- `; - } - protected getTextBody(): string { - return ` -${this.calEvent.attendees[0].language.translate( - this.recurringEvent?.count ? "your_event_has_been_scheduled_recurring" : "your_event_has_been_scheduled" -)} -${this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")} - -${getRichDescription(this.calEvent)} -`.trim(); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.attendees[0].language.translate("confirmed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.team?.name || this.calEvent.organizer.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("checkCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.attendees[0].language.translate( - this.recurringEvent?.count - ? "your_event_has_been_scheduled_recurring" - : "your_event_has_been_scheduled" - ), - this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getManageLink(): string { - // Only the original attendee can make changes to the event - // Guests cannot - if (this.attendee === this.calEvent.attendees[0]) { - const manageText = this.calEvent.attendees[0].language.translate("manage_this_event"); - return `

${this.calEvent.attendees[0].language.translate( - "need_to_reschedule_or_cancel" - )}

${manageText}

`; - } - - return ""; - } - - protected getWhat(): string { - return ` -
-

${this.calEvent.attendees[0].language.translate("what")}

-

${this.calEvent.type}

-
`; - } - - protected getRecurringWhen(): string { - if (this.recurringEvent?.freq) { - return ` - ${this.calEvent.attendees[0].language.translate("every_for_freq", { - freq: this.calEvent.attendees[0].language.translate( - `${rrule.FREQUENCIES[this.recurringEvent.freq].toString().toLowerCase()}` - ), - })} ${this.recurringEvent.count} ${this.calEvent.attendees[0].language.translate( - `${rrule.FREQUENCIES[this.recurringEvent.freq].toString().toLowerCase()}`, - { count: this.recurringEvent.count } - )}`; - } else { - return ""; - } - } - - protected getWhen(): string { - return ` -

-
-

${this.calEvent.attendees[0].language.translate("when")}${ - this.recurringEvent?.count ? this.getRecurringWhen() : "" - }

-

- ${this.recurringEvent?.count ? `${this.calEvent.attendees[0].language.translate("starting")} ` : ""} - ${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" - )} | ${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getWho(): string { - const attendees = this.calEvent.attendees - .map((attendee) => { - return `
${ - attendee?.name || `${this.calEvent.attendees[0].language.translate("guest")}` - } ${ - attendee.email - }
`; - }) - .join(""); - - const organizer = `
${ - this.calEvent.organizer.name - } - ${this.calEvent.attendees[0].language.translate( - "organizer" - )} ${this.calEvent.organizer.email}
`; - - return ` -

-
-

${this.calEvent.attendees[0].language.translate("who")}

- ${organizer + attendees} -
`; - } - - protected getAdditionalNotes(): string { - if (!this.calEvent.additionalNotes) return ""; - return ` -

-
-

${this.calEvent.attendees[0].language.translate("additional_notes")}

-

${ - this.calEvent.additionalNotes - }

-
- `; - } - - protected getCustomInputs(): string { - const { customInputs } = this.calEvent; - if (!customInputs) return ""; - const customInputsString = Object.keys(customInputs) - .map((key) => { - if (customInputs[key] !== "") { - return ` -

-
-

${key}

-

- ${customInputs[key]} -

-
- `; - } - }) - .join(""); - - return customInputsString; - } - - protected getRejectionReason(): string { - if (!this.calEvent.rejectionReason) return ""; - return ` -

-
-

${this.calEvent.attendees[0].language.translate("rejection_reason")}

-

${this.calEvent.rejectionReason}

-
`; - } - - protected getLocation(): string { - let providerName = this.calEvent.location && getAppName(this.calEvent.location); - - if (this.calEvent.location && this.calEvent.location.includes("integrations:")) { - const location = this.calEvent.location.split(":")[1]; - providerName = location[0].toUpperCase() + location.slice(1); - } - - // If location its a url, probably we should be validating it with a custom library - if (this.calEvent.location && /^https?:\/\//.test(this.calEvent.location)) { - providerName = this.calEvent.location; - } - - if (this.calEvent.videoCallData) { - const meetingId = this.calEvent.videoCallData.id; - const meetingPassword = this.calEvent.videoCallData.password; - const meetingUrl = this.calEvent.videoCallData.url; - - return ` -

-
-

${this.calEvent.attendees[0].language.translate("where")}

-

${providerName} ${ - meetingUrl && - `` - }

- ${ - meetingId && - `
${this.calEvent.attendees[0].language.translate( - "meeting_id" - )}: ${meetingId}
` - } - ${ - meetingPassword && - `
${this.calEvent.attendees[0].language.translate( - "meeting_password" - )}: ${meetingPassword}
` - } - ${ - meetingUrl && - `
${this.calEvent.attendees[0].language.translate( - "meeting_url" - )}: ${meetingUrl}
` - } -
- `; - } - - if (this.calEvent.additionInformation?.hangoutLink) { - const hangoutLink: string = this.calEvent.additionInformation.hangoutLink; - - return ` -

-
-

${this.calEvent.attendees[0].language.translate("where")}

-

${providerName} ${ - hangoutLink && - `` - }

- -
- `; - } - - return ` -

-
-

${this.calEvent.attendees[0].language.translate("where")}

-

${ - providerName || this.calEvent.location - }

- ${ - providerName === "Zoom" || providerName === "Google" - ? `

- ${this.calEvent.organizer.language.translate("meeting_url_provided_after_confirmed")} -

` - : `` - } -
- `; - } - - protected getTimezone(): string { - // Timezone is based on the first attendee in the attendee list - // as the first attendee is the one who created the booking - return this.calEvent.attendees[0].timeZone; - } - - protected getInviteeStart(): Dayjs { - return dayjs(this.calEvent.startTime).tz(this.getTimezone()); - } - - protected getInviteeEnd(): Dayjs { - return dayjs(this.calEvent.endTime).tz(this.getTimezone()); - } - - protected getReason(): string { - return ` -
-

${this.calEvent.attendees[0].language.translate("reschedule_reason")}

-

${this.calEvent.cancellationReason}

-
`; - } -} diff --git a/apps/web/lib/emails/templates/common/body-logo.ts b/apps/web/lib/emails/templates/common/body-logo.ts deleted file mode 100644 index 3b5b143e8a..0000000000 --- a/apps/web/lib/emails/templates/common/body-logo.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants"; - -export const emailBodyLogo = (): string => { - const image = IS_PRODUCTION - ? BASE_URL + "/emails/CalLogo@2x.png" - : "https://app.cal.com/emails/CalLogo@2x.png"; - - return ` - -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- - - -
-
-
- -
-
- `; -}; diff --git a/apps/web/lib/emails/templates/common/scheduling-body-head.ts b/apps/web/lib/emails/templates/common/scheduling-body-head.ts deleted file mode 100644 index fc715d2a26..0000000000 --- a/apps/web/lib/emails/templates/common/scheduling-body-head.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants"; - -export type BodyHeadType = "checkCircle" | "xCircle" | "calendarCircle"; - -export const getHeadImage = (headerType: BodyHeadType): string => { - switch (headerType) { - case "checkCircle": - return IS_PRODUCTION - ? BASE_URL + "/emails/checkCircle@2x.png" - : "https://app.cal.com/emails/checkCircle@2x.png"; - case "xCircle": - return IS_PRODUCTION - ? BASE_URL + "/emails/xCircle@2x.png" - : "https://app.cal.com/emails/xCircle@2x.png"; - case "calendarCircle": - return IS_PRODUCTION - ? BASE_URL + "/emails/calendarCircle@2x.png" - : "https://app.cal.com/emails/calendarCircle@2x.png"; - } -}; - -export const emailSchedulingBodyHeader = (headerType: BodyHeadType): string => { - const image = getHeadImage(headerType); - - return ` - -
- - - - - - -
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- -
-
-
- -
-
- `; -}; diff --git a/apps/web/lib/emails/templates/feedback-email.ts b/apps/web/lib/emails/templates/feedback-email.ts deleted file mode 100644 index b39a6f3ef2..0000000000 --- a/apps/web/lib/emails/templates/feedback-email.ts +++ /dev/null @@ -1,118 +0,0 @@ -import BaseEmail from "@lib/emails/templates/_base-email"; - -import { emailHead, emailBodyLogo } from "./common"; - -export interface Feedback { - userId: number; - rating: string; - comment: string; -} - -export default class FeedbackEmail extends BaseEmail { - feedback: Feedback; - - constructor(feedback: Feedback) { - super(); - this.feedback = feedback; - } - - protected getNodeMailerPayload(): Record { - return { - from: `Cal.com <${this.getMailerOptions().from}>`, - to: process.env.SEND_FEEDBACK_EMAIL, - subject: `User Feedback`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` - userId: ${this.feedback.userId} - rating: ${this.feedback.rating} - comment: ${this.feedback.comment} - `; - } - - protected getHtmlBody(): string { - return ` - - - ${emailHead("Feedback")} - - -
- - - - - - -
- -
- - - - - - -
-
Feedback
-
-
- -
-
-
- -
- - - - - - -
- -
- - - - - - -
-
-
-

Used id

-

${ - this.feedback.userId - }

-
-
-

Rating

-

${ - this.feedback.rating - }

-
-
-

Comment

-

${ - this.feedback.comment - }

-
-
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/forgot-password-email.ts b/apps/web/lib/emails/templates/forgot-password-email.ts deleted file mode 100644 index cd5c0f2d59..0000000000 --- a/apps/web/lib/emails/templates/forgot-password-email.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { TFunction } from "next-i18next"; - -import BaseEmail from "@lib/emails/templates/_base-email"; - -import { emailBodyLogo, emailHead, linkIcon } from "./common"; - -export type PasswordReset = { - language: TFunction; - user: { - name?: string | null; - email: string; - }; - resetLink: string; -}; - -export const PASSWORD_RESET_EXPIRY_HOURS = 6; - -export default class ForgotPasswordEmail extends BaseEmail { - passwordEvent: PasswordReset; - - constructor(passwordEvent: PasswordReset) { - super(); - this.name = "SEND_PASSWORD_RESET_EMAIL"; - this.passwordEvent = passwordEvent; - } - - protected getNodeMailerPayload(): Record { - return { - to: `${this.passwordEvent.user.name} <${this.passwordEvent.user.email}>`, - from: `Cal.com <${this.getMailerOptions().from}>`, - subject: this.passwordEvent.language("reset_password_subject"), - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` -${this.passwordEvent.language("reset_password_subject")} -${this.passwordEvent.language("hi_user_name", { user: this.passwordEvent.user.name })}, -${this.passwordEvent.language("someone_requested_password_reset")} -${this.passwordEvent.language("change_password")}: ${this.passwordEvent.resetLink} -${this.passwordEvent.language("password_reset_instructions")} -${this.passwordEvent.language("have_any_questions")} ${this.passwordEvent.language( - "contact_our_support_team" - )} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.passwordEvent.language("reset_password_subject"); - return ` - - - ${emailHead(headerContent)} - -
- ${emailBodyLogo()} - -
- - - - - - -
- -
-
- -
- - - - - - -
- -
-
- -
- - - - - - -
- -
- - - - - - - - - -
-
-
-

${this.passwordEvent.language("hi_user_name", { - user: this.passwordEvent.user.name, - })},

-

${this.passwordEvent.language( - "someone_requested_password_reset" - )}

-
-
-
- - - - -
-

- ${this.passwordEvent.language( - "change_password" - )} -

-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-
-
-

${this.passwordEvent.language( - "password_reset_instructions" - )}

-
-

-
-

${this.passwordEvent.language( - "have_any_questions" - )} ${this.passwordEvent.language( - "contact_our_support_team" - )}

-
-
-
-
- -
-
- -
- - - - - - -
- -
-
- -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/organizer-cancelled-email.ts b/apps/web/lib/emails/templates/organizer-cancelled-email.ts deleted file mode 100644 index de05d3d2ad..0000000000 --- a/apps/web/lib/emails/templates/organizer-cancelled-email.ts +++ /dev/null @@ -1,152 +0,0 @@ -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 { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; -import OrganizerScheduledEmail from "./organizer-scheduled-email"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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("event_cancelled_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("event_request_cancelled")} -${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -${this.calEvent.cancellationReason && this.getCancellationReason()} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("event_cancelled_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("xCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("event_request_cancelled"), - this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} - ${this.calEvent.cancellationReason && this.getCancellationReason()} -
-
-
- -
-
- -
- - - - - - -
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getCancellationReason(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("cancellation_reason")}

-

${this.calEvent.cancellationReason}

-
`; - } -} diff --git a/apps/web/lib/emails/templates/organizer-location-change-email.ts b/apps/web/lib/emails/templates/organizer-location-change-email.ts deleted file mode 100644 index 15bb3b6b55..0000000000 --- a/apps/web/lib/emails/templates/organizer-location-change-email.ts +++ /dev/null @@ -1,162 +0,0 @@ -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 { getCancelLink } from "@calcom/lib/CalEventParser"; - -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; -import OrganizerScheduledEmail from "./organizer-scheduled-email"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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 { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("location_changed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("event_location_changed")} -${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("location_changed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("event_location_changed"), - this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/organizer-payment-refund-failed-email.ts b/apps/web/lib/emails/templates/organizer-payment-refund-failed-email.ts deleted file mode 100644 index e977be1bb8..0000000000 --- a/apps/web/lib/emails/templates/organizer-payment-refund-failed-email.ts +++ /dev/null @@ -1,207 +0,0 @@ -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 { emailHead, emailSchedulingBodyHeader, emailBodyLogo, emailSchedulingBodyDivider } from "./common"; -import OrganizerScheduledEmail from "./organizer-scheduled-email"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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("refund_failed_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("a_refund_failed")} -${this.calEvent.organizer.language.translate("check_with_provider_and_user", { - user: this.calEvent.attendees[0].name, -})} -${ - this.calEvent.paymentInfo && - this.calEvent.organizer.language.translate("error_message", { - errorMessage: this.calEvent.paymentInfo.reason, - }) -} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("refund_failed_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - - -
- ${emailSchedulingBodyHeader("xCircle")} - -
- - - - - - -
- -
- - - - - - - - - ${this.getRefundInformation()} - -
-
${this.calEvent.organizer.language.translate( - "a_refund_failed" - )}
-
-
${this.calEvent.organizer.language.translate( - "check_with_provider_and_user", - { user: this.calEvent.attendees[0].name } - )}
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- -
- - - - - - -
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getRefundInformation(): string { - const paymentInfo = this.calEvent.paymentInfo; - let refundInformation = ""; - - if (paymentInfo) { - if (paymentInfo.reason) { - refundInformation = ` - - -
${this.calEvent.organizer.language.translate( - "error_message", - { errorMessage: paymentInfo.reason } - )}
- - - `; - } - - if (paymentInfo.id) { - refundInformation += ` - - -
Payment ${paymentInfo.id}
- - - `; - } - } - - return refundInformation; - } -} diff --git a/apps/web/lib/emails/templates/organizer-request-email.ts b/apps/web/lib/emails/templates/organizer-request-email.ts deleted file mode 100644 index 5fd5144875..0000000000 --- a/apps/web/lib/emails/templates/organizer-request-email.ts +++ /dev/null @@ -1,178 +0,0 @@ -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 { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, - linkIcon, -} from "./common"; -import OrganizerScheduledEmail from "./organizer-scheduled-email"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerRequestEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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("event_awaiting_approval_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("event_awaiting_approval")} -${this.calEvent.organizer.language.translate("someone_requested_an_event")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -${this.calEvent.organizer.language.translate("confirm_or_reject_request")} -${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming" -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("event_awaiting_approval_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate( - this.recurringEvent?.count ? "event_awaiting_approval_recurring" : "event_awaiting_approval" - ), - this.calEvent.organizer.language.translate("someone_requested_an_event") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - - - - -
- - - - -
-

- ${this.getManageLink()} -

-
-
-
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getManageLink(): string { - const manageText = this.calEvent.organizer.language.translate("confirm_or_reject_request"); - const manageLink = process.env.NEXT_PUBLIC_WEBAPP_URL + "/bookings/upcoming"; - return `${manageText} `; - } -} diff --git a/apps/web/lib/emails/templates/organizer-request-reminder-email.ts b/apps/web/lib/emails/templates/organizer-request-reminder-email.ts deleted file mode 100644 index 55fdde2ad8..0000000000 --- a/apps/web/lib/emails/templates/organizer-request-reminder-email.ts +++ /dev/null @@ -1,176 +0,0 @@ -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 { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, - linkIcon, -} from "./common"; -import OrganizerScheduledEmail from "./organizer-scheduled-email"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerRequestReminderEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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("event_awaiting_approval_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("event_still_awaiting_approval")} -${this.calEvent.organizer.language.translate("someone_requested_an_event")} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -${this.calEvent.organizer.language.translate("confirm_or_reject_request")} -${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming" -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("event_awaiting_approval_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("event_still_awaiting_approval"), - this.calEvent.organizer.language.translate("someone_requested_an_event") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - - - - -
- - - - -
-

- ${this.getManageLink()} -

-
-
-
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getManageLink(): string { - const manageText = this.calEvent.organizer.language.translate("confirm_or_reject_request"); - const manageLink = process.env.NEXT_PUBLIC_WEBAPP_URL + "/bookings/upcoming"; - return `${manageText} `; - } -} diff --git a/apps/web/lib/emails/templates/organizer-request-reschedule-email.ts b/apps/web/lib/emails/templates/organizer-request-reschedule-email.ts deleted file mode 100644 index b6251456b2..0000000000 --- a/apps/web/lib/emails/templates/organizer-request-reschedule-email.ts +++ /dev/null @@ -1,192 +0,0 @@ -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 { createEvent, DateArray, Person } from "ics"; - -import { getCancelLink } from "@calcom/lib/CalEventParser"; -import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; - -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; -import OrganizerScheduledEmail from "./organizer-scheduled-email"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerRequestRescheduledEmail extends OrganizerScheduledEmail { - private metadata: { rescheduleLink: string }; - constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }, recurringEvent: RecurringEvent) { - super(calEvent, recurringEvent); - this.metadata = metadata; - } - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - // @OVERRIDE - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CANCELLED", - method: "CANCEL", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - // @OVERRIDE - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}

-

- ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("request_reschedule_title_organizer", { - attendee: this.calEvent.attendees[0].name, -})} -${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { - attendee: this.calEvent.attendees[0].name, -})}, -${this.calEvent.cancellationReason && this.getReason()} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("request_reschedule_title_organizer", { - attendee: this.calEvent.attendees[0].name, - }), - this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { - attendee: this.calEvent.attendees[0].name, - }) - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/organizer-rescheduled-email.ts b/apps/web/lib/emails/templates/organizer-rescheduled-email.ts deleted file mode 100644 index 6f33489d5c..0000000000 --- a/apps/web/lib/emails/templates/organizer-rescheduled-email.ts +++ /dev/null @@ -1,166 +0,0 @@ -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 { getCancelLink } from "@calcom/lib/CalEventParser"; - -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; -import OrganizerScheduledEmail from "./organizer-scheduled-email"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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 { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("event_has_been_rescheduled")} -${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")} -${this.calEvent.cancellationReason && this.getReason()} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getDescription()} -${this.getAdditionalNotes()} -${this.getCustomInputs()} -${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("event_has_been_rescheduled"), - this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/apps/web/lib/emails/templates/organizer-scheduled-email.ts b/apps/web/lib/emails/templates/organizer-scheduled-email.ts deleted file mode 100644 index a6f251c0ee..0000000000 --- a/apps/web/lib/emails/templates/organizer-scheduled-email.ts +++ /dev/null @@ -1,450 +0,0 @@ -import dayjs, { 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 { createEvent, DateArray, Person } from "ics"; -import rrule from "rrule"; - -import { getAppName } from "@calcom/app-store/utils"; -import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser"; -import type { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; - -import BaseEmail from "@lib/emails/templates/_base-email"; - -import { - emailBodyLogo, - emailHead, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, - emailSchedulingBodyHeader, - linkIcon, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerScheduledEmail extends BaseEmail { - calEvent: CalendarEvent; - recurringEvent: RecurringEvent; - - constructor(calEvent: CalendarEvent, recurringEvent: RecurringEvent) { - super(); - this.name = "SEND_BOOKING_CONFIRMATION"; - this.calEvent = calEvent; - this.recurringEvent = recurringEvent; - } - - protected getiCalEventAsString(): string | undefined { - // Taking care of recurrence rule beforehand - let recurrenceRule: string | undefined = undefined; - if (this.recurringEvent?.count) { - recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", ""); - } - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - ...{ recurrenceRule }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CONFIRMED", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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 { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("confirmed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate( - this.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled" -)} -${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")} - -${getRichDescription(this.calEvent)} -`.trim(); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("confirmed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("checkCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate( - this.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled" - ), - this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} - ${this.getCustomInputs()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getManageLink(): string { - const manageText = this.calEvent.organizer.language.translate("manage_this_event"); - return `

${this.calEvent.organizer.language.translate( - "need_to_reschedule_or_cancel" - )}

${manageText}

`; - } - - protected getWhat(): string { - return ` -
-

${this.calEvent.organizer.language.translate("what")}

-

${this.calEvent.type}

-
`; - } - - protected getRecurringWhen(): string { - if (this.recurringEvent?.freq) { - return ` - ${this.calEvent.attendees[0].language.translate("every_for_freq", { - freq: this.calEvent.attendees[0].language.translate( - `${rrule.FREQUENCIES[this.recurringEvent.freq].toString().toLowerCase()}` - ), - })} ${this.recurringEvent.count} ${this.calEvent.attendees[0].language.translate( - `${rrule.FREQUENCIES[this.recurringEvent.freq].toString().toLowerCase()}`, - { count: this.recurringEvent.count } - )}`; - } else { - return ""; - } - } - - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}${ - this.recurringEvent?.count ? this.getRecurringWhen() : "" - }

-

- ${this.recurringEvent?.count ? `${this.calEvent.attendees[0].language.translate("starting")} ` : ""} - ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getWho(): string { - const attendees = this.calEvent.attendees - .map((attendee) => { - return `
${ - attendee?.name || `${this.calEvent.organizer.language.translate("guest")}` - } ${ - attendee.email - }
`; - }) - .join(""); - - const organizer = `
${ - this.calEvent.organizer.name - } - ${this.calEvent.organizer.language.translate( - "organizer" - )} ${this.calEvent.organizer.email}
`; - - return ` -

-
-

${this.calEvent.organizer.language.translate("who")}

- ${organizer + attendees} -
`; - } - - protected getAdditionalNotes(): string { - if (!this.calEvent.additionalNotes) return ""; - return ` -

-
-

${this.calEvent.organizer.language.translate("additional_notes")}

-

${ - this.calEvent.additionalNotes - }

-
- `; - } - - protected getCustomInputs(): string { - const { customInputs } = this.calEvent; - if (!customInputs) return ""; - const customInputsString = Object.keys(customInputs) - .map((key) => { - if (customInputs[key] !== "") { - return ` -

-
-

${key}

-

- ${customInputs[key]} -

-
- `; - } - }) - .join(""); - - return customInputsString; - } - - protected getDescription(): string { - if (!this.calEvent.description) return ""; - return ` -

-
-

${this.calEvent.organizer.language.translate("description")}

-

${ - this.calEvent.description - }

-
- `; - } - - protected getLocation(): string { - let providerName = this.calEvent.location && getAppName(this.calEvent.location); // This returns null if nothing is found - - if (this.calEvent.location && this.calEvent.location.includes("integrations:")) { - const location = this.calEvent.location.split(":")[1]; - providerName = location[0].toUpperCase() + location.slice(1); - } - - // If location its a url, probably we should be validating it with a custom library - if (this.calEvent.location && /^https?:\/\//.test(this.calEvent.location)) { - providerName = this.calEvent.location; - } - - if (this.calEvent.videoCallData) { - const meetingId = this.calEvent.videoCallData.id; - const meetingPassword = this.calEvent.videoCallData.password; - const meetingUrl = this.calEvent.videoCallData.url; - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${providerName} - ${ - meetingUrl && - `` - }

- ${ - meetingId && - `
${this.calEvent.organizer.language.translate( - "meeting_id" - )}: ${meetingId}
` - } - ${ - meetingPassword && - `
${this.calEvent.organizer.language.translate( - "meeting_password" - )}: ${meetingPassword}
` - } - ${ - meetingUrl && - `
${this.calEvent.organizer.language.translate( - "meeting_url" - )}: ${meetingUrl}
` - } -
- `; - } - - if (this.calEvent.additionInformation?.hangoutLink) { - const hangoutLink: string = this.calEvent.additionInformation.hangoutLink; - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${providerName} ${ - hangoutLink && - `` - }

- -
- `; - } - - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${ - providerName || this.calEvent.location - }

- ${ - providerName === "Zoom" || providerName === "Google" - ? `

- ${this.calEvent.organizer.language.translate("meeting_url_provided_after_confirmed")} -

` - : `` - } -
- `; - } - - protected getTimezone(): string { - return this.calEvent.organizer.timeZone; - } - - protected getOrganizerStart(): Dayjs { - return dayjs(this.calEvent.startTime).tz(this.getTimezone()); - } - - protected getOrganizerEnd(): Dayjs { - return dayjs(this.calEvent.endTime).tz(this.getTimezone()); - } - - protected getReason(): string { - return ` -
-

${this.calEvent.attendees[0].language.translate("reschedule_reason")}

-

${this.calEvent.cancellationReason}

-
`; - } -} diff --git a/apps/web/lib/emails/templates/team-invite-email.ts b/apps/web/lib/emails/templates/team-invite-email.ts deleted file mode 100644 index c15c705376..0000000000 --- a/apps/web/lib/emails/templates/team-invite-email.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { TFunction } from "next-i18next"; - -import BaseEmail from "@lib/emails/templates/_base-email"; - -import { emailBodyLogo, emailHead, linkIcon } from "./common"; - -export type TeamInvite = { - language: TFunction; - from: string; - to: string; - teamName: string; - joinLink: string; -}; - -export default class TeamInviteEmail extends BaseEmail { - teamInviteEvent: TeamInvite; - - constructor(teamInviteEvent: TeamInvite) { - super(); - this.name = "SEND_TEAM_INVITE_EMAIL"; - this.teamInviteEvent = teamInviteEvent; - } - - protected getNodeMailerPayload(): Record { - return { - to: this.teamInviteEvent.to, - from: `Cal.com <${this.getMailerOptions().from}>`, - subject: this.teamInviteEvent.language("user_invited_you", { - user: this.teamInviteEvent.from, - team: this.teamInviteEvent.teamName, - }), - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getTextBody(): string { - return ""; - } - - protected getHtmlBody(): string { - const headerContent = this.teamInviteEvent.language("user_invited_you", { - user: this.teamInviteEvent.from, - team: this.teamInviteEvent.teamName, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailBodyLogo()} - -
- - - - - - -
- -
-
- -
- - - - - - -
- -
-
- -
- - - - - - -
- -
- - - - - - - - - -
-
-
-

${this.teamInviteEvent.language("user_invited_you", { - user: this.teamInviteEvent.from, - team: this.teamInviteEvent.teamName, - })}!

-

${this.teamInviteEvent.language( - "calcom_explained" - )}

-
-
-
- - - - -
-

- ${this.teamInviteEvent.language( - "accept_invitation" - )} -

-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-
-
-

${this.teamInviteEvent.language( - "have_any_questions" - )} ${this.teamInviteEvent.language( - "contact_our_support_team" - )}

-
-
-
-
- -
-
- -
- - - - - - -
- -
-
- -
- - - `; - } -} diff --git a/apps/web/next.config.js b/apps/web/next.config.js index ac3902b8ca..b54e94410a 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -8,6 +8,7 @@ const withTM = require("next-transpile-modules")([ "@calcom/prisma", "@calcom/stripe", "@calcom/ui", + "@calcom/emails", "@calcom/embed-core", "@calcom/embed-snippet", ]); diff --git a/apps/web/pages/api/auth/[...nextauth].tsx b/apps/web/pages/api/auth/[...nextauth].tsx index f63ec6d407..016bdaa520 100644 --- a/apps/web/pages/api/auth/[...nextauth].tsx +++ b/apps/web/pages/api/auth/[...nextauth].tsx @@ -159,7 +159,7 @@ if (isSAMLLoginEnabled) { } if (true) { - const emailsDir = path.resolve(process.cwd(), "lib", "emails", "templates"); + const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates"); providers.push( EmailProvider({ maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only diff --git a/apps/web/pages/api/auth/forgot-password.ts b/apps/web/pages/api/auth/forgot-password.ts index c9ec0f63d2..6da67f7dd2 100644 --- a/apps/web/pages/api/auth/forgot-password.ts +++ b/apps/web/pages/api/auth/forgot-password.ts @@ -2,8 +2,9 @@ import { ResetPasswordRequest } from "@prisma/client"; import dayjs from "dayjs"; import { NextApiRequest, NextApiResponse } from "next"; -import { sendPasswordResetEmail } from "@lib/emails/email-manager"; -import { PasswordReset, PASSWORD_RESET_EXPIRY_HOURS } from "@lib/emails/templates/forgot-password-email"; +import { sendPasswordResetEmail } from "@calcom/emails"; +import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email"; + import prisma from "@lib/prisma"; import { getTranslation } from "@server/lib/i18n"; @@ -56,13 +57,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } const resetLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/forgot-password/${passwordRequest.id}`; - const passwordEmail: PasswordReset = { + await sendPasswordResetEmail({ language: t, user: maybeUser, resetLink, - }; - - await sendPasswordResetEmail(passwordEmail); + }); /** So we can test the password reset flow on CI */ if (process.env.NEXT_PUBLIC_IS_E2E) { diff --git a/apps/web/pages/api/book/confirm.ts b/apps/web/pages/api/book/confirm.ts index 06c5edf34a..fdaf42a7dc 100644 --- a/apps/web/pages/api/book/confirm.ts +++ b/apps/web/pages/api/book/confirm.ts @@ -3,6 +3,7 @@ import type { NextApiRequest } from "next"; import { z } from "zod"; import EventManager from "@calcom/core/EventManager"; +import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; @@ -11,7 +12,6 @@ import { refund } from "@ee/lib/stripe/server"; import { getSession } from "@lib/auth"; import { HttpError } from "@lib/core/http/error"; -import { sendDeclinedEmails, sendScheduledEmails } from "@lib/emails/email-manager"; import { getTranslation } from "@server/lib/i18n"; diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 9e5c461073..a203863fc2 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -11,6 +11,12 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; import EventManager from "@calcom/core/EventManager"; +import { + sendAttendeeRequestEmail, + sendOrganizerRequestEmail, + sendRescheduledEmails, + sendScheduledEmails, +} from "@calcom/emails"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents"; import { getErrorFromUnknown } from "@calcom/lib/errors"; @@ -20,12 +26,6 @@ import type { AdditionInformation, CalendarEvent, RecurringEvent } from "@calcom import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import { handlePayment } from "@ee/lib/stripe/server"; -import { - sendAttendeeRequestEmail, - sendOrganizerRequestEmail, - sendRescheduledEmails, - sendScheduledEmails, -} from "@lib/emails/email-manager"; import { ensureArray } from "@lib/ensureArray"; import { getEventName } from "@lib/event"; import getBusyTimes from "@lib/getBusyTimes"; diff --git a/apps/web/pages/api/book/request-reschedule.ts b/apps/web/pages/api/book/request-reschedule.ts index 8160ad90ab..3602bc0a78 100644 --- a/apps/web/pages/api/book/request-reschedule.ts +++ b/apps/web/pages/api/book/request-reschedule.ts @@ -9,10 +9,10 @@ import { getCalendar } from "@calcom/core/CalendarManager"; import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder"; import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director"; import { deleteMeeting } from "@calcom/core/videoClient"; +import { sendRequestRescheduleEmail } from "@calcom/emails"; import { getTranslation } from "@calcom/lib/server/i18n"; import { Person } from "@calcom/types/Calendar"; -import { sendRequestRescheduleEmail } from "@lib/emails/email-manager"; import prisma from "@lib/prisma"; export type RescheduleResponse = Booking & { diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts index c37cf73a23..85f18a349e 100644 --- a/apps/web/pages/api/cancel.ts +++ b/apps/web/pages/api/cancel.ts @@ -6,6 +6,7 @@ import { NextApiRequest, NextApiResponse } from "next"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { getCalendar } from "@calcom/core/CalendarManager"; import { deleteMeeting } from "@calcom/core/videoClient"; +import { sendCancelledEmails } from "@calcom/emails"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -13,7 +14,6 @@ import { refund } from "@ee/lib/stripe/server"; import { asStringOrNull } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; -import { sendCancelledEmails } from "@lib/emails/email-manager"; import sendPayload from "@lib/webhooks/sendPayload"; import getWebhooks from "@lib/webhooks/subscriptions"; diff --git a/apps/web/pages/api/cron/bookingReminder.ts b/apps/web/pages/api/cron/bookingReminder.ts index 9887b5d262..53ca7cf41d 100644 --- a/apps/web/pages/api/cron/bookingReminder.ts +++ b/apps/web/pages/api/cron/bookingReminder.ts @@ -2,12 +2,11 @@ import { BookingStatus, ReminderType } from "@prisma/client"; import dayjs from "dayjs"; import type { NextApiRequest, NextApiResponse } from "next"; +import { sendOrganizerRequestReminderEmail } from "@calcom/emails"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; -import { sendOrganizerRequestReminderEmail } from "@lib/emails/email-manager"; - import { getTranslation } from "@server/lib/i18n"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { diff --git a/apps/web/pages/api/email.ts b/apps/web/pages/api/email.ts new file mode 100644 index 0000000000..a8fdca5b29 --- /dev/null +++ b/apps/web/pages/api/email.ts @@ -0,0 +1,58 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { renderEmail } from "@calcom/emails"; +import { getTranslation } from "@calcom/lib/server/i18n"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (process.env.NODE_ENV !== "development") return res.write("Only for development purposes"), res.end(); + const t = await getTranslation("en", "common"); + + const evt = { + type: "30min", + title: "30min between Pro Example and pro@example.com", + description: null, + additionalNotes: "asdasdas", + customInputs: { + "Custom input 01": "sadasdasdsadasd", + "Custom input 02": "asdasdasd", + }, + startTime: "2022-06-03T09:00:00-06:00", + endTime: "2022-06-03T09:30:00-06:00", + organizer: { + name: "Pro Example", + email: "pro@example.com", + timeZone: "Europe/London", + language: { translate: t, locale: "en" }, + }, + attendees: [ + { + email: "pro@example.com", + name: "pro@example.com", + timeZone: "America/Chihuahua", + language: { translate: t, locale: "en" }, + }, + ], + location: "Zoom video", + destinationCalendar: null, + hideCalendarNotes: false, + uid: "bwPWLpjYrx4rZf6MCZdKgE", + metadata: {}, + cancellationReason: "It got late", + paymentInfo: { id: "pi_12312", link: "https://cal.com", reason: "no reason" }, + }; + + req.statusCode = 200; + + res.setHeader("Content-Type", "text/html"); + res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate"); + res.write( + renderEmail("OrganizerRequestReminderEmail", { + attendee: evt.attendees[0], + calEvent: evt, + recurringEvent: {}, + }) + ); + res.end(); +}; + +export default handler; diff --git a/apps/web/pages/api/teams/[team]/invite.ts b/apps/web/pages/api/teams/[team]/invite.ts index 3a9394f390..c66d5d568e 100644 --- a/apps/web/pages/api/teams/[team]/invite.ts +++ b/apps/web/pages/api/teams/[team]/invite.ts @@ -2,10 +2,11 @@ import { MembershipRole } from "@prisma/client"; import { randomBytes } from "crypto"; import type { NextApiRequest, NextApiResponse } from "next"; +import { sendTeamInviteEmail } from "@calcom/emails"; +import { TeamInvite } from "@calcom/emails/templates/team-invite-email"; + import { getSession } from "@lib/auth"; import { BASE_URL } from "@lib/config/constants"; -import { sendTeamInviteEmail } from "@lib/emails/email-manager"; -import { TeamInvite } from "@lib/emails/templates/team-invite-email"; import prisma from "@lib/prisma"; import slugify from "@lib/slugify"; diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx index 50ca54b1a6..0eeea4951f 100644 --- a/apps/web/server/routers/viewer.tsx +++ b/apps/web/server/routers/viewer.tsx @@ -7,11 +7,11 @@ import { z } from "zod"; import getApps, { getLocationOptions } from "@calcom/app-store/utils"; import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager"; import { checkPremiumUsername } from "@calcom/ee/lib/core/checkPremiumUsername"; +import { sendFeedbackEmail } from "@calcom/emails"; import { baseEventTypeSelect, bookingMinimalSelect } from "@calcom/prisma"; import { RecurringEvent } from "@calcom/types/Calendar"; import { checkRegularUsername } from "@lib/core/checkRegularUsername"; -import { sendFeedbackEmail } from "@lib/emails/email-manager"; import jackson from "@lib/jackson"; import prisma from "@lib/prisma"; import { isTeamOwner } from "@lib/queries/teams"; diff --git a/apps/web/server/routers/viewer/bookings.tsx b/apps/web/server/routers/viewer/bookings.tsx index 0437e04dfd..fd035bbf86 100644 --- a/apps/web/server/routers/viewer/bookings.tsx +++ b/apps/web/server/routers/viewer/bookings.tsx @@ -3,12 +3,11 @@ import dayjs from "dayjs"; import { z } from "zod"; import EventManager from "@calcom/core/EventManager"; +import { sendLocationChangeEmails } from "@calcom/emails"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; import type { AdditionInformation, CalendarEvent } from "@calcom/types/Calendar"; -import { sendLocationChangeEmails } from "@lib/emails/email-manager"; - import { createProtectedRouter } from "@server/createRouter"; import { TRPCError } from "@trpc/server"; diff --git a/apps/web/server/routers/viewer/teams.tsx b/apps/web/server/routers/viewer/teams.tsx index 9ea06ec55a..f9e9744a07 100644 --- a/apps/web/server/routers/viewer/teams.tsx +++ b/apps/web/server/routers/viewer/teams.tsx @@ -1,21 +1,18 @@ -import { MembershipRole, UserPlan } from "@prisma/client"; -import { Prisma } from "@prisma/client"; +import { MembershipRole, Prisma, UserPlan } from "@prisma/client"; import { randomBytes } from "crypto"; import { z } from "zod"; +import { sendTeamInviteEmail } from "@calcom/emails"; import { addSeat, - removeSeat, - getTeamSeatStats, downgradeTeamMembers, - upgradeTeam, ensureSubscriptionQuantityCorrectness, + getTeamSeatStats, + removeSeat, + upgradeTeam, } from "@calcom/stripe/team-billing"; -import { BASE_URL } from "@lib/config/constants"; -import { HOSTED_CAL_FEATURES } from "@lib/config/constants"; -import { sendTeamInviteEmail } from "@lib/emails/email-manager"; -import { TeamInvite } from "@lib/emails/templates/team-invite-email"; +import { BASE_URL, HOSTED_CAL_FEATURES } from "@lib/config/constants"; import { getUserAvailability } from "@lib/queries/availability"; import { getTeamWithMembers, isTeamAdmin, isTeamOwner } from "@lib/queries/teams"; import slugify from "@lib/slugify"; @@ -262,14 +259,13 @@ export const viewerTeamsRouter = createProtectedRouter() }); if (ctx?.user?.name && team?.name) { - const teamInviteEvent: TeamInvite = { + await sendTeamInviteEmail({ language: translation, from: ctx.user.name, to: input.usernameOrEmail, teamName: team.name, joinLink: `${BASE_URL}/auth/signup?token=${token}&callbackUrl=${BASE_URL + "/settings/teams"}`, - }; - await sendTeamInviteEmail(teamInviteEvent); + }); } } else { // create provisional membership @@ -294,15 +290,13 @@ export const viewerTeamsRouter = createProtectedRouter() // inform user of membership by email if (input.sendEmailInvitation && ctx?.user?.name && team?.name) { - const teamInviteEvent: TeamInvite = { + await sendTeamInviteEmail({ language: translation, from: ctx.user.name, to: input.usernameOrEmail, teamName: team.name, joinLink: BASE_URL + "/settings/teams", - }; - - await sendTeamInviteEmail(teamInviteEvent); + }); } } try { diff --git a/packages/app-store/vital/lib/emailManager.ts b/packages/app-store/vital/lib/emailManager.ts deleted file mode 100644 index abf2f52063..0000000000 --- a/packages/app-store/vital/lib/emailManager.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CalendarEvent } from "@calcom/types/Calendar"; - -import AttendeeRequestRescheduledEmail from "./templates/attendee-request-reschedule-email"; -import OrganizerRequestRescheduledEmail from "./templates/organizer-request-reschedule-email"; - -export const sendRequestRescheduleEmail = async ( - calEvent: CalendarEvent, - metadata: { rescheduleLink: string } -) => { - const emailsToSend: Promise[] = []; - - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const requestRescheduleEmail = new AttendeeRequestRescheduledEmail(calEvent, metadata); - resolve(requestRescheduleEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeRequestRescheduledEmail.sendEmail failed", e)); - } - }) - ); - - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const requestRescheduleEmail = new OrganizerRequestRescheduledEmail(calEvent, metadata); - resolve(requestRescheduleEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerRequestRescheduledEmail.sendEmail failed", e)); - } - }) - ); - - await Promise.all(emailsToSend); -}; diff --git a/packages/app-store/vital/lib/emailServerConfig.ts b/packages/app-store/vital/lib/emailServerConfig.ts deleted file mode 100644 index a51d0c7e4f..0000000000 --- a/packages/app-store/vital/lib/emailServerConfig.ts +++ /dev/null @@ -1,34 +0,0 @@ -import SendmailTransport from "nodemailer/lib/sendmail-transport"; -import SMTPConnection from "nodemailer/lib/smtp-connection"; - -function detectTransport(): SendmailTransport.Options | SMTPConnection.Options | string { - if (process.env.EMAIL_SERVER) { - return process.env.EMAIL_SERVER; - } - - if (process.env.EMAIL_SERVER_HOST) { - const port = parseInt(process.env.EMAIL_SERVER_PORT!); - const transport = { - host: process.env.EMAIL_SERVER_HOST, - port, - auth: { - user: process.env.EMAIL_SERVER_USER, - pass: process.env.EMAIL_SERVER_PASSWORD, - }, - secure: port === 465, - }; - - return transport; - } - - return { - sendmail: true, - newline: "unix", - path: "/usr/sbin/sendmail", - }; -} - -export const serverConfig = { - transport: detectTransport(), - from: process.env.EMAIL_FROM, -}; diff --git a/packages/app-store/vital/lib/reschedule.ts b/packages/app-store/vital/lib/reschedule.ts index 5602e548e0..a6820fe1ef 100644 --- a/packages/app-store/vital/lib/reschedule.ts +++ b/packages/app-store/vital/lib/reschedule.ts @@ -1,4 +1,4 @@ -import { BookingStatus, User, Booking, BookingReference } from "@prisma/client"; +import { Booking, BookingReference, BookingStatus, User } from "@prisma/client"; import dayjs from "dayjs"; import type { TFunction } from "next-i18next"; @@ -6,13 +6,13 @@ import EventManager from "@calcom/core/EventManager"; import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder"; import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director"; import { deleteMeeting } from "@calcom/core/videoClient"; +import { sendRequestRescheduleEmail } from "@calcom/emails"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import { Person } from "@calcom/types/Calendar"; import { getCalendar } from "../../_utils/getCalendar"; -import { sendRequestRescheduleEmail } from "./emailManager"; type PersonAttendeeCommonFields = Pick; diff --git a/packages/app-store/vital/lib/templates/attendee-request-reschedule-email.ts b/packages/app-store/vital/lib/templates/attendee-request-reschedule-email.ts deleted file mode 100644 index a032a37be8..0000000000 --- a/packages/app-store/vital/lib/templates/attendee-request-reschedule-email.ts +++ /dev/null @@ -1,210 +0,0 @@ -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 { createEvent, DateArray, Person } from "ics"; - -import { getCancelLink } from "@calcom/lib/CalEventParser"; -import { CalendarEvent } from "@calcom/types/Calendar"; - -import BaseTemplate from "./base-template"; -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class AttendeeRequestRescheduledEmail extends BaseTemplate { - private metadata: { rescheduleLink: string }; - constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) { - super(calEvent); - this.metadata = metadata; - } - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.attendees[0].email]; - - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("requested_to_reschedule_subject_attendee", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - // @OVERRIDE - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CANCELLED", - method: "CANCEL", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - // @OVERRIDE - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}

-

- ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("request_reschedule_title_attendee")} -${this.calEvent.organizer.language.translate("request_reschedule_subtitle", { - organizer: this.calEvent.organizer.name, -})}, -${this.calEvent.cancellationReason && this.getReason()} -${this.getWhat()} -${this.getWhen()} -${this.getAdditionalNotes()} -${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("request_reschedule_title_attendee"), - this.calEvent.organizer.language.translate("request_reschedule_subtitle", { - organizer: this.calEvent.organizer.name, - }) - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getAdditionalNotes()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
- -
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/packages/app-store/vital/lib/templates/base-template.ts b/packages/app-store/vital/lib/templates/base-template.ts deleted file mode 100644 index 3b8b164463..0000000000 --- a/packages/app-store/vital/lib/templates/base-template.ts +++ /dev/null @@ -1,417 +0,0 @@ -import dayjs, { 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 { createEvent, DateArray, Person } from "ics"; -import nodemailer from "nodemailer"; - -import { getAppName } from "@calcom/app-store/utils"; -import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser"; -import { getErrorFromUnknown } from "@calcom/lib/errors"; -import type { CalendarEvent } from "@calcom/types/Calendar"; - -import { serverConfig } from "../emailServerConfig"; -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, - linkIcon, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerScheduledEmail { - calEvent: CalendarEvent; - - constructor(calEvent: CalendarEvent) { - this.calEvent = calEvent; - } - - public sendEmail() { - new Promise((resolve, reject) => - nodemailer - .createTransport(this.getMailerOptions().transport) - .sendMail(this.getNodeMailerPayload(), (_err, info) => { - if (_err) { - const err = getErrorFromUnknown(_err); - this.printNodeMailerError(err); - reject(err); - } else { - resolve(info); - } - }) - ).catch((e) => console.error("sendEmail", e)); - return new Promise((resolve) => resolve("send mail async")); - } - - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CONFIRMED", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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 { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("confirmed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getMailerOptions() { - return { - transport: serverConfig.transport, - from: serverConfig.from, - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("new_event_scheduled")} -${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")} - -${getRichDescription(this.calEvent)} -`.trim(); - } - - protected printNodeMailerError(error: Error): void { - console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.calEvent.organizer.email, error); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("confirmed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("checkCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("new_event_scheduled"), - this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getManageLink(): string { - const manageText = this.calEvent.organizer.language.translate("manage_this_event"); - return `

${this.calEvent.organizer.language.translate( - "need_to_reschedule_or_cancel" - )}

${manageText}

`; - } - - protected getWhat(): string { - return ` -
-

${this.calEvent.organizer.language.translate("what")}

-

${this.calEvent.type}

-
`; - } - - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}

-

- ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getWho(): string { - const attendees = this.calEvent.attendees - .map((attendee) => { - return `
${ - attendee?.name || `${this.calEvent.organizer.language.translate("guest")}` - } ${ - attendee.email - }
`; - }) - .join(""); - - const organizer = `
${ - this.calEvent.organizer.name - } - ${this.calEvent.organizer.language.translate( - "organizer" - )} ${this.calEvent.organizer.email}
`; - - return ` -

-
-

${this.calEvent.organizer.language.translate("who")}

- ${organizer + attendees} -
`; - } - - protected getAdditionalNotes(): string { - if (!this.calEvent.additionalNotes) return ""; - return ` -

-
-

${this.calEvent.organizer.language.translate("additional_notes")}

-

${ - this.calEvent.additionalNotes - }

-
- `; - } - - protected getDescription(): string { - if (!this.calEvent.description) return ""; - return ` -

-
-

${this.calEvent.organizer.language.translate("description")}

-

${ - this.calEvent.description - }

-
- `; - } - - protected getLocation(): string { - let providerName = this.calEvent.location ? getAppName(this.calEvent.location) : ""; - - if (this.calEvent.location && this.calEvent.location.includes("integrations:")) { - const location = this.calEvent.location.split(":")[1]; - providerName = location[0].toUpperCase() + location.slice(1); - } - - // If location its a url, probably we should be validating it with a custom library - if (this.calEvent.location && /^https?:\/\//.test(this.calEvent.location)) { - providerName = this.calEvent.location; - } - - if (this.calEvent.videoCallData) { - const meetingId = this.calEvent.videoCallData.id; - const meetingPassword = this.calEvent.videoCallData.password; - const meetingUrl = this.calEvent.videoCallData.url; - - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${providerName} ${ - meetingUrl && - `` - }

- ${ - meetingId && - `
${this.calEvent.organizer.language.translate( - "meeting_id" - )}: ${meetingId}
` - } - ${ - meetingPassword && - `
${this.calEvent.organizer.language.translate( - "meeting_password" - )}: ${meetingPassword}
` - } - ${ - meetingUrl && - `
${this.calEvent.organizer.language.translate( - "meeting_url" - )}: ${meetingUrl}
` - } -
- `; - } - - if (this.calEvent.additionInformation?.hangoutLink) { - const hangoutLink: string = this.calEvent.additionInformation.hangoutLink; - - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${providerName} ${ - hangoutLink && - `` - }

- -
- `; - } - - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${ - providerName || this.calEvent.location - }

-
- `; - } - - protected getTimezone(): string { - return this.calEvent.organizer.timeZone; - } - - protected getOrganizerStart(): Dayjs { - return dayjs(this.calEvent.startTime).tz(this.getTimezone()); - } - - protected getOrganizerEnd(): Dayjs { - return dayjs(this.calEvent.endTime).tz(this.getTimezone()); - } - - protected getReason(): string { - return ` -
-

${this.calEvent.attendees[0].language.translate("reschedule_reason")}

-

${this.calEvent.cancellationReason}

-
`; - } -} diff --git a/packages/app-store/vital/lib/templates/common/body-logo.ts b/packages/app-store/vital/lib/templates/common/body-logo.ts deleted file mode 100644 index 3b5b143e8a..0000000000 --- a/packages/app-store/vital/lib/templates/common/body-logo.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants"; - -export const emailBodyLogo = (): string => { - const image = IS_PRODUCTION - ? BASE_URL + "/emails/CalLogo@2x.png" - : "https://app.cal.com/emails/CalLogo@2x.png"; - - return ` - -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- - - -
-
-
- -
-
- `; -}; diff --git a/packages/app-store/vital/lib/templates/common/head.ts b/packages/app-store/vital/lib/templates/common/head.ts deleted file mode 100644 index be22403827..0000000000 --- a/packages/app-store/vital/lib/templates/common/head.ts +++ /dev/null @@ -1,91 +0,0 @@ -export const emailHead = (headerContent: string): string => { - return ` - - ${headerContent} - - - - - - - - - - - - - - - - - `; -}; diff --git a/packages/app-store/vital/lib/templates/common/index.ts b/packages/app-store/vital/lib/templates/common/index.ts deleted file mode 100644 index 686d871f91..0000000000 --- a/packages/app-store/vital/lib/templates/common/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { emailHead } from "./head"; -export { emailSchedulingBodyHeader } from "./scheduling-body-head"; -export { emailBodyLogo } from "./body-logo"; -export { emailScheduledBodyHeaderContent } from "./scheduling-body-head-content"; -export { emailSchedulingBodyDivider } from "./scheduling-body-divider"; -export { linkIcon } from "./link-icon"; diff --git a/packages/app-store/vital/lib/templates/common/link-icon.ts b/packages/app-store/vital/lib/templates/common/link-icon.ts deleted file mode 100644 index 434ae2cbcb..0000000000 --- a/packages/app-store/vital/lib/templates/common/link-icon.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants"; - -export const linkIcon = (): string => { - return IS_PRODUCTION ? BASE_URL + "/emails/linkIcon.png" : "https://app.cal.com/emails/linkIcon.png"; -}; diff --git a/packages/app-store/vital/lib/templates/common/scheduling-body-divider.ts b/packages/app-store/vital/lib/templates/common/scheduling-body-divider.ts deleted file mode 100644 index b8723c3e7b..0000000000 --- a/packages/app-store/vital/lib/templates/common/scheduling-body-divider.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const emailSchedulingBodyDivider = (): string => { - return ` - -
- - - - - - -
- -
- - - - - - -
-

-

- -
-
- -
-
- `; -}; diff --git a/packages/app-store/vital/lib/templates/common/scheduling-body-head-content.ts b/packages/app-store/vital/lib/templates/common/scheduling-body-head-content.ts deleted file mode 100644 index 31515fc760..0000000000 --- a/packages/app-store/vital/lib/templates/common/scheduling-body-head-content.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const emailScheduledBodyHeaderContent = (title: string, subtitle: string): string => { - return ` - -
- - - - - - -
- -
- - - - - - - - - -
-
${title}
-
-
${subtitle}
-
-
- -
-
- `; -}; diff --git a/packages/app-store/vital/lib/templates/common/scheduling-body-head.ts b/packages/app-store/vital/lib/templates/common/scheduling-body-head.ts deleted file mode 100644 index fc715d2a26..0000000000 --- a/packages/app-store/vital/lib/templates/common/scheduling-body-head.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants"; - -export type BodyHeadType = "checkCircle" | "xCircle" | "calendarCircle"; - -export const getHeadImage = (headerType: BodyHeadType): string => { - switch (headerType) { - case "checkCircle": - return IS_PRODUCTION - ? BASE_URL + "/emails/checkCircle@2x.png" - : "https://app.cal.com/emails/checkCircle@2x.png"; - case "xCircle": - return IS_PRODUCTION - ? BASE_URL + "/emails/xCircle@2x.png" - : "https://app.cal.com/emails/xCircle@2x.png"; - case "calendarCircle": - return IS_PRODUCTION - ? BASE_URL + "/emails/calendarCircle@2x.png" - : "https://app.cal.com/emails/calendarCircle@2x.png"; - } -}; - -export const emailSchedulingBodyHeader = (headerType: BodyHeadType): string => { - const image = getHeadImage(headerType); - - return ` - -
- - - - - - -
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- -
-
-
- -
-
- `; -}; diff --git a/packages/app-store/vital/lib/templates/organizer-request-reschedule-email.ts b/packages/app-store/vital/lib/templates/organizer-request-reschedule-email.ts deleted file mode 100644 index 2c5a9a56e1..0000000000 --- a/packages/app-store/vital/lib/templates/organizer-request-reschedule-email.ts +++ /dev/null @@ -1,190 +0,0 @@ -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 { createEvent, DateArray, Person } from "ics"; - -import { getCancelLink } from "@calcom/lib/CalEventParser"; -import { CalendarEvent } from "@calcom/types/Calendar"; - -import BaseTemplate from "./base-template"; -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerRequestRescheduledEmail extends BaseTemplate { - private metadata: { rescheduleLink: string }; - constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) { - super(calEvent); - this.metadata = metadata; - } - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - // @OVERRIDE - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CANCELLED", - method: "CANCEL", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - // @OVERRIDE - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}

-

- ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("request_reschedule_title_organizer", { - attendee: this.calEvent.attendees[0].name, -})} -${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { - attendee: this.calEvent.attendees[0].name, -})}, -${this.calEvent.cancellationReason && this.getReason()} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getAdditionalNotes()} -${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("request_reschedule_title_organizer", { - attendee: this.calEvent.attendees[0].name, - }), - this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { - attendee: this.calEvent.attendees[0].name, - }) - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getAdditionalNotes()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/packages/app-store/wipemycalother/lib/emailManager.ts b/packages/app-store/wipemycalother/lib/emailManager.ts deleted file mode 100644 index abf2f52063..0000000000 --- a/packages/app-store/wipemycalother/lib/emailManager.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CalendarEvent } from "@calcom/types/Calendar"; - -import AttendeeRequestRescheduledEmail from "./templates/attendee-request-reschedule-email"; -import OrganizerRequestRescheduledEmail from "./templates/organizer-request-reschedule-email"; - -export const sendRequestRescheduleEmail = async ( - calEvent: CalendarEvent, - metadata: { rescheduleLink: string } -) => { - const emailsToSend: Promise[] = []; - - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const requestRescheduleEmail = new AttendeeRequestRescheduledEmail(calEvent, metadata); - resolve(requestRescheduleEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeRequestRescheduledEmail.sendEmail failed", e)); - } - }) - ); - - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const requestRescheduleEmail = new OrganizerRequestRescheduledEmail(calEvent, metadata); - resolve(requestRescheduleEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerRequestRescheduledEmail.sendEmail failed", e)); - } - }) - ); - - await Promise.all(emailsToSend); -}; diff --git a/packages/app-store/wipemycalother/lib/emailServerConfig.ts b/packages/app-store/wipemycalother/lib/emailServerConfig.ts deleted file mode 100644 index a51d0c7e4f..0000000000 --- a/packages/app-store/wipemycalother/lib/emailServerConfig.ts +++ /dev/null @@ -1,34 +0,0 @@ -import SendmailTransport from "nodemailer/lib/sendmail-transport"; -import SMTPConnection from "nodemailer/lib/smtp-connection"; - -function detectTransport(): SendmailTransport.Options | SMTPConnection.Options | string { - if (process.env.EMAIL_SERVER) { - return process.env.EMAIL_SERVER; - } - - if (process.env.EMAIL_SERVER_HOST) { - const port = parseInt(process.env.EMAIL_SERVER_PORT!); - const transport = { - host: process.env.EMAIL_SERVER_HOST, - port, - auth: { - user: process.env.EMAIL_SERVER_USER, - pass: process.env.EMAIL_SERVER_PASSWORD, - }, - secure: port === 465, - }; - - return transport; - } - - return { - sendmail: true, - newline: "unix", - path: "/usr/sbin/sendmail", - }; -} - -export const serverConfig = { - transport: detectTransport(), - from: process.env.EMAIL_FROM, -}; diff --git a/packages/app-store/wipemycalother/lib/reschedule.ts b/packages/app-store/wipemycalother/lib/reschedule.ts index 97f70b9f4e..a6820fe1ef 100644 --- a/packages/app-store/wipemycalother/lib/reschedule.ts +++ b/packages/app-store/wipemycalother/lib/reschedule.ts @@ -6,13 +6,13 @@ import EventManager from "@calcom/core/EventManager"; import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder"; import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director"; import { deleteMeeting } from "@calcom/core/videoClient"; +import { sendRequestRescheduleEmail } from "@calcom/emails"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import { Person } from "@calcom/types/Calendar"; import { getCalendar } from "../../_utils/getCalendar"; -import { sendRequestRescheduleEmail } from "./emailManager"; type PersonAttendeeCommonFields = Pick; diff --git a/packages/app-store/wipemycalother/lib/templates/attendee-request-reschedule-email.ts b/packages/app-store/wipemycalother/lib/templates/attendee-request-reschedule-email.ts deleted file mode 100644 index a032a37be8..0000000000 --- a/packages/app-store/wipemycalother/lib/templates/attendee-request-reschedule-email.ts +++ /dev/null @@ -1,210 +0,0 @@ -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 { createEvent, DateArray, Person } from "ics"; - -import { getCancelLink } from "@calcom/lib/CalEventParser"; -import { CalendarEvent } from "@calcom/types/Calendar"; - -import BaseTemplate from "./base-template"; -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class AttendeeRequestRescheduledEmail extends BaseTemplate { - private metadata: { rescheduleLink: string }; - constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) { - super(calEvent); - this.metadata = metadata; - } - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.attendees[0].email]; - - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("requested_to_reschedule_subject_attendee", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - // @OVERRIDE - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CANCELLED", - method: "CANCEL", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - // @OVERRIDE - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}

-

- ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("request_reschedule_title_attendee")} -${this.calEvent.organizer.language.translate("request_reschedule_subtitle", { - organizer: this.calEvent.organizer.name, -})}, -${this.calEvent.cancellationReason && this.getReason()} -${this.getWhat()} -${this.getWhen()} -${this.getAdditionalNotes()} -${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("request_reschedule_title_attendee"), - this.calEvent.organizer.language.translate("request_reschedule_subtitle", { - organizer: this.calEvent.organizer.name, - }) - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getAdditionalNotes()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
- -
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/packages/app-store/wipemycalother/lib/templates/base-template.ts b/packages/app-store/wipemycalother/lib/templates/base-template.ts deleted file mode 100644 index 244804b48e..0000000000 --- a/packages/app-store/wipemycalother/lib/templates/base-template.ts +++ /dev/null @@ -1,417 +0,0 @@ -import dayjs, { 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 { createEvent, DateArray, Person } from "ics"; -import nodemailer from "nodemailer"; - -import { getAppName } from "@calcom/app-store/utils"; -import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser"; -import { getErrorFromUnknown } from "@calcom/lib/errors"; -import type { CalendarEvent } from "@calcom/types/Calendar"; - -import { serverConfig } from "../emailServerConfig"; -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, - linkIcon, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerScheduledEmail { - calEvent: CalendarEvent; - - constructor(calEvent: CalendarEvent) { - this.calEvent = calEvent; - } - - public sendEmail() { - new Promise((resolve, reject) => - nodemailer - .createTransport(this.getMailerOptions().transport) - .sendMail(this.getNodeMailerPayload(), (_err, info) => { - if (_err) { - const err = getErrorFromUnknown(_err); - this.printNodeMailerError(err); - reject(err); - } else { - resolve(info); - } - }) - ).catch((e) => console.error("sendEmail", e)); - return new Promise((resolve) => resolve("send mail async")); - } - - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CONFIRMED", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.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 { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("confirmed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - protected getMailerOptions() { - return { - transport: serverConfig.transport, - from: serverConfig.from, - }; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("new_event_scheduled")} -${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")} - -${getRichDescription(this.calEvent)} -`.trim(); - } - - protected printNodeMailerError(error: Error): void { - console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.calEvent.organizer.email, error); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("confirmed_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("checkCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("new_event_scheduled"), - this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees") - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getLocation()} - ${this.getDescription()} - ${this.getAdditionalNotes()} -
-
-
- -
-
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } - - protected getManageLink(): string { - const manageText = this.calEvent.organizer.language.translate("manage_this_event"); - return `

${this.calEvent.organizer.language.translate( - "need_to_reschedule_or_cancel" - )}

${manageText}

`; - } - - protected getWhat(): string { - return ` -
-

${this.calEvent.organizer.language.translate("what")}

-

${this.calEvent.type}

-
`; - } - - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}

-

- ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getWho(): string { - const attendees = this.calEvent.attendees - .map((attendee) => { - return `
${ - attendee?.name || `${this.calEvent.organizer.language.translate("guest")}` - } ${ - attendee.email - }
`; - }) - .join(""); - - const organizer = `
${ - this.calEvent.organizer.name - } - ${this.calEvent.organizer.language.translate( - "organizer" - )} ${this.calEvent.organizer.email}
`; - - return ` -

-
-

${this.calEvent.organizer.language.translate("who")}

- ${organizer + attendees} -
`; - } - - protected getAdditionalNotes(): string { - if (!this.calEvent.additionalNotes) return ""; - return ` -

-
-

${this.calEvent.organizer.language.translate("additional_notes")}

-

${ - this.calEvent.additionalNotes - }

-
- `; - } - - protected getDescription(): string { - if (!this.calEvent.description) return ""; - return ` -

-
-

${this.calEvent.organizer.language.translate("description")}

-

${ - this.calEvent.description - }

-
- `; - } - - protected getLocation(): string { - let providerName = this.calEvent.location && getAppName(this.calEvent.location); - - if (this.calEvent.location && this.calEvent.location.includes("integrations:")) { - const location = this.calEvent.location.split(":")[1]; - providerName = location[0].toUpperCase() + location.slice(1); - } - - // If location its a url, probably we should be validating it with a custom library - if (this.calEvent.location && /^https?:\/\//.test(this.calEvent.location)) { - providerName = this.calEvent.location; - } - - if (this.calEvent.videoCallData) { - const meetingId = this.calEvent.videoCallData.id; - const meetingPassword = this.calEvent.videoCallData.password; - const meetingUrl = this.calEvent.videoCallData.url; - - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${providerName} ${ - meetingUrl && - `` - }

- ${ - meetingId && - `
${this.calEvent.organizer.language.translate( - "meeting_id" - )}: ${meetingId}
` - } - ${ - meetingPassword && - `
${this.calEvent.organizer.language.translate( - "meeting_password" - )}: ${meetingPassword}
` - } - ${ - meetingUrl && - `
${this.calEvent.organizer.language.translate( - "meeting_url" - )}: ${meetingUrl}
` - } -
- `; - } - - if (this.calEvent.additionInformation?.hangoutLink) { - const hangoutLink: string = this.calEvent.additionInformation.hangoutLink; - - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${providerName} ${ - hangoutLink && - `` - }

- -
- `; - } - - return ` -

-
-

${this.calEvent.organizer.language.translate("where")}

-

${ - providerName || this.calEvent.location - }

-
- `; - } - - protected getTimezone(): string { - return this.calEvent.organizer.timeZone; - } - - protected getOrganizerStart(): Dayjs { - return dayjs(this.calEvent.startTime).tz(this.getTimezone()); - } - - protected getOrganizerEnd(): Dayjs { - return dayjs(this.calEvent.endTime).tz(this.getTimezone()); - } - - protected getReason(): string { - return ` -
-

${this.calEvent.attendees[0].language.translate("reschedule_reason")}

-

${this.calEvent.cancellationReason}

-
`; - } -} diff --git a/packages/app-store/wipemycalother/lib/templates/common/head.ts b/packages/app-store/wipemycalother/lib/templates/common/head.ts deleted file mode 100644 index be22403827..0000000000 --- a/packages/app-store/wipemycalother/lib/templates/common/head.ts +++ /dev/null @@ -1,91 +0,0 @@ -export const emailHead = (headerContent: string): string => { - return ` - - ${headerContent} - - - - - - - - - - - - - - - - - `; -}; diff --git a/packages/app-store/wipemycalother/lib/templates/common/index.ts b/packages/app-store/wipemycalother/lib/templates/common/index.ts deleted file mode 100644 index 686d871f91..0000000000 --- a/packages/app-store/wipemycalother/lib/templates/common/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { emailHead } from "./head"; -export { emailSchedulingBodyHeader } from "./scheduling-body-head"; -export { emailBodyLogo } from "./body-logo"; -export { emailScheduledBodyHeaderContent } from "./scheduling-body-head-content"; -export { emailSchedulingBodyDivider } from "./scheduling-body-divider"; -export { linkIcon } from "./link-icon"; diff --git a/packages/app-store/wipemycalother/lib/templates/common/link-icon.ts b/packages/app-store/wipemycalother/lib/templates/common/link-icon.ts deleted file mode 100644 index 434ae2cbcb..0000000000 --- a/packages/app-store/wipemycalother/lib/templates/common/link-icon.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants"; - -export const linkIcon = (): string => { - return IS_PRODUCTION ? BASE_URL + "/emails/linkIcon.png" : "https://app.cal.com/emails/linkIcon.png"; -}; diff --git a/packages/app-store/wipemycalother/lib/templates/common/scheduling-body-divider.ts b/packages/app-store/wipemycalother/lib/templates/common/scheduling-body-divider.ts deleted file mode 100644 index b8723c3e7b..0000000000 --- a/packages/app-store/wipemycalother/lib/templates/common/scheduling-body-divider.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const emailSchedulingBodyDivider = (): string => { - return ` - -
- - - - - - -
- -
- - - - - - -
-

-

- -
-
- -
-
- `; -}; diff --git a/packages/app-store/wipemycalother/lib/templates/common/scheduling-body-head-content.ts b/packages/app-store/wipemycalother/lib/templates/common/scheduling-body-head-content.ts deleted file mode 100644 index 31515fc760..0000000000 --- a/packages/app-store/wipemycalother/lib/templates/common/scheduling-body-head-content.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const emailScheduledBodyHeaderContent = (title: string, subtitle: string): string => { - return ` - -
- - - - - - -
- -
- - - - - - - - - -
-
${title}
-
-
${subtitle}
-
-
- -
-
- `; -}; diff --git a/packages/app-store/wipemycalother/lib/templates/organizer-request-reschedule-email.ts b/packages/app-store/wipemycalother/lib/templates/organizer-request-reschedule-email.ts deleted file mode 100644 index c79f335014..0000000000 --- a/packages/app-store/wipemycalother/lib/templates/organizer-request-reschedule-email.ts +++ /dev/null @@ -1,190 +0,0 @@ -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 { createEvent, DateArray, Person } from "ics"; - -import { getCancelLink } from "@calcom/lib/CalEventParser"; -import { CalendarEvent } from "@calcom/types/Calendar"; - -import BaseTemplate from "./base-template"; -import { - emailHead, - emailSchedulingBodyHeader, - emailBodyLogo, - emailScheduledBodyHeaderContent, - emailSchedulingBodyDivider, -} from "./common"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(localizedFormat); -dayjs.extend(toArray); - -export default class OrganizerRequestRescheduledEmail extends BaseTemplate { - private metadata: { rescheduleLink: string }; - constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) { - super(calEvent); - this.metadata = metadata; - } - protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - - return { - icalEvent: { - filename: "event.ics", - content: this.getiCalEventAsString(), - }, - from: `Cal.com <${this.getMailerOptions().from}>`, - to: toAddresses.join(","), - subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - })}`, - html: this.getHtmlBody(), - text: this.getTextBody(), - }; - } - - // @OVERRIDE - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calendso/ics", - title: this.calEvent.organizer.language.translate("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody(), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, - organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), - status: "CANCELLED", - method: "CANCEL", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - // @OVERRIDE - protected getWhen(): string { - return ` -

-
-

${this.calEvent.organizer.language.translate("when")}

-

- ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( - "YYYY" - )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )} (${this.getTimezone()}) -

-
`; - } - - protected getTextBody(): string { - return ` -${this.calEvent.organizer.language.translate("request_reschedule_title_organizer", { - attendee: this.calEvent.attendees[0].name, -})} -${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { - attendee: this.calEvent.attendees[0].name, -})}, -${this.calEvent.cancellationReason && this.getReason()} -${this.getWhat()} -${this.getWhen()} -${this.getLocation()} -${this.getAdditionalNotes()} -${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} -`.replace(/(<([^>]+)>)/gi, ""); - } - - protected getHtmlBody(): string { - const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( - "h:mma" - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("dddd").toLowerCase() - )}, ${this.calEvent.organizer.language.translate( - this.getOrganizerStart().format("MMMM").toLowerCase() - )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, - }); - - return ` - - - ${emailHead(headerContent)} - -
- ${emailSchedulingBodyHeader("calendarCircle")} - ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("request_reschedule_title_organizer", { - attendee: this.calEvent.attendees[0].name, - }), - this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { - attendee: this.calEvent.attendees[0].name, - }) - )} - ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.calEvent.cancellationReason && this.getReason()} - ${this.getWhat()} - ${this.getWhen()} - ${this.getWho()} - ${this.getAdditionalNotes()} -
-
-
- -
-
- ${emailBodyLogo()} - -
- - - `; - } -} diff --git a/packages/emails/README.md b/packages/emails/README.md new file mode 100644 index 0000000000..520b7e1ffd --- /dev/null +++ b/packages/emails/README.md @@ -0,0 +1,24 @@ +# JSX email templates + +- `components` Holds reusable patterns +- `templates` A template equals a type of email sent + +## Usage + +```ts +import { renderEmail } from "@calcom/emails"; + +renderEmail("TeamInviteEmail", */{ + language: t, + from: "teampro@example.com", + to: "pro@example.com", + teamName: "Team Pro", + joinLink: "https://cal.com", +}); +``` + +The first argument is the template name as defined inside `templates/index.ts`. The second argument are the template props. + +## Development + +You can use an API endpoint to preview the email HTML, there's already one on `/apps/web/pages/api/email.ts` feel free to change the template to the one you're currently working on. diff --git a/apps/web/lib/emails/email-manager.ts b/packages/emails/email-manager.ts similarity index 83% rename from apps/web/lib/emails/email-manager.ts rename to packages/emails/email-manager.ts index 163e6d9f8c..f195ac4c9f 100644 --- a/apps/web/lib/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -1,24 +1,24 @@ import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calendar"; -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 AttendeeLocationChangeEmail from "@lib/emails/templates/attendee-location-change-email"; -import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email"; -import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email"; -import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email"; -import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-email"; -import FeedbackEmail, { Feedback } from "@lib/emails/templates/feedback-email"; -import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email"; -import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-email"; -import OrganizerLocationChangeEmail from "@lib/emails/templates/organizer-location-change-email"; -import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email"; -import OrganizerRequestEmail from "@lib/emails/templates/organizer-request-email"; -import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-request-reminder-email"; -import OrganizerRequestRescheduleEmail from "@lib/emails/templates/organizer-request-reschedule-email"; -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 AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email"; +import AttendeeCancelledEmail from "./templates/attendee-cancelled-email"; +import AttendeeDeclinedEmail from "./templates/attendee-declined-email"; +import AttendeeLocationChangeEmail from "./templates/attendee-location-change-email"; +import AttendeeRequestEmail from "./templates/attendee-request-email"; +import AttendeeRequestRescheduledEmail from "./templates/attendee-request-reschedule-email"; +import AttendeeRescheduledEmail from "./templates/attendee-rescheduled-email"; +import AttendeeScheduledEmail from "./templates/attendee-scheduled-email"; +import FeedbackEmail, { Feedback } from "./templates/feedback-email"; +import ForgotPasswordEmail, { PasswordReset } from "./templates/forgot-password-email"; +import OrganizerCancelledEmail from "./templates/organizer-cancelled-email"; +import OrganizerLocationChangeEmail from "./templates/organizer-location-change-email"; +import OrganizerPaymentRefundFailedEmail from "./templates/organizer-payment-refund-failed-email"; +import OrganizerRequestEmail from "./templates/organizer-request-email"; +import OrganizerRequestReminderEmail from "./templates/organizer-request-reminder-email"; +import OrganizerRequestRescheduleEmail from "./templates/organizer-request-reschedule-email"; +import OrganizerRescheduledEmail from "./templates/organizer-rescheduled-email"; +import OrganizerScheduledEmail from "./templates/organizer-scheduled-email"; +import TeamInviteEmail, { TeamInvite } from "./templates/team-invite-email"; export const sendScheduledEmails = async (calEvent: CalendarEvent, recurringEvent: RecurringEvent = {}) => { const emailsToSend: Promise[] = []; diff --git a/packages/emails/index.ts b/packages/emails/index.ts new file mode 100644 index 0000000000..bf927192b9 --- /dev/null +++ b/packages/emails/index.ts @@ -0,0 +1,2 @@ +export * from "./email-manager"; +export { default as renderEmail } from "./src/renderEmail"; diff --git a/packages/emails/package.json b/packages/emails/package.json new file mode 100644 index 0000000000..b1e4d8b383 --- /dev/null +++ b/packages/emails/package.json @@ -0,0 +1,15 @@ +{ + "name": "@calcom/emails", + "version": "0.0.0", + "private": true, + "scripts": {}, + "dependencies": { + "@calcom/lib": "*", + "dayjs": "^1.11.2", + "next-i18next": "^11.0.0", + "rrule": "^2.6.9" + }, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/emails/src/components/BaseEmailHtml.tsx b/packages/emails/src/components/BaseEmailHtml.tsx new file mode 100644 index 0000000000..adeb44c7e5 --- /dev/null +++ b/packages/emails/src/components/BaseEmailHtml.tsx @@ -0,0 +1,188 @@ +/* eslint-disable @next/next/no-head-element */ +import BaseTable from "./BaseTable"; +import EmailBodyLogo from "./EmailBodyLogo"; +import EmailHead from "./EmailHead"; +import EmailScheduledBodyHeaderContent from "./EmailScheduledBodyHeaderContent"; +import EmailSchedulingBodyDivider from "./EmailSchedulingBodyDivider"; +import EmailSchedulingBodyHeader, { BodyHeadType } from "./EmailSchedulingBodyHeader"; +import RawHtml from "./RawHtml"; +import Row from "./Row"; + +const Html = (props: { children: React.ReactNode }) => ( + <> + `} /> + {props.children} + +); + +export const BaseEmailHtml = (props: { + children: React.ReactNode; + callToAction?: React.ReactNode; + subject: string; + title?: string; + subtitle?: string; + headerType?: BodyHeadType; +}) => { + return ( + + + +
+ + + + {props.headerType && } + {props.title && } + {(props.headerType || props.title || props.subtitle) && } + +
`} + /> +
+ +
+
`} + /> +
+ + + {props.callToAction && } +
`} + /> +
+ +
+ + + +
`} + /> +
+ +
+
+ {props.children} +
+
`} /> +
+ + + +
`} + /> + +
+ +
+ + + + + + + + + + + )} +
`} + /> + {props.callToAction && ( +
+ +
+ {props.callToAction} +
+
+
`} /> +
`} /> +
+ + + ); +}; diff --git a/packages/emails/src/components/BaseTable.tsx b/packages/emails/src/components/BaseTable.tsx new file mode 100644 index 0000000000..58dc4c983d --- /dev/null +++ b/packages/emails/src/components/BaseTable.tsx @@ -0,0 +1,12 @@ +type BaseTableProps = React.DetailedHTMLProps, HTMLTableElement> & + Partial>; + +const BaseTable = ({ children, ...rest }: BaseTableProps) => ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + + {children} +
+); + +export default BaseTable; diff --git a/packages/emails/src/components/CallToAction.tsx b/packages/emails/src/components/CallToAction.tsx new file mode 100644 index 0000000000..448f3cfaf1 --- /dev/null +++ b/packages/emails/src/components/CallToAction.tsx @@ -0,0 +1,30 @@ +import { LinkIcon } from "./LinkIcon"; + +export const CallToAction = (props: { label: string; href: string }) => ( +

+ + {props.label} + +

+); diff --git a/packages/emails/src/components/CallToActionTable.tsx b/packages/emails/src/components/CallToActionTable.tsx new file mode 100644 index 0000000000..5ffac164b5 --- /dev/null +++ b/packages/emails/src/components/CallToActionTable.tsx @@ -0,0 +1,24 @@ +export const CallToActionTable = (props: { children: React.ReactNode }) => ( + + + + + + +
+ {props.children} +
+); diff --git a/packages/emails/src/components/CustomInputs.tsx b/packages/emails/src/components/CustomInputs.tsx new file mode 100644 index 0000000000..a3e25d7c38 --- /dev/null +++ b/packages/emails/src/components/CustomInputs.tsx @@ -0,0 +1,18 @@ +import type { CalendarEvent } from "@calcom/types/Calendar"; + +import { Info } from "./Info"; + +export function CustomInputs(props: { calEvent: CalendarEvent }) { + const { customInputs } = props.calEvent; + if (!customInputs) return null; + + return ( + <> + {Object.keys(customInputs).map((key) => + customInputs[key] !== "" ? ( + + ) : null + )} + + ); +} diff --git a/packages/emails/src/components/EmailBodyLogo.tsx b/packages/emails/src/components/EmailBodyLogo.tsx new file mode 100644 index 0000000000..0cd38e2a3b --- /dev/null +++ b/packages/emails/src/components/EmailBodyLogo.tsx @@ -0,0 +1,81 @@ +import { BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants"; + +import RawHtml from "./RawHtml"; +import Row from "./Row"; + +const CommentIE = ({ html = "" }) => ${html}`} />; + +const EmailBodyLogo = () => { + const image = IS_PRODUCTION + ? BASE_URL + "/emails/CalLogo@2x.png" + : "https://app.cal.com/emails/CalLogo@2x.png"; + + return ( + <> + `} + /> +
+ +
+ + + + +
+
`} + /> +
+ +
+ + + + + +
`} /> + + + + + ); +}; + +export default EmailBodyLogo; diff --git a/packages/emails/src/components/EmailCommonDivider.tsx b/packages/emails/src/components/EmailCommonDivider.tsx new file mode 100644 index 0000000000..38a1f98535 --- /dev/null +++ b/packages/emails/src/components/EmailCommonDivider.tsx @@ -0,0 +1,71 @@ +import RawHtml from "./RawHtml"; +import Row from "./Row"; + +const EmailCommonDivider = ({ + children, + mutipleRows = false, + headStyles, +}: { + children: React.ReactNode; + mutipleRows?: boolean; + headStyles?: React.DetailedHTMLProps< + React.TdHTMLAttributes, + HTMLTableCellElement + >["style"]; +}) => { + return ( + <> + + + + + ); +}; + +export default EmailCommonDivider; diff --git a/packages/emails/src/components/EmailHead.tsx b/packages/emails/src/components/EmailHead.tsx new file mode 100644 index 0000000000..484fa0031b --- /dev/null +++ b/packages/emails/src/components/EmailHead.tsx @@ -0,0 +1,93 @@ +/* eslint-disable @next/next/no-head-element */ +import RawHtml from "./RawHtml"; + +const EmailHead = ({ title = "" }) => { + return ( + + {title} + `} + /> + + + + `} + /> + `} + /> + + `} + /> + + + + + ); +}; + +export default EmailHead; diff --git a/packages/emails/src/components/EmailScheduledBodyHeaderContent.tsx b/packages/emails/src/components/EmailScheduledBodyHeaderContent.tsx new file mode 100644 index 0000000000..8e36765a9d --- /dev/null +++ b/packages/emails/src/components/EmailScheduledBodyHeaderContent.tsx @@ -0,0 +1,48 @@ +import EmailCommonDivider from "./EmailCommonDivider"; + +const EmailScheduledBodyHeaderContent = (props: { title: string; subtitle?: string }) => ( + + + + + {props.subtitle && ( + + + + )} + +); + +export default EmailScheduledBodyHeaderContent; diff --git a/packages/emails/src/components/EmailSchedulingBodyDivider.tsx b/packages/emails/src/components/EmailSchedulingBodyDivider.tsx new file mode 100644 index 0000000000..8375b7b5da --- /dev/null +++ b/packages/emails/src/components/EmailSchedulingBodyDivider.tsx @@ -0,0 +1,28 @@ +import EmailCommonDivider from "./EmailCommonDivider"; +import RawHtml from "./RawHtml"; + +export const EmailSchedulingBodyDivider = () => ( + + + +); + +export default EmailSchedulingBodyDivider; diff --git a/packages/emails/src/components/EmailSchedulingBodyHeader.tsx b/packages/emails/src/components/EmailSchedulingBodyHeader.tsx new file mode 100644 index 0000000000..5a04bd941c --- /dev/null +++ b/packages/emails/src/components/EmailSchedulingBodyHeader.tsx @@ -0,0 +1,63 @@ +import { BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants"; + +import EmailCommonDivider from "./EmailCommonDivider"; +import Row from "./Row"; + +export type BodyHeadType = "checkCircle" | "xCircle" | "calendarCircle"; + +export const getHeadImage = (headerType: BodyHeadType): string => { + switch (headerType) { + case "checkCircle": + return IS_PRODUCTION + ? BASE_URL + "/emails/checkCircle@2x.png" + : "https://app.cal.com/emails/checkCircle@2x.png"; + case "xCircle": + return IS_PRODUCTION + ? BASE_URL + "/emails/xCircle@2x.png" + : "https://app.cal.com/emails/xCircle@2x.png"; + case "calendarCircle": + return IS_PRODUCTION + ? BASE_URL + "/emails/calendarCircle@2x.png" + : "https://app.cal.com/emails/calendarCircle@2x.png"; + } +}; + +const EmailSchedulingBodyHeader = (props: { headerType: BodyHeadType }) => { + const image = getHeadImage(props.headerType); + + return ( + <> + + + + + + + ); +}; + +export default EmailSchedulingBodyHeader; diff --git a/packages/emails/src/components/Info.tsx b/packages/emails/src/components/Info.tsx new file mode 100644 index 0000000000..07b4ae81b1 --- /dev/null +++ b/packages/emails/src/components/Info.tsx @@ -0,0 +1,30 @@ +const Spacer = () =>

; + +export const Info = (props: { + label: string; + description: React.ReactNode | undefined | null; + extraInfo?: React.ReactNode; + withSpacer?: boolean; + lineThrough?: boolean; +}) => { + if (!props.description) return null; + return ( + <> + {props.withSpacer && } +

+

{props.label}

+

+ {props.description} +

+ {props.extraInfo} +
+ + ); +}; diff --git a/packages/emails/src/components/LinkIcon.tsx b/packages/emails/src/components/LinkIcon.tsx new file mode 100644 index 0000000000..2c4d0e12e3 --- /dev/null +++ b/packages/emails/src/components/LinkIcon.tsx @@ -0,0 +1,9 @@ +import { BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants"; + +export const LinkIcon = () => ( + +); diff --git a/packages/emails/src/components/LocationInfo.tsx b/packages/emails/src/components/LocationInfo.tsx new file mode 100644 index 0000000000..302b3c528c --- /dev/null +++ b/packages/emails/src/components/LocationInfo.tsx @@ -0,0 +1,124 @@ +import { getAppName } from "@calcom/app-store/utils"; +import type { CalendarEvent } from "@calcom/types/Calendar"; + +import { Info } from "./Info"; +import { LinkIcon } from "./LinkIcon"; + +export function LocationInfo(props: { calEvent: CalendarEvent }) { + const t = props.calEvent.attendees[0].language.translate; + let providerName = props.calEvent.location && getAppName(props.calEvent.location); + + if (props.calEvent.location && props.calEvent.location.includes("integrations:")) { + const location = props.calEvent.location.split(":")[1]; + providerName = location[0].toUpperCase() + location.slice(1); + } + + // If location its a url, probably we should be validating it with a custom library + if (props.calEvent.location && /^https?:\/\//.test(props.calEvent.location)) { + providerName = props.calEvent.location; + } + + if (props.calEvent.videoCallData) { + const meetingId = props.calEvent.videoCallData.id; + const meetingPassword = props.calEvent.videoCallData.password; + const meetingUrl = props.calEvent.videoCallData.url; + + return ( + + {providerName} + {meetingUrl && ( + + + + )} + + } + extraInfo={ + <> + {meetingId && ( +
+ <> + {t("meeting_id")}: {meetingId} + +
+ )} + {meetingPassword && ( +
+ <> + {t("meeting_password")}: {meetingPassword} + +
+ )} + {meetingUrl && ( +
+ <> + {t("meeting_url")}:{" "} + + {meetingUrl} + + +
+ )} + + } + /> + ); + } + + if (props.calEvent.additionInformation?.hangoutLink) { + const hangoutLink: string = props.calEvent.additionInformation.hangoutLink; + + return ( + + {providerName} + {hangoutLink && ( + + + + )} + + } + extraInfo={ + providerName === "Zoom" || providerName === "Google" ? ( +

+ <>{t("meeting_url_provided_after_confirmed")} +

+ ) : null + } + /> + ); + } + + return ( + + <>{t("meeting_url_provided_after_confirmed")} +

+ ) : null + } + /> + ); +} diff --git a/packages/emails/src/components/ManageLink.tsx b/packages/emails/src/components/ManageLink.tsx new file mode 100644 index 0000000000..b5db552c65 --- /dev/null +++ b/packages/emails/src/components/ManageLink.tsx @@ -0,0 +1,30 @@ +import { getCancelLink } from "@calcom/lib/CalEventParser"; +import type { CalendarEvent, Person } from "@calcom/types/Calendar"; + +export function ManageLink(props: { calEvent: CalendarEvent; attendee: Person }) { + // Only the original attendee can make changes to the event + // Guests cannot + if (props.attendee.email !== props.calEvent.attendees[0].email) return null; + const t = props.attendee.language.translate; + + return ( +
+

+ <>{t("need_to_reschedule_or_cancel")} +

+

+ + <>{t("manage_this_event")} + +

+
+ ); +} diff --git a/packages/emails/src/components/RawHtml.tsx b/packages/emails/src/components/RawHtml.tsx new file mode 100644 index 0000000000..1cb2a7e2e4 --- /dev/null +++ b/packages/emails/src/components/RawHtml.tsx @@ -0,0 +1,5 @@ +const RawHtml = ({ html = "" }) => ( +
`} + /> +
+ +
+
`} + /> +
+ + {children} + +
+
`} /> +
+
+ {props.title} +
+
+
+ {props.subtitle} +
+
+

+
 
`} + /> +
+ + + +