Emails Revamp (#1201)
* refactor: emails (WIP) * wip * wip * refactor: calendarClient * chore: remove comment * feat: new templates * feat: more templates (wip) * feat: email templates wip * feat: email templates wip * feat: prepare for testing * For testing stripe integration * Uses imported BASE_URL * Fixes types * use BASE_URL Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
36767afbf5
commit
8322e5c8d1
57 changed files with 4950 additions and 2572 deletions
|
@ -4,13 +4,18 @@ import { JsonValue } from "type-fest";
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import EventOrganizerRefundFailedMail from "@lib/emails/EventOrganizerRefundFailedMail";
|
||||
import EventPaymentMail from "@lib/emails/EventPaymentMail";
|
||||
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { createPaymentLink } from "./client";
|
||||
|
||||
export type PaymentInfo = {
|
||||
link?: string | null;
|
||||
reason?: string | null;
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
export type PaymentData = Stripe.Response<Stripe.PaymentIntent> & {
|
||||
stripe_publishable_key: string;
|
||||
stripeAccount: string;
|
||||
|
@ -74,15 +79,16 @@ export async function handlePayment(
|
|||
},
|
||||
});
|
||||
|
||||
const mail = new EventPaymentMail(
|
||||
createPaymentLink({
|
||||
paymentUid: payment.uid,
|
||||
name: booking.user?.name,
|
||||
date: booking.startTime.toISOString(),
|
||||
}),
|
||||
evt
|
||||
);
|
||||
await mail.sendEmail();
|
||||
await sendAwaitingPaymentEmail({
|
||||
...evt,
|
||||
paymentInfo: {
|
||||
link: createPaymentLink({
|
||||
paymentUid: payment.uid,
|
||||
name: booking.user?.name,
|
||||
date: booking.startTime.toISOString(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return payment;
|
||||
}
|
||||
|
@ -153,11 +159,10 @@ export async function refund(
|
|||
|
||||
async function handleRefundError(opts: { event: CalendarEvent; reason: string; paymentId: string }) {
|
||||
console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
|
||||
try {
|
||||
await new EventOrganizerRefundFailedMail(opts.event, opts.reason, opts.paymentId).sendEmail();
|
||||
} catch (e) {
|
||||
console.error("Error while sending refund error email", e);
|
||||
}
|
||||
await sendOrganizerPaymentRefundFailedEmail({
|
||||
...opts.event,
|
||||
paymentInfo: { reason: opts.reason, id: opts.paymentId },
|
||||
});
|
||||
}
|
||||
|
||||
export default stripe;
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { stringify } from "querystring";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { BASE_URL } from "@lib/config/constants";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
const client_id = process.env.STRIPE_CLIENT_ID;
|
||||
|
@ -27,7 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
},
|
||||
});
|
||||
|
||||
const redirect_uri = encodeURI(process.env.BASE_URL + "/api/integrations/stripepayment/callback");
|
||||
const redirect_uri = encodeURI(BASE_URL + "/api/integrations/stripepayment/callback");
|
||||
const stripeConnectParams = {
|
||||
client_id,
|
||||
scope: "read_write",
|
||||
|
|
|
@ -1,151 +1,15 @@
|
|||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
|
||||
import { CalendarEvent } from "./calendarClient";
|
||||
import { stripHtml } from "./emails/helpers";
|
||||
import { BASE_URL } from "./config/constants";
|
||||
|
||||
const translator = short();
|
||||
|
||||
export default class CalEventParser {
|
||||
protected calEvent: CalendarEvent;
|
||||
export const getUid = (calEvent: CalendarEvent) => {
|
||||
return calEvent.uid ?? translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
|
||||
};
|
||||
|
||||
constructor(calEvent: CalendarEvent) {
|
||||
this.calEvent = calEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to reschedule the given booking.
|
||||
*/
|
||||
public getRescheduleLink(): string {
|
||||
return process.env.BASE_URL + "/reschedule/" + this.getUid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to cancel the given booking.
|
||||
*/
|
||||
public getCancelLink(): string {
|
||||
return process.env.BASE_URL + "/cancel/" + this.getUid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique identifier for the given calendar event.
|
||||
*/
|
||||
public getUid(): string {
|
||||
return this.calEvent.uid ?? translator.fromUUID(uuidv5(JSON.stringify(this.calEvent), uuidv5.URL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a footer section with links to change the event (as HTML).
|
||||
*/
|
||||
public getChangeEventFooterHtml(): string {
|
||||
return `<p style="color: #4b5563; margin-top: 20px;">${this.calEvent.language(
|
||||
"need_to_make_a_change"
|
||||
)} <a href="${this.getCancelLink()}" style="color: #161e2e;">${this.calEvent.language(
|
||||
"cancel"
|
||||
)}</a> ${this.calEvent
|
||||
.language("or")
|
||||
.toLowerCase()} <a href="${this.getRescheduleLink()}" style="color: #161e2e;">${this.calEvent.language(
|
||||
"reschedule"
|
||||
)}</a></p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a footer section with links to change the event (as plain text).
|
||||
*/
|
||||
public getChangeEventFooter(): string {
|
||||
return stripHtml(this.getChangeEventFooterHtml());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an extended description with all important information (as HTML).
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
public getRichDescriptionHtml(): string {
|
||||
// This odd indentation is necessary because otherwise the leading tabs will be applied into the event description.
|
||||
return (
|
||||
`
|
||||
<strong>${this.calEvent.language("event_type")}:</strong><br />${this.calEvent.type}<br />
|
||||
<strong>${this.calEvent.language("invitee_email")}:</strong><br /><a href="mailto:${
|
||||
this.calEvent.attendees[0].email
|
||||
}">${this.calEvent.attendees[0].email}</a><br />
|
||||
` +
|
||||
(this.getLocation()
|
||||
? `<strong>${this.calEvent.language("location")}:</strong><br />${this.getLocation()}<br />
|
||||
`
|
||||
: "") +
|
||||
`<strong>${this.calEvent.language("invitee_timezone")}:</strong><br />${
|
||||
this.calEvent.attendees[0].timeZone
|
||||
}<br />
|
||||
<strong>${this.calEvent.language("additional_notes")}:</strong><br />${this.getDescriptionText()}<br />` +
|
||||
this.getChangeEventFooterHtml()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally returns the event's location. When VideoCallData is set,
|
||||
* it returns the meeting url. Otherwise, the regular location is returned.
|
||||
* For Daily video calls returns the direct link
|
||||
* @protected
|
||||
*/
|
||||
protected getLocation(): string | null | undefined {
|
||||
const isDaily = this.calEvent.location === "integrations:daily";
|
||||
if (this.calEvent.videoCallData) {
|
||||
return this.calEvent.videoCallData.url;
|
||||
}
|
||||
if (isDaily) {
|
||||
return process.env.BASE_URL + "/call/" + this.getUid();
|
||||
}
|
||||
return this.calEvent.location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the event's description text. If VideoCallData is set, it prepends
|
||||
* some video call information before the text as well.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getDescriptionText(): string | null | undefined {
|
||||
if (this.calEvent.videoCallData) {
|
||||
return `
|
||||
${this.calEvent.language("integration_meeting_id", {
|
||||
integrationName: getIntegrationName(this.calEvent.videoCallData.type),
|
||||
meetingId: this.calEvent.videoCallData.id,
|
||||
})}
|
||||
${this.calEvent.language("password")}: ${this.calEvent.videoCallData.password}
|
||||
${this.calEvent.description}`;
|
||||
}
|
||||
return this.calEvent.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an extended description with all important information (as plain text).
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
public getRichDescription(): string {
|
||||
return stripHtml(this.getRichDescriptionHtml());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a calendar event with rich description.
|
||||
*/
|
||||
public asRichEvent(): CalendarEvent {
|
||||
const eventCopy: CalendarEvent = { ...this.calEvent };
|
||||
eventCopy.description = this.getRichDescriptionHtml();
|
||||
eventCopy.location = this.getLocation();
|
||||
return eventCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a calendar event with rich description as plain text.
|
||||
*/
|
||||
public asRichEventPlain(): CalendarEvent {
|
||||
const eventCopy: CalendarEvent = { ...this.calEvent };
|
||||
eventCopy.description = this.getRichDescription();
|
||||
eventCopy.location = this.getLocation();
|
||||
return eventCopy;
|
||||
}
|
||||
}
|
||||
export const getCancelLink = (calEvent: CalendarEvent) => {
|
||||
return BASE_URL + "/cancel/" + getUid(calEvent);
|
||||
};
|
||||
|
|
|
@ -1,128 +1,26 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
|
||||
import { Credential, Prisma, SelectedCalendar } from "@prisma/client";
|
||||
import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
|
||||
import { Auth, calendar_v3, google } from "googleapis";
|
||||
import { Credential, SelectedCalendar } from "@prisma/client";
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import { PaymentInfo } from "@ee/lib/stripe/server";
|
||||
|
||||
import { getUid } from "@lib/CalEventParser";
|
||||
import { Event, EventResult } from "@lib/events/EventManager";
|
||||
import { AppleCalendar } from "@lib/integrations/Apple/AppleCalendarAdapter";
|
||||
import { CalDavCalendar } from "@lib/integrations/CalDav/CalDavCalendarAdapter";
|
||||
import {
|
||||
GoogleCalendarApiAdapter,
|
||||
ConferenceData,
|
||||
} from "@lib/integrations/GoogleCalendar/GoogleCalendarApiAdapter";
|
||||
import {
|
||||
Office365CalendarApiAdapter,
|
||||
BufferedBusyTime,
|
||||
} from "@lib/integrations/Office365Calendar/Office365CalendarApiAdapter";
|
||||
import logger from "@lib/logger";
|
||||
import { VideoCallData } from "@lib/videoClient";
|
||||
|
||||
import CalEventParser from "./CalEventParser";
|
||||
import EventOrganizerMail from "./emails/EventOrganizerMail";
|
||||
import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail";
|
||||
import { AppleCalendar } from "./integrations/Apple/AppleCalendarAdapter";
|
||||
import { CalDavCalendar } from "./integrations/CalDav/CalDavCalendarAdapter";
|
||||
import prisma from "./prisma";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] });
|
||||
|
||||
const googleAuth = (credential: Credential) => {
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS!).web;
|
||||
const myGoogleAuth = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
|
||||
const googleCredentials = credential.key as Auth.Credentials;
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
|
||||
// FIXME - type errors IDK Why this is a protected method ¯\_(ツ)_/¯
|
||||
const isExpired = () => myGoogleAuth.isTokenExpiring();
|
||||
|
||||
const refreshAccessToken = () =>
|
||||
myGoogleAuth
|
||||
// FIXME - type errors IDK Why this is a protected method ¯\_(ツ)_/¯
|
||||
.refreshToken(googleCredentials.refresh_token)
|
||||
.then((res: GetTokenResponse) => {
|
||||
const token = res.res?.data;
|
||||
googleCredentials.access_token = token.access_token;
|
||||
googleCredentials.expiry_date = token.expiry_date;
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: googleCredentials as Prisma.InputJsonValue,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error refreshing google token", err);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
|
||||
return {
|
||||
getToken: () => (!isExpired() ? Promise.resolve(myGoogleAuth) : refreshAccessToken()),
|
||||
};
|
||||
};
|
||||
|
||||
function handleErrorsJson(response: Response) {
|
||||
if (!response.ok) {
|
||||
response.json().then((e) => console.error("O365 Error", e));
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function handleErrorsRaw(response: Response) {
|
||||
if (!response.ok) {
|
||||
response.text().then((e) => console.error("O365 Error", e));
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
type O365AuthCredentials = {
|
||||
expiry_date: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
const o365Auth = (credential: Credential) => {
|
||||
const isExpired = (expiryDate: number) => expiryDate < Math.round(+new Date() / 1000);
|
||||
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
||||
|
||||
const refreshAccessToken = (refreshToken: string) => {
|
||||
return fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
// FIXME types - IDK how to type this TBH
|
||||
body: new URLSearchParams({
|
||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||
client_id: process.env.MS_GRAPH_CLIENT_ID,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
client_secret: process.env.MS_GRAPH_CLIENT_SECRET,
|
||||
}),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody) => {
|
||||
o365AuthCredentials.access_token = responseBody.access_token;
|
||||
o365AuthCredentials.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in);
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: o365AuthCredentials,
|
||||
},
|
||||
})
|
||||
.then(() => o365AuthCredentials.access_token);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
getToken: () =>
|
||||
!isExpired(o365AuthCredentials.expiry_date)
|
||||
? Promise.resolve(o365AuthCredentials.access_token)
|
||||
: refreshAccessToken(o365AuthCredentials.refresh_token),
|
||||
};
|
||||
};
|
||||
|
||||
export type Person = { name: string; email: string; timeZone: string };
|
||||
|
||||
export interface EntryPoint {
|
||||
|
@ -158,20 +56,16 @@ export interface CalendarEvent {
|
|||
conferenceData?: ConferenceData;
|
||||
language: TFunction;
|
||||
additionInformation?: AdditionInformation;
|
||||
/** If this property exist it we can assume it's a reschedule/update */
|
||||
uid?: string | null;
|
||||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: PaymentInfo | null;
|
||||
}
|
||||
|
||||
export interface ConferenceData {
|
||||
createRequest: calendar_v3.Schema$CreateConferenceRequest;
|
||||
}
|
||||
export interface IntegrationCalendar extends Partial<SelectedCalendar> {
|
||||
primary?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
type BufferedBusyTime = { start: string; end: string };
|
||||
export interface CalendarApiAdapter {
|
||||
createEvent(event: CalendarEvent): Promise<Event>;
|
||||
|
||||
|
@ -188,381 +82,12 @@ export interface CalendarApiAdapter {
|
|||
listCalendars(): Promise<IntegrationCalendar[]>;
|
||||
}
|
||||
|
||||
const MicrosoftOffice365Calendar = (credential: Credential): CalendarApiAdapter => {
|
||||
const auth = o365Auth(credential);
|
||||
|
||||
const translateEvent = (event: CalendarEvent) => {
|
||||
return {
|
||||
subject: event.title,
|
||||
body: {
|
||||
contentType: "HTML",
|
||||
content: event.description,
|
||||
},
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees.map((attendee) => ({
|
||||
emailAddress: {
|
||||
address: attendee.email,
|
||||
name: attendee.name,
|
||||
},
|
||||
type: "required",
|
||||
})),
|
||||
location: event.location ? { displayName: event.location } : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const integrationType = "office365_calendar";
|
||||
|
||||
function listCalendars(): Promise<IntegrationCalendar[]> {
|
||||
return auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendars", {
|
||||
method: "get",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody: { value: OfficeCalendar[] }) => {
|
||||
return responseBody.value.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No Id",
|
||||
integration: integrationType,
|
||||
name: cal.name ?? "No calendar name",
|
||||
primary: cal.isDefaultCalendar ?? false,
|
||||
};
|
||||
return calendar;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getAvailability: (dateFrom, dateTo, selectedCalendars) => {
|
||||
const filter = `?startdatetime=${encodeURIComponent(dateFrom)}&enddatetime=${encodeURIComponent(
|
||||
dateTo
|
||||
)}`;
|
||||
return auth
|
||||
.getToken()
|
||||
.then((accessToken) => {
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === integrationType)
|
||||
.map((e) => e.externalId)
|
||||
.filter(Boolean);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return (
|
||||
selectedCalendarIds.length === 0
|
||||
? listCalendars().then((cals) => cals.map((e) => e.externalId).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
).then((ids) => {
|
||||
const requests = ids.map((calendarId, id) => ({
|
||||
id,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Prefer: 'outlook.timezone="Etc/GMT"',
|
||||
},
|
||||
url: `/me/calendars/${calendarId}/calendarView${filter}`,
|
||||
}));
|
||||
|
||||
type BatchResponse = {
|
||||
responses: SubResponse[];
|
||||
};
|
||||
type SubResponse = {
|
||||
body: { value: { start: { dateTime: string }; end: { dateTime: string } }[] };
|
||||
};
|
||||
|
||||
return fetch("https://graph.microsoft.com/v1.0/$batch", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ requests }),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody: BatchResponse) =>
|
||||
responseBody.responses.reduce(
|
||||
(acc: BufferedBusyTime[], subResponse) =>
|
||||
acc.concat(
|
||||
subResponse.body.value.map((evt) => {
|
||||
return {
|
||||
start: evt.start.dateTime + "Z",
|
||||
end: evt.end.dateTime + "Z",
|
||||
};
|
||||
})
|
||||
),
|
||||
[]
|
||||
)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return Promise.reject([]);
|
||||
});
|
||||
},
|
||||
createEvent: (event: CalendarEvent) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendar/events", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody) => ({
|
||||
...responseBody,
|
||||
disableConfirmationEmail: true,
|
||||
}))
|
||||
),
|
||||
deleteEvent: (uid: string) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
}).then(handleErrorsRaw)
|
||||
),
|
||||
updateEvent: (uid: string, event: CalendarEvent) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsRaw)
|
||||
),
|
||||
listCalendars,
|
||||
};
|
||||
};
|
||||
|
||||
const GoogleCalendar = (credential: Credential): CalendarApiAdapter => {
|
||||
const auth = googleAuth(credential);
|
||||
const integrationType = "google_calendar";
|
||||
|
||||
return {
|
||||
getAvailability: (dateFrom, dateTo, selectedCalendars) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === integrationType)
|
||||
.map((e) => e.externalId);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
(selectedCalendarIds.length === 0
|
||||
? calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
)
|
||||
.then((calsIds) => {
|
||||
calendar.freebusy.query(
|
||||
{
|
||||
requestBody: {
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id: id })),
|
||||
},
|
||||
},
|
||||
(err, apires) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
// @ts-ignore FIXME
|
||||
resolve(Object.values(apires.data.calendars).flatMap((item) => item["busy"]));
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
),
|
||||
createEvent: (event: CalendarEvent) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: event.description,
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: false,
|
||||
overrides: [{ method: "email", minutes: 10 }],
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = event.location;
|
||||
}
|
||||
|
||||
if (event.conferenceData && event.location === "integrations:google:meet") {
|
||||
payload["conferenceData"] = event.conferenceData;
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: "primary",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err || !event?.data) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
// @ts-ignore FIXME
|
||||
return resolve(event.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
updateEvent: (uid: string, event: CalendarEvent) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: event.description,
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: false,
|
||||
overrides: [{ method: "email", minutes: 10 }],
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = event.location;
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.update(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
requestBody: payload,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
deleteEvent: (uid: string) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.delete(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
listCalendars: () =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => {
|
||||
resolve(
|
||||
cals.data.items?.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No id",
|
||||
integration: integrationType,
|
||||
name: cal.summary ?? "No name",
|
||||
primary: cal.primary ?? false,
|
||||
};
|
||||
return calendar;
|
||||
}) || []
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
function getCalendarAdapterOrNull(credential: Credential): CalendarApiAdapter | null {
|
||||
switch (credential.type) {
|
||||
case "google_calendar":
|
||||
return GoogleCalendar(credential);
|
||||
return GoogleCalendarApiAdapter(credential);
|
||||
case "office365_calendar":
|
||||
return MicrosoftOffice365Calendar(credential);
|
||||
return Office365CalendarApiAdapter(credential);
|
||||
case "caldav_calendar":
|
||||
// FIXME types wrong & type casting should not be needed
|
||||
return new CalDavCalendar(credential) as never as CalendarApiAdapter;
|
||||
|
@ -581,9 +106,9 @@ const calendars = (withCredentials: Credential[]): CalendarApiAdapter[] =>
|
|||
.map((cred) => {
|
||||
switch (cred.type) {
|
||||
case "google_calendar":
|
||||
return GoogleCalendar(cred);
|
||||
return GoogleCalendarApiAdapter(cred);
|
||||
case "office365_calendar":
|
||||
return MicrosoftOffice365Calendar(cred);
|
||||
return Office365CalendarApiAdapter(cred);
|
||||
case "caldav_calendar":
|
||||
return new CalDavCalendar(cred);
|
||||
case "apple_calendar":
|
||||
|
@ -616,25 +141,13 @@ const listCalendars = (withCredentials: Credential[]) =>
|
|||
results.reduce((acc, calendars) => acc.concat(calendars), []).filter((c) => c != null)
|
||||
);
|
||||
|
||||
const createEvent = async (
|
||||
credential: Credential,
|
||||
calEvent: CalendarEvent,
|
||||
noMail: boolean | null = false
|
||||
): Promise<EventResult> => {
|
||||
const parser: CalEventParser = new CalEventParser(calEvent);
|
||||
const uid: string = parser.getUid();
|
||||
/*
|
||||
* Matching the credential type is a workaround because the office calendar simply strips away newlines (\n and \r).
|
||||
* We need HTML there. Google Calendar understands newlines and Apple Calendar cannot show HTML, so no HTML should
|
||||
* be used for Google and Apple Calendar.
|
||||
*/
|
||||
const richEvent: CalendarEvent = parser.asRichEventPlain();
|
||||
|
||||
const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
|
||||
const uid: string = getUid(calEvent);
|
||||
let success = true;
|
||||
|
||||
const creationResult = credential
|
||||
? await calendars([credential])[0]
|
||||
.createEvent(richEvent)
|
||||
.createEvent(calEvent)
|
||||
.catch((e) => {
|
||||
log.error("createEvent failed", e, calEvent);
|
||||
success = false;
|
||||
|
@ -651,26 +164,6 @@ const createEvent = async (
|
|||
};
|
||||
}
|
||||
|
||||
const metadata: AdditionInformation = {};
|
||||
if (creationResult) {
|
||||
// TODO: Handle created event metadata more elegantly
|
||||
metadata.hangoutLink = creationResult.hangoutLink;
|
||||
metadata.conferenceData = creationResult.conferenceData;
|
||||
metadata.entryPoints = creationResult.entryPoints;
|
||||
}
|
||||
|
||||
calEvent.additionInformation = metadata;
|
||||
|
||||
if (!noMail) {
|
||||
const organizerMail = new EventOrganizerMail(calEvent);
|
||||
|
||||
try {
|
||||
await organizerMail.sendEmail();
|
||||
} catch (e) {
|
||||
console.error("organizerMail.sendEmail failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
|
@ -683,27 +176,23 @@ const createEvent = async (
|
|||
const updateEvent = async (
|
||||
credential: Credential,
|
||||
calEvent: CalendarEvent,
|
||||
noMail: boolean | null = false,
|
||||
bookingRefUid: string | null
|
||||
): Promise<EventResult> => {
|
||||
const parser: CalEventParser = new CalEventParser(calEvent);
|
||||
const uid = parser.getUid();
|
||||
const richEvent: CalendarEvent = parser.asRichEventPlain();
|
||||
|
||||
const uid = getUid(calEvent);
|
||||
let success = true;
|
||||
|
||||
const updatedResult =
|
||||
const updationResult =
|
||||
credential && bookingRefUid
|
||||
? await calendars([credential])[0]
|
||||
.updateEvent(bookingRefUid, richEvent)
|
||||
.updateEvent(bookingRefUid, calEvent)
|
||||
.catch((e) => {
|
||||
log.error("updateEvent failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
if (!updatedResult) {
|
||||
if (!updationResult) {
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
|
@ -712,20 +201,11 @@ const updateEvent = async (
|
|||
};
|
||||
}
|
||||
|
||||
if (!noMail) {
|
||||
const organizerMail = new EventOrganizerRescheduledMail(calEvent);
|
||||
try {
|
||||
await organizerMail.sendEmail();
|
||||
} catch (e) {
|
||||
console.error("organizerMail.sendEmail failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
updatedEvent: updatedResult,
|
||||
updatedEvent: updationResult,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
|
1
lib/config/constants.ts
Normal file
1
lib/config/constants.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const BASE_URL = process.env.BASE_URL || `https://${process.env.VERCEL_URL}`;
|
|
@ -1,189 +0,0 @@
|
|||
import dayjs, { Dayjs } from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import EventMail from "./EventMail";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventAttendeeMail extends EventMail {
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getHtmlRepresentation(): string {
|
||||
return (
|
||||
`
|
||||
<body style="background: #f4f5f7; font-family: Helvetica, sans-serif">
|
||||
<div
|
||||
style="
|
||||
margin: 0 auto;
|
||||
max-width: 450px;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 2rem 2rem 2rem 2rem;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #31c48d"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 style="font-weight: 500; color: #161e2e;">${this.calEvent.language(
|
||||
"your_meeting_has_been_booked"
|
||||
)}</h1>
|
||||
<p style="color: #4b5563; margin-bottom: 30px;">${this.calEvent.language("emailed_you_and_attendees")}</p>
|
||||
<hr />
|
||||
<table style="border-spacing: 20px; color: #161e2e; margin-bottom: 10px;">
|
||||
<colgroup>
|
||||
<col span="1" style="width: 40%;">
|
||||
<col span="1" style="width: 60%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("what")}</td>
|
||||
<td>${this.calEvent.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("when")}</td>
|
||||
<td>${this.getInviteeStart().format("dddd, LL")}<br>${this.getInviteeStart().format("h:mma")} (${
|
||||
this.calEvent.attendees[0].timeZone
|
||||
})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("who")}</td>
|
||||
<td>
|
||||
${this.calEvent.team?.name || this.calEvent.organizer.name}<br />
|
||||
<small>
|
||||
${this.calEvent.organizer.email && !this.calEvent.team ? this.calEvent.organizer.email : ""}
|
||||
${this.calEvent.team ? this.calEvent.team.members.join(", ") : ""}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
${this.getLocation()}
|
||||
<tr>
|
||||
<td>${this.calEvent.language("notes")}</td>
|
||||
<td>${this.calEvent.description}</td>
|
||||
</tr>
|
||||
</table>
|
||||
` +
|
||||
this.getAdditionalBody() +
|
||||
"<br />" +
|
||||
`
|
||||
<hr />
|
||||
` +
|
||||
this.getAdditionalFooter() +
|
||||
`
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px; color: #ccc; font-size: 12px;">
|
||||
<img style="opacity: 0.25; width: 120px;" src="https://app.cal.com/cal-logo-word.svg" alt="Cal.com Logo"></div>
|
||||
</body>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the video call information to the mail body.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getLocation(): string {
|
||||
if (this.calEvent.additionInformation?.hangoutLink) {
|
||||
return `<tr>
|
||||
<td>${this.calEvent.language("where")}</td>
|
||||
<td><a href="${this.calEvent.additionInformation?.hangoutLink}">${
|
||||
this.calEvent.additionInformation?.hangoutLink
|
||||
}</a><br /></td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
if (
|
||||
this.calEvent.additionInformation?.entryPoints &&
|
||||
this.calEvent.additionInformation?.entryPoints.length > 0
|
||||
) {
|
||||
const locations = this.calEvent.additionInformation?.entryPoints
|
||||
.map((entryPoint) => {
|
||||
return `
|
||||
${this.calEvent.language("join_by_entrypoint", { entryPoint: entryPoint.entryPointType })}: <br />
|
||||
<a href="${entryPoint.uri}">${entryPoint.label}</a> <br />
|
||||
`;
|
||||
})
|
||||
.join("<br />");
|
||||
|
||||
return `<tr>
|
||||
<td>${this.calEvent.language("where")}</td>
|
||||
<td>${locations}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this.calEvent.location) {
|
||||
return ``;
|
||||
}
|
||||
|
||||
if (this.calEvent.location === "integrations:zoom" || this.calEvent.location === "integrations:daily") {
|
||||
return ``;
|
||||
}
|
||||
|
||||
return `<tr><td>${this.calEvent.language("where")}</td><td>${
|
||||
this.calEvent.location
|
||||
}<br /><br /></td></tr>`;
|
||||
}
|
||||
|
||||
protected getAdditionalBody(): string {
|
||||
return ``;
|
||||
}
|
||||
|
||||
protected getAdditionalFooter(): string {
|
||||
return this.parser.getChangeEventFooterHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payload object for the nodemailer.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: this.calEvent.language("confirmed_event_type_subject", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||
date: this.getInviteeStart().format("LT dddd, LL"),
|
||||
}),
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
};
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inviteeStart value used at multiple points.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
protected getInviteeStart(): Dayjs {
|
||||
return dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone);
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import EventAttendeeMail from "./EventAttendeeMail";
|
||||
|
||||
export default class EventAttendeeRescheduledMail extends EventAttendeeMail {
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getHtmlRepresentation(): string {
|
||||
return (
|
||||
`
|
||||
<div>
|
||||
${this.calEvent.language("hi_user_name", { userName: this.calEvent.attendees[0].name })},<br />
|
||||
<br />
|
||||
${this.calEvent.language("event_type_has_been_rescheduled_on_time_date", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||
time: this.getInviteeStart().format("h:mma"),
|
||||
timeZone: this.calEvent.attendees[0].timeZone,
|
||||
date:
|
||||
`${this.calEvent.language(this.getInviteeStart().format("dddd, ").toLowerCase())}` +
|
||||
`${this.calEvent.language(this.getInviteeStart().format("LL").toLowerCase())}`,
|
||||
})}<br />
|
||||
` +
|
||||
this.getAdditionalFooter() +
|
||||
`
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payload object for the nodemailer.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: this.calEvent.language("rescheduled_event_type_with_organizer", {
|
||||
eventType: this.calEvent.type,
|
||||
organizerName: this.calEvent.organizer.name,
|
||||
date: this.getInviteeStart().format("dddd, LL"),
|
||||
}),
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
};
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_RESCHEDULE_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error);
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
import nodemailer from "nodemailer";
|
||||
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
|
||||
import CalEventParser from "../CalEventParser";
|
||||
import { CalendarEvent } from "../calendarClient";
|
||||
import { serverConfig } from "../serverConfig";
|
||||
import { stripHtml } from "./helpers";
|
||||
|
||||
export default abstract class EventMail {
|
||||
calEvent: CalendarEvent;
|
||||
parser: CalEventParser;
|
||||
|
||||
/**
|
||||
* An EventMail always consists of a CalendarEvent
|
||||
* that stores the data of the event (like date, title, uid etc).
|
||||
*
|
||||
* @param calEvent
|
||||
*/
|
||||
constructor(calEvent: CalendarEvent) {
|
||||
this.calEvent = calEvent;
|
||||
this.parser = new CalEventParser(calEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected abstract getHtmlRepresentation(): string;
|
||||
|
||||
/**
|
||||
* Returns the email text in a plain text representation
|
||||
* by stripping off the HTML tags.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getPlainTextRepresentation(): string {
|
||||
return stripHtml(this.getHtmlRepresentation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payload object for the nodemailer.
|
||||
* @protected
|
||||
*/
|
||||
protected abstract getNodeMailerPayload(): Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Sends the email to the event attendant and returns a Promise.
|
||||
*/
|
||||
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"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers the required provider information from the config.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getMailerOptions() {
|
||||
return {
|
||||
transport: serverConfig.transport,
|
||||
from: serverConfig.from,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to include additional HTML or plain text
|
||||
* content into the mail body. Leave it to an empty
|
||||
* string if not desired.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getAdditionalBody(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected abstract getLocation(): string;
|
||||
|
||||
/**
|
||||
* Prints out the desired information when an error
|
||||
* occured while sending the mail.
|
||||
* @param error
|
||||
* @protected
|
||||
*/
|
||||
protected abstract printNodeMailerError(error: Error): void;
|
||||
|
||||
/**
|
||||
* Returns a link to reschedule the given booking.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getRescheduleLink(): string {
|
||||
return this.parser.getRescheduleLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to cancel the given booking.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getCancelLink(): string {
|
||||
return this.parser.getCancelLink();
|
||||
}
|
||||
}
|
|
@ -1,251 +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 { Person } from "@lib/calendarClient";
|
||||
|
||||
import EventMail from "./EventMail";
|
||||
import { stripHtml } from "./helpers";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventOrganizerMail extends EventMail {
|
||||
/**
|
||||
* Returns the instance's event as an iCal event in string representation.
|
||||
* @protected
|
||||
*/
|
||||
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.language("organizer_ics_event_title", {
|
||||
eventType: this.calEvent.type,
|
||||
attendeeName: this.calEvent.attendees[0].name,
|
||||
}),
|
||||
description:
|
||||
this.calEvent.description +
|
||||
stripHtml(this.getAdditionalBody()) +
|
||||
stripHtml(this.getAdditionalFooter()),
|
||||
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 getBodyHeader(): string {
|
||||
return this.calEvent.language("new_event_scheduled");
|
||||
}
|
||||
|
||||
protected getAdditionalFooter(): string {
|
||||
return `<p style="color: #4b5563; margin-top: 20px;">${this.calEvent.language(
|
||||
"need_to_make_a_change"
|
||||
)} <a href=${process.env.BASE_URL + "/bookings"} style="color: #161e2e;">${this.calEvent.language(
|
||||
"manage_my_bookings"
|
||||
)}</a></p>`;
|
||||
}
|
||||
|
||||
protected getImage(): string {
|
||||
return `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #31c48d"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getHtmlRepresentation(): string {
|
||||
return (
|
||||
`
|
||||
<body style="background: #f4f5f7; font-family: Helvetica, sans-serif">
|
||||
<div
|
||||
style="
|
||||
margin: 0 auto;
|
||||
max-width: 450px;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 2rem 2rem 2rem 2rem;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
"
|
||||
>
|
||||
${this.getImage()}
|
||||
<h1 style="font-weight: 500; color: #161e2e;">${this.getBodyHeader()}</h1>
|
||||
<hr />
|
||||
<table style="border-spacing: 20px; color: #161e2e; margin-bottom: 10px;">
|
||||
<colgroup>
|
||||
<col span="1" style="width: 40%;">
|
||||
<col span="1" style="width: 60%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("what")}</td>
|
||||
<td>${this.calEvent.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("when")}</td>
|
||||
<td>${this.getOrganizerStart().format("dddd, LL")}<br>${this.getOrganizerStart().format("h:mma")} (${
|
||||
this.calEvent.organizer.timeZone
|
||||
})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("who")}</td>
|
||||
<td>${this.calEvent.attendees[0].name}<br /><small><a href="mailto:${
|
||||
this.calEvent.attendees[0].email
|
||||
}">${this.calEvent.attendees[0].email}</a></small></td>
|
||||
</tr>
|
||||
${this.getLocation()}
|
||||
<tr>
|
||||
<td>${this.calEvent.language("notes")}</td>
|
||||
<td>${this.calEvent.description}</td>
|
||||
</tr>
|
||||
</table>
|
||||
` +
|
||||
this.getAdditionalBody() +
|
||||
"<br />" +
|
||||
`
|
||||
<hr />
|
||||
` +
|
||||
this.getAdditionalFooter() +
|
||||
`
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px; color: #ccc; font-size: 12px;">
|
||||
<img style="opacity: 0.25; width: 120px;" src="https://app.cal.com/cal-logo-word.svg" alt="Cal.com Logo"></div>
|
||||
</body>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the video call information to the mail body.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getLocation(): string {
|
||||
if (this.calEvent.additionInformation?.hangoutLink) {
|
||||
return `<tr>
|
||||
<td>${this.calEvent.language("where")}</td>
|
||||
<td><a href="${this.calEvent.additionInformation?.hangoutLink}">${
|
||||
this.calEvent.additionInformation?.hangoutLink
|
||||
}</a><br /></td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
if (
|
||||
this.calEvent.additionInformation?.entryPoints &&
|
||||
this.calEvent.additionInformation?.entryPoints.length > 0
|
||||
) {
|
||||
const locations = this.calEvent.additionInformation?.entryPoints
|
||||
.map((entryPoint) => {
|
||||
return `
|
||||
${this.calEvent.language("join_by_entrypoint", { entryPoint: entryPoint.entryPointType })}: <br />
|
||||
<a href="${entryPoint.uri}">${entryPoint.label}</a> <br />
|
||||
`;
|
||||
})
|
||||
.join("<br />");
|
||||
|
||||
return `<tr>
|
||||
<td>${this.calEvent.language("where")}</td>
|
||||
<td>${locations}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this.calEvent.location) {
|
||||
return ``;
|
||||
}
|
||||
|
||||
if (this.calEvent.location === "integrations:zoom" || this.calEvent.location === "integrations:daily") {
|
||||
return ``;
|
||||
}
|
||||
|
||||
return `<tr><td>${this.calEvent.language("where")}</td><td>${
|
||||
this.calEvent.location
|
||||
}<br /><br /></td></tr>`;
|
||||
}
|
||||
|
||||
protected getAdditionalBody(): string {
|
||||
return ``;
|
||||
}
|
||||
/**
|
||||
* Returns the payload object for the nodemailer.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
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.getSubject(),
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getSubject(): string {
|
||||
return this.calEvent.language("new_event_subject", {
|
||||
attendeeName: this.calEvent.attendees[0].name,
|
||||
date: this.getOrganizerStart().format("LT dddd, LL"),
|
||||
eventType: this.calEvent.type,
|
||||
});
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the organizerStart value used at multiple points.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
protected getOrganizerStart(): Dayjs {
|
||||
return dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
}
|
||||
}
|
|
@ -1,71 +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 { CalendarEvent } from "@lib/calendarClient";
|
||||
import EventOrganizerMail from "@lib/emails/EventOrganizerMail";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventOrganizerRefundFailedMail extends EventOrganizerMail {
|
||||
reason: string;
|
||||
paymentId: string;
|
||||
|
||||
constructor(calEvent: CalendarEvent, reason: string, paymentId: string) {
|
||||
super(calEvent);
|
||||
this.reason = reason;
|
||||
this.paymentId = paymentId;
|
||||
}
|
||||
|
||||
protected getBodyHeader(): string {
|
||||
return this.calEvent.language("a_refund_failed");
|
||||
}
|
||||
|
||||
protected getBodyText(): string {
|
||||
const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
return `${this.calEvent.language("refund_failed", {
|
||||
eventType: this.calEvent.type,
|
||||
userName: this.calEvent.attendees[0].name,
|
||||
date: organizerStart.format("LT dddd, LL"),
|
||||
})} ${this.calEvent.language("check_with_provider_and_user", {
|
||||
userName: this.calEvent.attendees[0].name,
|
||||
})}<br>${this.calEvent.language("error_message", { errorMessage: this.reason })}<br>PaymentId: '${
|
||||
this.paymentId
|
||||
}'`;
|
||||
}
|
||||
|
||||
protected getAdditionalBody(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected getImage(): string {
|
||||
return `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #9b0125"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
protected getSubject(): string {
|
||||
const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
return this.calEvent.language("refund_failed_subject", {
|
||||
userName: this.calEvent.attendees[0].name,
|
||||
date: organizerStart.format("LT dddd, LL"),
|
||||
eventType: this.calEvent.type,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,54 +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 EventOrganizerMail from "@lib/emails/EventOrganizerMail";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventOrganizerRequestMail extends EventOrganizerMail {
|
||||
protected getBodyHeader(): string {
|
||||
return this.calEvent.language("event_awaiting_approval");
|
||||
}
|
||||
|
||||
protected getBodyText(): string {
|
||||
return this.calEvent.language("check_bookings_page_to_confirm_or_reject");
|
||||
}
|
||||
|
||||
protected getAdditionalBody(): string {
|
||||
return `<a href="${process.env.BASE_URL}/bookings">${this.calEvent.language(
|
||||
"confirm_or_reject_booking"
|
||||
)}</a>`;
|
||||
}
|
||||
|
||||
protected getImage(): string {
|
||||
return `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #01579b"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
protected getSubject(): string {
|
||||
const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
return this.calEvent.language("new_event_request", {
|
||||
attendeeName: this.calEvent.attendees[0].name,
|
||||
date: organizerStart.format("LT dddd, LL"),
|
||||
eventType: this.calEvent.type,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,27 +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 EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventOrganizerRequestReminderMail extends EventOrganizerRequestMail {
|
||||
protected getBodyHeader(): string {
|
||||
return this.calEvent.language("still_waiting_for_approval");
|
||||
}
|
||||
|
||||
protected getSubject(): string {
|
||||
const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
return this.calEvent.language("event_is_still_waiting", {
|
||||
attendeeName: this.calEvent.attendees[0].name,
|
||||
date: organizerStart.format("LT dddd, LL"),
|
||||
eventType: this.calEvent.type,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import dayjs, { Dayjs } from "dayjs";
|
||||
|
||||
import EventOrganizerMail from "./EventOrganizerMail";
|
||||
|
||||
export default class EventOrganizerRescheduledMail extends EventOrganizerMail {
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getHtmlRepresentation(): string {
|
||||
return (
|
||||
`
|
||||
<div>
|
||||
${this.calEvent.language("hi_user_name", { userName: this.calEvent.organizer.name })},<br />
|
||||
<br />
|
||||
${this.calEvent.language("event_has_been_rescheduled")}<br />
|
||||
<br />
|
||||
<strong>${this.calEvent.language("event_type")}:</strong><br />
|
||||
${this.calEvent.type}<br />
|
||||
<br />
|
||||
<strong>${this.calEvent.language("invitee_email")}:</strong><br />
|
||||
<a href="mailto:${this.calEvent.attendees[0].email}">${this.calEvent.attendees[0].email}</a><br />
|
||||
<br />` +
|
||||
this.getAdditionalBody() +
|
||||
(this.calEvent.location
|
||||
? `
|
||||
<strong>${this.calEvent.language("location")}:</strong><br />
|
||||
${this.calEvent.location}<br />
|
||||
<br />
|
||||
`
|
||||
: "") +
|
||||
`<strong>${this.calEvent.language("invitee_timezone")}:</strong><br />
|
||||
${this.calEvent.attendees[0].timeZone}<br />
|
||||
<br />
|
||||
<strong>${this.calEvent.language("additional_notes")}:</strong><br />
|
||||
${this.calEvent.description}
|
||||
` +
|
||||
this.getAdditionalFooter() +
|
||||
`
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payload object for the nodemailer.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
|
||||
return {
|
||||
icalEvent: {
|
||||
filename: "event.ics",
|
||||
content: this.getiCalEventAsString(),
|
||||
},
|
||||
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||
to: this.calEvent.organizer.email,
|
||||
subject: this.calEvent.language("rescheduled_event_type_with_attendee", {
|
||||
attendeeName: this.calEvent.attendees[0].name,
|
||||
date: organizerStart.format("LT dddd, LL"),
|
||||
eventType: this.calEvent.type,
|
||||
}),
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
};
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_RESCHEDULE_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error);
|
||||
}
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
import dayjs, { Dayjs } from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
|
||||
import EventMail from "./EventMail";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventPaymentMail extends EventMail {
|
||||
paymentLink: string;
|
||||
|
||||
constructor(paymentLink: string, calEvent: CalendarEvent) {
|
||||
super(calEvent);
|
||||
this.paymentLink = paymentLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getHtmlRepresentation(): string {
|
||||
return (
|
||||
`
|
||||
<body style="background: #f4f5f7; font-family: Helvetica, sans-serif">
|
||||
<div
|
||||
style="
|
||||
margin: 0 auto;
|
||||
max-width: 450px;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 2rem 2rem 2rem 2rem;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #31c48d"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 style="font-weight: 500; color: #161e2e;">${this.calEvent.language("meeting_awaiting_payment")}</h1>
|
||||
<p style="color: #4b5563; margin-bottom: 30px;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</p>
|
||||
<hr />
|
||||
<table style="border-spacing: 20px; color: #161e2e; margin-bottom: 10px;">
|
||||
<colgroup>
|
||||
<col span="1" style="width: 40%;">
|
||||
<col span="1" style="width: 60%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("what")}</td>
|
||||
<td>${this.calEvent.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("when")}</td>
|
||||
<td>${this.getInviteeStart().format("dddd, LL")}<br>${this.getInviteeStart().format("h:mma")} (${
|
||||
this.calEvent.attendees[0].timeZone
|
||||
})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("who")}</td>
|
||||
<td>${this.calEvent.organizer.name}<br /><small>${this.calEvent.organizer.email}</small></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("where")}</td>
|
||||
<td>${this.getLocation()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${this.calEvent.language("notes")}Notes</td>
|
||||
<td>${this.calEvent.description}</td>
|
||||
</tr>
|
||||
</table>
|
||||
` +
|
||||
this.getAdditionalBody() +
|
||||
"<br />" +
|
||||
`
|
||||
<hr />
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px; color: #ccc; font-size: 12px;">
|
||||
<img style="opacity: 0.25; width: 120px;" src="https://app.cal.com/cal-logo-word.svg" alt="Cal.com Logo"></div>
|
||||
</body>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the video call information to the mail body.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getLocation(): string {
|
||||
if (this.calEvent.additionInformation?.hangoutLink) {
|
||||
return `<a href="${this.calEvent.additionInformation?.hangoutLink}">${this.calEvent.additionInformation?.hangoutLink}</a><br />`;
|
||||
}
|
||||
|
||||
if (
|
||||
this.calEvent.additionInformation?.entryPoints &&
|
||||
this.calEvent.additionInformation?.entryPoints.length > 0
|
||||
) {
|
||||
const locations = this.calEvent.additionInformation?.entryPoints
|
||||
.map((entryPoint) => {
|
||||
return `
|
||||
${this.calEvent.language("join_by_entrypoint", { entryPoint: entryPoint.entryPointType })}: <br />
|
||||
<a href="${entryPoint.uri}">${entryPoint.label}</a> <br />
|
||||
`;
|
||||
})
|
||||
.join("<br />");
|
||||
|
||||
return `${locations}`;
|
||||
}
|
||||
|
||||
return this.calEvent.location ? `${this.calEvent.location}<br /><br />` : "";
|
||||
}
|
||||
|
||||
protected getAdditionalBody(): string {
|
||||
return `<a href="${this.paymentLink}">${this.calEvent.language("pay_now")}</a>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payload object for the nodemailer.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: this.calEvent.language("awaiting_payment", {
|
||||
eventType: this.calEvent.type,
|
||||
organizerName: this.calEvent.organizer.name,
|
||||
date: this.getInviteeStart().format("dddd, LL"),
|
||||
}),
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
};
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_BOOKING_PAYMENT_ERROR", this.calEvent.attendees[0].email, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inviteeStart value used at multiple points.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
protected getInviteeStart(): Dayjs {
|
||||
return dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone);
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
import dayjs, { Dayjs } from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import EventMail from "./EventMail";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventRejectionMail extends EventMail {
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getHtmlRepresentation(): string {
|
||||
return (
|
||||
`
|
||||
<body style="background: #f4f5f7; font-family: Helvetica, sans-serif">
|
||||
<div
|
||||
style="
|
||||
margin: 0 auto;
|
||||
max-width: 450px;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 2rem 2rem 2rem 2rem;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #ba2525"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 style="font-weight: 500; color: #161e2e;">${this.calEvent.language("meeting_request_rejected")}</h1>
|
||||
<p style="color: #4b5563; margin-bottom: 30px;">${this.calEvent.language("emailed_you_and_attendees")}</p>
|
||||
<hr />
|
||||
` +
|
||||
`
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px; color: #ccc; font-size: 12px;">
|
||||
<img style="opacity: 0.25; width: 120px;" src="https://app.cal.com/cal-logo-word.svg" alt="Cal.com Logo"></div>
|
||||
</body>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payload object for the nodemailer.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: this.calEvent.language("rejected_event_type_with_organizer", {
|
||||
eventType: this.calEvent.type,
|
||||
organizer: this.calEvent.organizer.name,
|
||||
date: this.getInviteeStart().format("dddd, LL"),
|
||||
}),
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
};
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inviteeStart value used at multiple points.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getInviteeStart(): Dayjs {
|
||||
return dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the video call information to the mail body.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getLocation(): string {
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import EventAttendeeMail from "./EventAttendeeMail";
|
||||
import { getFormattedMeetingId, getIntegrationName } from "./helpers";
|
||||
|
||||
export default class VideoEventAttendeeMail extends EventAttendeeMail {
|
||||
/**
|
||||
* Adds the video call information to the mail body.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getAdditionalBody(): string {
|
||||
if (!this.calEvent.videoCallData) {
|
||||
return "";
|
||||
}
|
||||
const meetingPassword = this.calEvent.videoCallData.password;
|
||||
const meetingId = getFormattedMeetingId(this.calEvent.videoCallData);
|
||||
|
||||
if (meetingId && meetingPassword) {
|
||||
return `
|
||||
<strong>${this.calEvent.language("video_call_provider")}:</strong> ${getIntegrationName(
|
||||
this.calEvent.videoCallData
|
||||
)}<br />
|
||||
<strong>${this.calEvent.language("meeting_id")}:</strong> ${getFormattedMeetingId(
|
||||
this.calEvent.videoCallData
|
||||
)}<br />
|
||||
<strong>${this.calEvent.language("meeting_password")}:</strong> ${
|
||||
this.calEvent.videoCallData.password
|
||||
}<br />
|
||||
<strong>${this.calEvent.language("meeting_url")}:</strong> <a href="${
|
||||
this.calEvent.videoCallData.url
|
||||
}">${this.calEvent.videoCallData.url}</a><br />
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<strong>${this.calEvent.language("video_call_provider")}:</strong> ${getIntegrationName(
|
||||
this.calEvent.videoCallData
|
||||
)}<br />
|
||||
<strong>${this.calEvent.language("meeting_url")}:</strong> <a href="${
|
||||
this.calEvent.videoCallData.url
|
||||
}">${this.calEvent.videoCallData.url}</a><br />
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import EventOrganizerMail from "./EventOrganizerMail";
|
||||
import { getFormattedMeetingId, getIntegrationName } from "./helpers";
|
||||
|
||||
export default class VideoEventOrganizerMail extends EventOrganizerMail {
|
||||
/**
|
||||
* Adds the video call information to the mail body
|
||||
* and calendar event description.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getAdditionalBody(): string {
|
||||
if (!this.calEvent.videoCallData) {
|
||||
return "";
|
||||
}
|
||||
const meetingPassword = this.calEvent.videoCallData.password;
|
||||
const meetingId = getFormattedMeetingId(this.calEvent.videoCallData);
|
||||
// This odd indentation is necessary because otherwise the leading tabs will be applied into the event description.
|
||||
if (meetingPassword && meetingId) {
|
||||
return `
|
||||
<strong>${this.calEvent.language("video_call_provider")}:</strong> ${getIntegrationName(
|
||||
this.calEvent.videoCallData
|
||||
)}<br />
|
||||
<strong>${this.calEvent.language("meeting_id")}:</strong> ${getFormattedMeetingId(
|
||||
this.calEvent.videoCallData
|
||||
)}<br />
|
||||
<strong>${this.calEvent.language("meeting_password")}:</strong> ${this.calEvent.videoCallData.password}<br />
|
||||
<strong>${this.calEvent.language("meeting_url")}:</strong> <a href="${this.calEvent.videoCallData.url}">${
|
||||
this.calEvent.videoCallData.url
|
||||
}</a><br />
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<strong>${this.calEvent.language("video_call_provider")}:</strong> ${getIntegrationName(
|
||||
this.calEvent.videoCallData
|
||||
)}<br />
|
||||
<strong>${this.calEvent.language("meeting_url")}:</strong> <a href="${this.calEvent.videoCallData.url}">${
|
||||
this.calEvent.videoCallData.url
|
||||
}</a><br />
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import Handlebars from "handlebars";
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
export type VarType = {
|
||||
language: TFunction;
|
||||
user: {
|
||||
name: string | null;
|
||||
};
|
||||
link: string;
|
||||
};
|
||||
|
||||
export type MessageTemplateTypes = {
|
||||
messageTemplate: string;
|
||||
subjectTemplate: string;
|
||||
vars: VarType;
|
||||
};
|
||||
|
||||
export type BuildTemplateResult = {
|
||||
subject: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const buildMessageTemplate = ({
|
||||
messageTemplate,
|
||||
subjectTemplate,
|
||||
vars,
|
||||
}: MessageTemplateTypes): BuildTemplateResult => {
|
||||
const buildMessage = Handlebars.compile(messageTemplate);
|
||||
const message = buildMessage(vars);
|
||||
|
||||
const buildSubject = Handlebars.compile(subjectTemplate);
|
||||
const subject = buildSubject(vars);
|
||||
|
||||
return {
|
||||
subject,
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
export default buildMessageTemplate;
|
197
lib/emails/email-manager.ts
Normal file
197
lib/emails/email-manager.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
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 AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email";
|
||||
import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-email";
|
||||
import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email";
|
||||
import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-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 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";
|
||||
|
||||
export const sendScheduledEmails = async (calEvent: CalendarEvent) => {
|
||||
const emailsToSend = [];
|
||||
|
||||
emailsToSend.push(
|
||||
calEvent.attendees.map((attendee) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new AttendeeScheduledEmail(calEvent, attendee);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
emailsToSend.push(
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new OrganizerScheduledEmail(calEvent);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerScheduledEmail.sendEmail failed", e));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
|
||||
const emailsToSend = [];
|
||||
|
||||
emailsToSend.push(
|
||||
calEvent.attendees.map((attendee) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new AttendeeRescheduledEmail(calEvent, attendee);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
emailsToSend.push(
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new OrganizerRescheduledEmail(calEvent);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerScheduledEmail.sendEmail failed", e));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
const organizerRequestEmail = new OrganizerRequestEmail(calEvent);
|
||||
resolve(organizerRequestEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerRequestEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const sendDeclinedEmails = async (calEvent: CalendarEvent) => {
|
||||
const emailsToSend = [];
|
||||
|
||||
emailsToSend.push(
|
||||
calEvent.attendees.map((attendee) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const declinedEmail = new AttendeeDeclinedEmail(calEvent, attendee);
|
||||
resolve(declinedEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendCancelledEmails = async (calEvent: CalendarEvent) => {
|
||||
const emailsToSend = [];
|
||||
|
||||
emailsToSend.push(
|
||||
calEvent.attendees.map((attendee) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new AttendeeCancelledEmail(calEvent, attendee);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("AttendeeCancelledEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
emailsToSend.push(
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new OrganizerCancelledEmail(calEvent);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerCancelledEmail.sendEmail failed", e));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendOrganizerRequestReminderEmail = async (calEvent: CalendarEvent) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
const organizerRequestReminderEmail = new OrganizerRequestReminderEmail(calEvent);
|
||||
resolve(organizerRequestReminderEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerRequestReminderEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const sendAwaitingPaymentEmail = async (calEvent: CalendarEvent) => {
|
||||
const emailsToSend = [];
|
||||
|
||||
emailsToSend.push(
|
||||
calEvent.attendees.map((attendee) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const paymentEmail = new AttendeeAwaitingPaymentEmail(calEvent, attendee);
|
||||
resolve(paymentEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("AttendeeAwaitingPaymentEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendOrganizerPaymentRefundFailedEmail = async (calEvent: CalendarEvent) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
const paymentRefundFailedEmail = new OrganizerPaymentRefundFailedEmail(calEvent);
|
||||
resolve(paymentRefundFailedEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerPaymentRefundFailedEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const sendPasswordResetEmail = async (passwordResetEvent: PasswordReset) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
const passwordResetEmail = new ForgotPasswordEmail(passwordResetEvent);
|
||||
resolve(passwordResetEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerPaymentRefundFailedEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const sendTeamInviteEmail = async (teamInviteEvent: TeamInvite) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
const teamInviteEmail = new TeamInviteEmail(teamInviteEvent);
|
||||
resolve(teamInviteEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("TeamInviteEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
import { VideoCallData } from "../videoClient";
|
||||
|
||||
export function getIntegrationName(videoCallData: VideoCallData): string {
|
||||
//TODO: When there are more complex integration type strings, we should consider using an extra field in the DB for that.
|
||||
const nameProto = videoCallData.type.split("_")[0];
|
||||
return nameProto.charAt(0).toUpperCase() + nameProto.slice(1);
|
||||
}
|
||||
|
||||
function extractZoom(videoCallData: VideoCallData): string {
|
||||
const strId = videoCallData.id.toString();
|
||||
const part1 = strId.slice(0, 3);
|
||||
const part2 = strId.slice(3, 7);
|
||||
const part3 = strId.slice(7, 11);
|
||||
|
||||
return part1 + " " + part2 + " " + part3;
|
||||
}
|
||||
|
||||
export function getFormattedMeetingId(videoCallData: VideoCallData): string {
|
||||
switch (videoCallData.type) {
|
||||
case "zoom_video":
|
||||
return extractZoom(videoCallData);
|
||||
default:
|
||||
return videoCallData.id.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export function stripHtml(html: string): string {
|
||||
const aMailToRegExp = /<a[\s\w="_:#;]*href="mailto:([^<>"]*)"[\s\w="_:#;]*>([^<>]*)<\/a>/g;
|
||||
const aLinkRegExp = /<a[\s\w="_:#;]*href="([^<>"]*)"[\s\w="_:#;]*>([^<>]*)<\/a>/g;
|
||||
return html
|
||||
.replace(/<br\s?\/>/g, "\n")
|
||||
.replace(aMailToRegExp, "$1")
|
||||
.replace(aLinkRegExp, "$2: $1")
|
||||
.replace(/<[^>]+>/g, "");
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
import { TFunction } from "next-i18next";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
|
||||
import { serverConfig } from "../serverConfig";
|
||||
|
||||
export type Invitation = {
|
||||
language: TFunction;
|
||||
from?: string;
|
||||
toEmail: string;
|
||||
teamName: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
type EmailProvider = {
|
||||
from: string;
|
||||
transport: any;
|
||||
};
|
||||
|
||||
export function createInvitationEmail(data: Invitation) {
|
||||
const provider = {
|
||||
transport: serverConfig.transport,
|
||||
from: serverConfig.from,
|
||||
} as EmailProvider;
|
||||
return sendEmail(data, provider);
|
||||
}
|
||||
|
||||
const sendEmail = (invitation: Invitation, provider: EmailProvider): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const { transport, from } = provider;
|
||||
|
||||
const { language: t } = invitation;
|
||||
const invitationHtml = html(invitation);
|
||||
nodemailer.createTransport(transport).sendMail(
|
||||
{
|
||||
from: `Cal.com <${from}>`,
|
||||
to: invitation.toEmail,
|
||||
subject: invitation.from
|
||||
? t("user_invited_you", { user: invitation.from, teamName: invitation.teamName })
|
||||
: t("you_have_been_invited", { teamName: invitation.teamName }),
|
||||
html: invitationHtml,
|
||||
text: text(invitationHtml),
|
||||
},
|
||||
(_err) => {
|
||||
if (_err) {
|
||||
const err = getErrorFromUnknown(_err);
|
||||
console.error("SEND_INVITATION_NOTIFICATION_ERROR", invitation.toEmail, err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
return resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export function html(invitation: Invitation): string {
|
||||
const { language: t } = invitation;
|
||||
let url: string = process.env.BASE_URL + "/settings/teams";
|
||||
if (invitation.token) {
|
||||
url = `${process.env.BASE_URL}/auth/signup?token=${invitation.token}&callbackUrl=${url}`;
|
||||
}
|
||||
|
||||
return (
|
||||
`
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<center>
|
||||
<table style="width: 640px; border: 1px solid gray; padding: 15px; margin: 0 auto; text-align: left;">
|
||||
<tr>
|
||||
<td>
|
||||
${t("hi")},<br />
|
||||
<br />` +
|
||||
(invitation.from
|
||||
? t("user_invited_you", { user: invitation.from, teamName: invitation.teamName })
|
||||
: t("you_have_been_invited", { teamName: invitation.teamName })) +
|
||||
`<br />
|
||||
<br />
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${url}" style="height:40px;v-text-anchor:middle;width:130px;" arcsize="5%" strokecolor="#19cca3" fillcolor="#19cca3;width: 130;">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Helvetica, sans-serif;font-size:18px; font-weight: 600;">Join team</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<a href="${url}" style="display: inline-block; mso-hide:all; background-color: #19cca3; color: #FFFFFF; border:1px solid #19cca3; border-radius: 6px; line-height: 220%; width: 200px; font-family: Helvetica, sans-serif; font-size:18px; font-weight:600; text-align: center; text-decoration: none; -webkit-text-size-adjust:none; " target="_blank">${t(
|
||||
"join_team"
|
||||
)}</a>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table><br />
|
||||
${t("request_another_invitation_email", { toEmail: invitation.toEmail })}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
// just strip all HTML and convert <br /> to \n
|
||||
export function text(htmlStr: string): string {
|
||||
return htmlStr.replace("<br />", "\n").replace(/<[^>]+>/g, "");
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import nodemailer, { SentMessageInfo } from "nodemailer";
|
||||
import { SendMailOptions } from "nodemailer";
|
||||
|
||||
import { serverConfig } from "../serverConfig";
|
||||
|
||||
const sendEmail = ({ to, subject, text, html }: SendMailOptions): Promise<string | SentMessageInfo> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const { transport, from } = serverConfig;
|
||||
|
||||
if (!to || !subject || (!text && !html)) {
|
||||
return reject("Missing required elements to send email.");
|
||||
}
|
||||
|
||||
nodemailer.createTransport(transport).sendMail(
|
||||
{
|
||||
from: `Cal.com ${from}`,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
console.error("SEND_INVITATION_NOTIFICATION_ERROR", to, error);
|
||||
return reject(error.message);
|
||||
}
|
||||
return resolve(info);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export default sendEmail;
|
275
lib/emails/templates/attendee-awaiting-payment-email.ts
Normal file
275
lib/emails/templates/attendee-awaiting-payment-email.ts
Normal file
|
@ -0,0 +1,275 @@
|
|||
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 } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(toArray);
|
||||
|
||||
export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail {
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: `${this.calEvent.language("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.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("meeting_awaiting_payment")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("calendarCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"meeting_awaiting_payment"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
${this.getManageLink()}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getManageLink(): string {
|
||||
const manageText = this.calEvent.language("pay_now");
|
||||
|
||||
if (this.calEvent.paymentInfo) {
|
||||
return `
|
||||
<tr>
|
||||
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
|
||||
<p style="display:inline-block;background:#292929;color:#ffffff;font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
|
||||
<a style="color: #FFFFFF; text-decoration: none;" href="${this.calEvent.paymentInfo.link}" target="_blank">${manageText} <img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
212
lib/emails/templates/attendee-cancelled-email.ts
Normal file
212
lib/emails/templates/attendee-cancelled-email.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
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 } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(toArray);
|
||||
|
||||
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: `${this.calEvent.language("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.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("event_request_cancelled")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("xCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"event_request_cancelled"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
212
lib/emails/templates/attendee-declined-email.ts
Normal file
212
lib/emails/templates/attendee-declined-email.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
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 } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(toArray);
|
||||
|
||||
export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: `${this.calEvent.language("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.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("event_request_declined")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("xCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"event_request_declined"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
273
lib/emails/templates/attendee-rescheduled-email.ts
Normal file
273
lib/emails/templates/attendee-rescheduled-email.ts
Normal file
|
@ -0,0 +1,273 @@
|
|||
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 "@lib/CalEventParser";
|
||||
|
||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||
import { emailHead } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(toArray);
|
||||
|
||||
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
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.language("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.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.calEvent.language("event_has_been_rescheduled")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
${this.calEvent.language("need_to_reschedule_or_cancel")}
|
||||
${getCancelLink(this.calEvent)}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
return `
|
||||
${this.calEvent.language("event_has_been_rescheduled")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("calendarCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"event_has_been_rescheduled"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||
${this.getManageLink()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
509
lib/emails/templates/attendee-scheduled-email.ts
Normal file
509
lib/emails/templates/attendee-scheduled-email.ts
Normal file
|
@ -0,0 +1,509 @@
|
|||
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 nodemailer from "nodemailer";
|
||||
|
||||
import { getCancelLink } from "@lib/CalEventParser";
|
||||
import { CalendarEvent, Person } from "@lib/calendarClient";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
import { serverConfig } from "@lib/serverConfig";
|
||||
|
||||
import { emailHead } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(toArray);
|
||||
|
||||
export default class AttendeeScheduledEmail {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
|
||||
constructor(calEvent: CalendarEvent, attendee: Person) {
|
||||
this.calEvent = calEvent;
|
||||
this.attendee = attendee;
|
||||
}
|
||||
|
||||
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.language("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<string, unknown> {
|
||||
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.language("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.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||
})}`,
|
||||
html: this.getHtmlBody(),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getMailerOptions() {
|
||||
return {
|
||||
transport: serverConfig.transport,
|
||||
from: serverConfig.from,
|
||||
};
|
||||
}
|
||||
|
||||
protected getTextBody(): string {
|
||||
// Only the original attendee can make changes to the event
|
||||
// Guests cannot
|
||||
if (this.attendee === this.calEvent.attendees[0]) {
|
||||
return `
|
||||
${this.calEvent.language("your_event_has_been_scheduled")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
${this.calEvent.language("need_to_reschedule_or_cancel")}
|
||||
${getCancelLink(this.calEvent)}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
return `
|
||||
${this.calEvent.language("your_event_has_been_scheduled")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.attendee.email, error);
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("checkCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"your_event_has_been_scheduled"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||
${this.getManageLink()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
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.language("manage_this_event");
|
||||
return `<p>${this.calEvent.language(
|
||||
"need_to_reschedule_or_cancel"
|
||||
)}</p><p style="font-weight: 400; line-height: 24px;"><a href="${getCancelLink(
|
||||
this.calEvent
|
||||
)}" style="color: #3E3E3E;" alt="${manageText}">${manageText}</a></p>`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected getWhat(): string {
|
||||
return `
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("what")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.type}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected getWhen(): string {
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("when")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">
|
||||
${this.calEvent.language(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format(
|
||||
"YYYY"
|
||||
)} | ${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
|
||||
"h:mma"
|
||||
)} <span style="color: #888888">(${this.getTimezone()})</span>
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected getWho(): string {
|
||||
const attendees = this.calEvent.attendees
|
||||
.map((attendee) => {
|
||||
return `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
attendee?.name || `${this.calEvent.language("guest")}`
|
||||
} <span style="color: #888888"><a href="mailto:${attendee.email}" style="color: #888888;">${
|
||||
attendee.email
|
||||
}</a></span></div>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const organizer = `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
this.calEvent.organizer.name
|
||||
} - ${this.calEvent.language("organizer")} <span style="color: #888888"><a href="mailto:${
|
||||
this.calEvent.organizer.email
|
||||
}" style="color: #888888;">${this.calEvent.organizer.email}</a></span></div>`;
|
||||
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("who")}</p>
|
||||
${organizer + attendees}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected getAdditionalNotes(): string {
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("additional_notes")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.description}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getLocation(): string {
|
||||
let providerName = this.calEvent.location ? getIntegrationName(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 (this.calEvent.videoCallData) {
|
||||
const meetingId = this.calEvent.videoCallData.id;
|
||||
const meetingPassword = this.calEvent.videoCallData.password;
|
||||
const meetingUrl = this.calEvent.videoCallData.url;
|
||||
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
|
||||
meetingUrl &&
|
||||
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}"><img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>`
|
||||
}</p>
|
||||
${
|
||||
meetingId &&
|
||||
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
|
||||
"meeting_id"
|
||||
)}: <span>${meetingId}</span></div>`
|
||||
}
|
||||
${
|
||||
meetingPassword &&
|
||||
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
|
||||
"meeting_password"
|
||||
)}: <span>${meetingPassword}</span></div>`
|
||||
}
|
||||
${
|
||||
meetingUrl &&
|
||||
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}: <a href="${meetingUrl}" alt="${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}" style="color: #3E3E3E" target="_blank">${meetingUrl}</a></div>`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.calEvent.additionInformation?.hangoutLink) {
|
||||
const hangoutLink: string = this.calEvent.additionInformation.hangoutLink;
|
||||
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
hangoutLink &&
|
||||
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}"><img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>`
|
||||
}</p>
|
||||
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
91
lib/emails/templates/common/head.ts
Normal file
91
lib/emails/templates/common/head.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
export const emailHead = (headerContent: string): string => {
|
||||
return `
|
||||
<head>
|
||||
<title>${headerContent}</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Roboto:400,500,700);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
`;
|
||||
};
|
69
lib/emails/templates/common/scheduling-body-head.ts
Normal file
69
lib/emails/templates/common/scheduling-body-head.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
export type BodyHeadType = "checkCircle" | "xCircle" | "calendarCircle";
|
||||
|
||||
export const getHeadImage = (headerType: BodyHeadType) => {
|
||||
switch (headerType) {
|
||||
case "checkCircle":
|
||||
return isProduction
|
||||
? "https://www.cal.com/emails/checkCircle@2x.png"
|
||||
: "https://i.imgur.com/6BHFgjS.png";
|
||||
case "xCircle":
|
||||
return isProduction ? "https://www.cal.com/emails/xCircle@2x.png" : "https://i.imgur.com/44Dq2je.png";
|
||||
case "calendarCircle":
|
||||
return isProduction
|
||||
? "https://www.cal.com/emails/calendarCircle@2x.png"
|
||||
: "https://i.imgur.com/aQOp1mm.png";
|
||||
}
|
||||
};
|
||||
|
||||
export const emailSchedulingBodyHeader = (headerType: BodyHeadType): string => {
|
||||
const image = getHeadImage(headerType);
|
||||
|
||||
return `
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;padding-top:40px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 20px 0 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:558px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:64px;">
|
||||
<img height="64" src="${image}" style="border:0;display:block;outline:none;text-decoration:none;height:64px;width:100%;font-size:13px;" width="64" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
};
|
251
lib/emails/templates/forgot-password-email.ts
Normal file
251
lib/emails/templates/forgot-password-email.ts
Normal file
|
@ -0,0 +1,251 @@
|
|||
import { TFunction } from "next-i18next";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { serverConfig } from "@lib/serverConfig";
|
||||
|
||||
import { emailHead } from "./common/head";
|
||||
|
||||
export type PasswordReset = {
|
||||
language: TFunction;
|
||||
user: {
|
||||
name?: string | null;
|
||||
email: string;
|
||||
};
|
||||
resetLink: string;
|
||||
};
|
||||
|
||||
export const PASSWORD_RESET_EXPIRY_HOURS = 6;
|
||||
|
||||
export default class ForgotPasswordEmail {
|
||||
passwordEvent: PasswordReset;
|
||||
|
||||
constructor(passwordEvent: PasswordReset) {
|
||||
this.passwordEvent = passwordEvent;
|
||||
}
|
||||
|
||||
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 getMailerOptions() {
|
||||
return {
|
||||
transport: serverConfig.transport,
|
||||
from: serverConfig.from,
|
||||
};
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
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 printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_PASSWORD_RESET_EMAIL_ERROR", this.passwordEvent.user.email, error);
|
||||
}
|
||||
|
||||
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 `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;padding-top:40px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 20px 0 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
<div style="line-height: 6px;">
|
||||
<p>${this.passwordEvent.language("hi_user_name", {
|
||||
user: this.passwordEvent.user.name,
|
||||
})},</p>
|
||||
<p style="font-weight: 400; line-height: 24px;">${this.passwordEvent.language(
|
||||
"someone_requested_password_reset"
|
||||
)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
|
||||
<p style="display:inline-block;background:#292929;color:#292929;font-family:Roboto, Helvetica, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
|
||||
<a href="${
|
||||
this.passwordEvent.resetLink
|
||||
}" target="_blank" style="color: #FFFFFF; text-decoration: none">${this.passwordEvent.language(
|
||||
"change_password"
|
||||
)} <img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
<div style="line-height: 6px;">
|
||||
<p style="font-weight: 400; line-height: 24px;">${this.passwordEvent.language(
|
||||
"password_reset_instructions"
|
||||
)}</p>
|
||||
</div>
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="font-weight: 400; line-height: 24px;">${this.passwordEvent.language(
|
||||
"have_any_questions"
|
||||
)} <a href="mailto:support@cal.com" style="color: #3E3E3E" target="_blank">${this.passwordEvent.language(
|
||||
"contact_our_support_team"
|
||||
)}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
220
lib/emails/templates/organizer-cancelled-email.ts
Normal file
220
lib/emails/templates/organizer-cancelled-email.ts
Normal file
|
@ -0,0 +1,220 @@
|
|||
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 } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
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<string, unknown> {
|
||||
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.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("event_request_cancelled")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("xCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"event_request_cancelled"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
259
lib/emails/templates/organizer-payment-refund-failed-email.ts
Normal file
259
lib/emails/templates/organizer-payment-refund-failed-email.ts
Normal file
|
@ -0,0 +1,259 @@
|
|||
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 } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
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<string, unknown> {
|
||||
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.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("a_refund_failed")}
|
||||
${this.calEvent.language("check_with_provider_and_user", { user: this.calEvent.attendees[0].name })}
|
||||
${
|
||||
this.calEvent.paymentInfo &&
|
||||
this.calEvent.language("error_message", { errorMessage: this.calEvent.paymentInfo.reason })
|
||||
}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("xCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"a_refund_failed"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"check_with_provider_and_user",
|
||||
{ user: this.calEvent.attendees[0].name }
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
${this.getRefundInformation()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getRefundInformation(): string {
|
||||
const paymentInfo = this.calEvent.paymentInfo;
|
||||
let refundInformation = "";
|
||||
|
||||
if (paymentInfo) {
|
||||
if (paymentInfo.reason) {
|
||||
refundInformation = `
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"error_message",
|
||||
{ errorMessage: paymentInfo.reason }
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
if (paymentInfo.id) {
|
||||
refundInformation += `
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">Payment ${paymentInfo.id}</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return refundInformation;
|
||||
}
|
||||
}
|
281
lib/emails/templates/organizer-request-email.ts
Normal file
281
lib/emails/templates/organizer-request-email.ts
Normal file
|
@ -0,0 +1,281 @@
|
|||
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 } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
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<string, unknown> {
|
||||
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.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("event_awaiting_approval")}
|
||||
${this.calEvent.language("someone_requested_an_event")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
${this.calEvent.language("confirm_or_reject_request")}
|
||||
${process.env.BASE_URL} + "/bookings/upcoming"
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("calendarCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"event_awaiting_approval"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"someone_requested_an_event"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
|
||||
<p style="display:inline-block;background:#292929;color:#ffffff;font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
|
||||
${this.getManageLink()}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getManageLink(): string {
|
||||
const manageText = this.calEvent.language("confirm_or_reject_request");
|
||||
const manageLink = process.env.BASE_URL + "/bookings/upcoming";
|
||||
return `<a style="color: #FFFFFF; text-decoration: none;" href="${manageLink}" target="_blank">${manageText} <img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>`;
|
||||
}
|
||||
}
|
280
lib/emails/templates/organizer-request-reminder-email.ts
Normal file
280
lib/emails/templates/organizer-request-reminder-email.ts
Normal file
|
@ -0,0 +1,280 @@
|
|||
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 } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
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<string, unknown> {
|
||||
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.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("event_still_awaiting_approval")}
|
||||
${this.calEvent.language("someone_requested_an_event")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
${this.calEvent.language("confirm_or_reject_request")}
|
||||
${process.env.BASE_URL} + "/bookings/upcoming"
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("calendarCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"event_still_awaiting_approval"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"someone_requested_an_event"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
|
||||
<p style="display:inline-block;background:#292929;color:#ffffff;font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
|
||||
${this.getManageLink()}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getManageLink(): string {
|
||||
const manageText = this.calEvent.language("confirm_or_reject_request");
|
||||
const manageLink = process.env.BASE_URL + "/bookings/upcoming";
|
||||
return `<a style="color: #FFFFFF; text-decoration: none;" href="${manageLink}" target="_blank">${manageText} <img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>`;
|
||||
}
|
||||
}
|
269
lib/emails/templates/organizer-rescheduled-email.ts
Normal file
269
lib/emails/templates/organizer-rescheduled-email.ts
Normal file
|
@ -0,0 +1,269 @@
|
|||
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 "@lib/CalEventParser";
|
||||
|
||||
import { emailHead } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
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<string, unknown> {
|
||||
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.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("event_has_been_rescheduled")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
${this.calEvent.language("need_to_reschedule_or_cancel")}
|
||||
${getCancelLink(this.calEvent)}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("calendarCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"event_has_been_rescheduled"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||
${this.getManageLink()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
495
lib/emails/templates/organizer-scheduled-email.ts
Normal file
495
lib/emails/templates/organizer-scheduled-email.ts
Normal file
|
@ -0,0 +1,495 @@
|
|||
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 nodemailer from "nodemailer";
|
||||
|
||||
import { getCancelLink } from "@lib/CalEventParser";
|
||||
import { CalendarEvent, Person } from "@lib/calendarClient";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
import { serverConfig } from "@lib/serverConfig";
|
||||
|
||||
import { emailHead } from "./common/head";
|
||||
import { emailSchedulingBodyHeader } from "./common/scheduling-body-head";
|
||||
|
||||
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.language("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<string, unknown> {
|
||||
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.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
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.language("new_event_scheduled")}
|
||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
${this.calEvent.language("need_to_reschedule_or_cancel")}
|
||||
${getCancelLink(this.calEvent)}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.calEvent.organizer.email, error);
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.language("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.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("checkCircle")}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.language(
|
||||
"new_event_scheduled"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.language(
|
||||
"emailed_you_and_any_other_attendees"
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
|
||||
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||
${this.getManageLink()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getManageLink(): string {
|
||||
const manageText = this.calEvent.language("manage_this_event");
|
||||
return `<p>${this.calEvent.language(
|
||||
"need_to_reschedule_or_cancel"
|
||||
)}</p><p style="font-weight: 400; line-height: 24px;"><a href="${getCancelLink(
|
||||
this.calEvent
|
||||
)}" style="color: #3E3E3E;" alt="${manageText}">${manageText}</a></p>`;
|
||||
}
|
||||
|
||||
protected getWhat(): string {
|
||||
return `
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("what")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.type}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected getWhen(): string {
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("when")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">
|
||||
${this.calEvent.language(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.language(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format(
|
||||
"YYYY"
|
||||
)} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
|
||||
"h:mma"
|
||||
)} <span style="color: #888888">(${this.getTimezone()})</span>
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected getWho(): string {
|
||||
const attendees = this.calEvent.attendees
|
||||
.map((attendee) => {
|
||||
return `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
attendee?.name || `${this.calEvent.language("guest")}`
|
||||
} <span style="color: #888888"><a href="mailto:${attendee.email}" style="color: #888888;">${
|
||||
attendee.email
|
||||
}</a></span></div>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const organizer = `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
this.calEvent.organizer.name
|
||||
} - ${this.calEvent.language("organizer")} <span style="color: #888888"><a href="mailto:${
|
||||
this.calEvent.organizer.email
|
||||
}" style="color: #888888;">${this.calEvent.organizer.email}</a></span></div>`;
|
||||
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("who")}</p>
|
||||
${organizer + attendees}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected getAdditionalNotes(): string {
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("additional_notes")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.description}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getLocation(): string {
|
||||
let providerName = this.calEvent.location ? getIntegrationName(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 (this.calEvent.videoCallData) {
|
||||
const meetingId = this.calEvent.videoCallData.id;
|
||||
const meetingPassword = this.calEvent.videoCallData.password;
|
||||
const meetingUrl = this.calEvent.videoCallData.url;
|
||||
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
|
||||
meetingUrl &&
|
||||
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}"><img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>`
|
||||
}</p>
|
||||
${
|
||||
meetingId &&
|
||||
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
|
||||
"meeting_id"
|
||||
)}: <span>${meetingId}</span></div>`
|
||||
}
|
||||
${
|
||||
meetingPassword &&
|
||||
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
|
||||
"meeting_password"
|
||||
)}: <span>${meetingPassword}</span></div>`
|
||||
}
|
||||
${
|
||||
meetingUrl &&
|
||||
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}: <a href="${meetingUrl}" alt="${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}" style="color: #3E3E3E" target="_blank">${meetingUrl}</a></div>`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.calEvent.additionInformation?.hangoutLink) {
|
||||
const hangoutLink: string = this.calEvent.additionInformation.hangoutLink;
|
||||
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
hangoutLink &&
|
||||
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}"><img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>`
|
||||
}</p>
|
||||
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.language(
|
||||
"meeting_url"
|
||||
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
240
lib/emails/templates/team-invite-email.ts
Normal file
240
lib/emails/templates/team-invite-email.ts
Normal file
|
@ -0,0 +1,240 @@
|
|||
import { TFunction } from "next-i18next";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { serverConfig } from "@lib/serverConfig";
|
||||
|
||||
import { emailHead } from "./common/head";
|
||||
|
||||
export type TeamInvite = {
|
||||
language: TFunction;
|
||||
from: string;
|
||||
to: string;
|
||||
teamName: string;
|
||||
joinLink: string;
|
||||
};
|
||||
|
||||
export default class TeamInviteEmail {
|
||||
teamInviteEvent: TeamInvite;
|
||||
|
||||
constructor(teamInviteEvent: TeamInvite) {
|
||||
this.teamInviteEvent = teamInviteEvent;
|
||||
}
|
||||
|
||||
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 getMailerOptions() {
|
||||
return {
|
||||
transport: serverConfig.transport,
|
||||
from: serverConfig.from,
|
||||
};
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
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 printNodeMailerError(error: Error): void {
|
||||
console.error("SEND_TEAM_INVITE_EMAIL_ERROR", this.teamInviteEvent.to, error);
|
||||
}
|
||||
|
||||
protected getTextBody(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.teamInviteEvent.language("user_invited_you", {
|
||||
user: this.teamInviteEvent.from,
|
||||
team: this.teamInviteEvent.teamName,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:89px;">
|
||||
<a href="#" target="_blank">
|
||||
<img height="19" src="https://i.imgur.com/esapZ47.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;padding-top:40px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 20px 0 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
<div style="line-height: 6px;">
|
||||
<p>${this.teamInviteEvent.language("user_invited_you", {
|
||||
user: this.teamInviteEvent.from,
|
||||
team: this.teamInviteEvent.teamName,
|
||||
})}!</p>
|
||||
<p style="font-weight: 400; line-height: 24px;">${this.teamInviteEvent.language(
|
||||
"calcom_explained"
|
||||
)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
|
||||
<p style="display:inline-block;background:#292929;color:#292929;font-family:Roboto, Helvetica, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
|
||||
<a href="${
|
||||
this.teamInviteEvent.joinLink
|
||||
}" target="_blank" style="color: #FFFFFF; text-decoration: none">${this.teamInviteEvent.language(
|
||||
"accept_invitation"
|
||||
)} <img src="https://i.imgur.com/rKsIBcc.png" width="12px"></img></a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
<div style="line-height: 6px;">
|
||||
<p style="font-weight: 400; line-height: 24px;">${this.teamInviteEvent.language(
|
||||
"have_any_questions"
|
||||
)} <a href="mailto:support@cal.com" style="color: #3E3E3E" target="_blank">${this.teamInviteEvent.language(
|
||||
"contact_our_support_team"
|
||||
)}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -4,19 +4,13 @@ import merge from "lodash/merge";
|
|||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { AdditionInformation, CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
|
||||
import EventAttendeeMail from "@lib/emails/EventAttendeeMail";
|
||||
import EventAttendeeRescheduledMail from "@lib/emails/EventAttendeeRescheduledMail";
|
||||
import { DailyEventResult, FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { ZoomEventResult } from "@lib/integrations/Zoom/ZoomVideoApiAdapter";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { LocationType } from "@lib/location";
|
||||
import prisma from "@lib/prisma";
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient";
|
||||
|
||||
export type Event = AdditionInformation & { name: string; id: string; disableConfirmationEmail?: boolean } & (
|
||||
| ZoomEventResult
|
||||
| DailyEventResult
|
||||
);
|
||||
export type Event = AdditionInformation & VideoCallData;
|
||||
|
||||
export interface EventResult {
|
||||
type: string;
|
||||
|
@ -25,7 +19,6 @@ export interface EventResult {
|
|||
createdEvent?: Event;
|
||||
updatedEvent?: Event;
|
||||
originalEvent: CalendarEvent;
|
||||
videoCallData?: VideoCallData;
|
||||
}
|
||||
|
||||
export interface CreateUpdateResult {
|
||||
|
@ -47,9 +40,51 @@ export interface PartialReference {
|
|||
meetingUrl?: string | null;
|
||||
}
|
||||
|
||||
interface GetLocationRequestFromIntegrationRequest {
|
||||
location: string;
|
||||
}
|
||||
export const isZoom = (location: string): boolean => {
|
||||
return location === "integrations:zoom";
|
||||
};
|
||||
|
||||
export const isDaily = (location: string): boolean => {
|
||||
return location === "integrations:daily";
|
||||
};
|
||||
|
||||
export const isDedicatedIntegration = (location: string): boolean => {
|
||||
return isZoom(location) || isDaily(location);
|
||||
};
|
||||
|
||||
export const getLocationRequestFromIntegration = (location: string) => {
|
||||
if (
|
||||
location === LocationType.GoogleMeet.valueOf() ||
|
||||
location === LocationType.Zoom.valueOf() ||
|
||||
location === LocationType.Daily.valueOf()
|
||||
) {
|
||||
const requestId = uuidv5(location, uuidv5.URL);
|
||||
|
||||
return {
|
||||
conferenceData: {
|
||||
createRequest: {
|
||||
requestId: requestId,
|
||||
},
|
||||
},
|
||||
location,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const processLocation = (event: CalendarEvent): CalendarEvent => {
|
||||
// If location is set to an integration location
|
||||
// Build proper transforms for evt object
|
||||
// Extend evt object with those transformations
|
||||
if (event.location?.includes("integration")) {
|
||||
const maybeLocationRequestObject = getLocationRequestFromIntegration(event.location);
|
||||
|
||||
event = merge(event, maybeLocationRequestObject);
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
export default class EventManager {
|
||||
calendarCredentials: Array<Credential>;
|
||||
|
@ -79,38 +114,27 @@ export default class EventManager {
|
|||
* @param event
|
||||
*/
|
||||
public async create(event: Ensure<CalendarEvent, "language">): Promise<CreateUpdateResult> {
|
||||
const evt = EventManager.processLocation(event);
|
||||
const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null;
|
||||
const evt = processLocation(event);
|
||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
||||
|
||||
// First, create all calendar events. If this is a dedicated integration event, don't send a mail right here.
|
||||
const results: Array<EventResult> = await this.createAllCalendarEvents(evt, isDedicated);
|
||||
const results: Array<EventResult> = await this.createAllCalendarEvents(evt);
|
||||
// If and only if event type is a dedicated meeting, create a dedicated video meeting.
|
||||
if (isDedicated) {
|
||||
const result = await this.createVideoEvent(evt);
|
||||
if (result.videoCallData) {
|
||||
evt.videoCallData = result.videoCallData;
|
||||
if (result.createdEvent) {
|
||||
evt.videoCallData = result.createdEvent;
|
||||
}
|
||||
results.push(result);
|
||||
} else {
|
||||
await EventManager.sendAttendeeMail("new", results, evt);
|
||||
}
|
||||
|
||||
const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => {
|
||||
let uid = "";
|
||||
if (result.createdEvent) {
|
||||
const isDailyResult = result.type === "daily_video";
|
||||
if (isDailyResult) {
|
||||
uid = (result.createdEvent as DailyEventResult).name.toString();
|
||||
} else {
|
||||
uid = (result.createdEvent as ZoomEventResult).id.toString();
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: result.type,
|
||||
uid,
|
||||
meetingId: result.videoCallData?.id.toString(),
|
||||
meetingPassword: result.videoCallData?.password,
|
||||
meetingUrl: result.videoCallData?.url,
|
||||
uid: result.createdEvent?.id.toString() ?? "",
|
||||
meetingId: result.createdEvent?.id.toString(),
|
||||
meetingPassword: result.createdEvent?.password,
|
||||
meetingUrl: result.createdEvent?.url,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -130,7 +154,7 @@ export default class EventManager {
|
|||
event: Ensure<CalendarEvent, "language">,
|
||||
rescheduleUid: string
|
||||
): Promise<CreateUpdateResult> {
|
||||
const evt = EventManager.processLocation(event);
|
||||
const evt = processLocation(event);
|
||||
|
||||
if (!rescheduleUid) {
|
||||
throw new Error("You called eventManager.update without an `rescheduleUid`. This should never happen.");
|
||||
|
@ -160,19 +184,18 @@ export default class EventManager {
|
|||
throw new Error("booking not found");
|
||||
}
|
||||
|
||||
const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null;
|
||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
||||
// First, create all calendar events. If this is a dedicated integration event, don't send a mail right here.
|
||||
const results: Array<EventResult> = await this.updateAllCalendarEvents(evt, booking, isDedicated);
|
||||
const results: Array<EventResult> = await this.updateAllCalendarEvents(evt, booking);
|
||||
// If and only if event type is a dedicated meeting, update the dedicated video meeting.
|
||||
if (isDedicated) {
|
||||
const result = await this.updateVideoEvent(evt, booking);
|
||||
if (result.videoCallData) {
|
||||
evt.videoCallData = result.videoCallData;
|
||||
if (result.updatedEvent) {
|
||||
evt.videoCallData = result.updatedEvent;
|
||||
}
|
||||
results.push(result);
|
||||
} else {
|
||||
await EventManager.sendAttendeeMail("reschedule", results, evt);
|
||||
}
|
||||
|
||||
// Now we can delete the old booking and its references.
|
||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
|
@ -213,15 +236,12 @@ export default class EventManager {
|
|||
* @private
|
||||
*/
|
||||
|
||||
private async createAllCalendarEvents(
|
||||
event: CalendarEvent,
|
||||
noMail: boolean | null
|
||||
): Promise<Array<EventResult>> {
|
||||
private async createAllCalendarEvents(event: CalendarEvent): Promise<Array<EventResult>> {
|
||||
const [firstCalendar] = this.calendarCredentials;
|
||||
if (!firstCalendar) {
|
||||
return [];
|
||||
}
|
||||
return [await createEvent(firstCalendar, event, noMail)];
|
||||
return [await createEvent(firstCalendar, event)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -267,19 +287,18 @@ export default class EventManager {
|
|||
*
|
||||
* @param event
|
||||
* @param booking
|
||||
* @param noMail
|
||||
* @private
|
||||
*/
|
||||
private updateAllCalendarEvents(
|
||||
event: CalendarEvent,
|
||||
booking: PartialBooking | null,
|
||||
noMail: boolean | null
|
||||
booking: PartialBooking
|
||||
): Promise<Array<EventResult>> {
|
||||
return async.mapLimit(this.calendarCredentials, 5, async (credential) => {
|
||||
return async.mapLimit(this.calendarCredentials, 5, async (credential: Credential) => {
|
||||
const bookingRefUid = booking
|
||||
? booking.references.filter((ref) => ref.type === credential.type)[0]?.uid
|
||||
: null;
|
||||
return updateEvent(credential, event, noMail, bookingRefUid);
|
||||
|
||||
return updateEvent(credential, event, bookingRefUid);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -295,172 +314,9 @@ export default class EventManager {
|
|||
|
||||
if (credential) {
|
||||
const bookingRef = booking ? booking.references.filter((ref) => ref.type === credential.type)[0] : null;
|
||||
const bookingRefUid = bookingRef ? bookingRef.uid : null;
|
||||
return updateMeeting(credential, event, bookingRefUid).then((returnVal: EventResult) => {
|
||||
// Some video integrations, such as Zoom, don't return any data about the booking when updating it.
|
||||
if (returnVal.videoCallData === undefined) {
|
||||
returnVal.videoCallData = EventManager.bookingReferenceToVideoCallData(bookingRef);
|
||||
}
|
||||
return returnVal;
|
||||
});
|
||||
return updateMeeting(credential, event, bookingRef);
|
||||
} else {
|
||||
return Promise.reject("No suitable credentials given for the requested integration name.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given location describes a dedicated integration that
|
||||
* delivers meeting credentials. Zoom, for example, is dedicated, because it
|
||||
* needs to be called independently from any calendar APIs to receive meeting
|
||||
* credentials. Google Meetings, in contrast, are not dedicated, because they
|
||||
* are created while scheduling a regular calendar event by simply adding some
|
||||
* attributes to the payload JSON.
|
||||
*
|
||||
* @param location
|
||||
* @private
|
||||
*/
|
||||
private static isDedicatedIntegration(location: string): boolean {
|
||||
// Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't.
|
||||
|
||||
return location === "integrations:zoom" || location === "integrations:daily";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for processLocation: Returns the conferenceData object to be merged
|
||||
* with the CalendarEvent.
|
||||
*
|
||||
* @param locationObj
|
||||
* @private
|
||||
*/
|
||||
private static getLocationRequestFromIntegration(locationObj: GetLocationRequestFromIntegrationRequest) {
|
||||
const location = locationObj.location;
|
||||
|
||||
if (
|
||||
location === LocationType.GoogleMeet.valueOf() ||
|
||||
location === LocationType.Zoom.valueOf() ||
|
||||
location === LocationType.Daily.valueOf()
|
||||
) {
|
||||
const requestId = uuidv5(location, uuidv5.URL);
|
||||
|
||||
return {
|
||||
conferenceData: {
|
||||
createRequest: {
|
||||
requestId: requestId,
|
||||
},
|
||||
},
|
||||
location,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a CalendarEvent and adds a ConferenceData object to the event
|
||||
* if the event has an integration-related location.
|
||||
*
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
private static processLocation<T extends CalendarEvent>(event: T): T {
|
||||
// If location is set to an integration location
|
||||
// Build proper transforms for evt object
|
||||
// Extend evt object with those transformations
|
||||
if (event.location?.includes("integration")) {
|
||||
const maybeLocationRequestObject = EventManager.getLocationRequestFromIntegration({
|
||||
location: event.location,
|
||||
});
|
||||
|
||||
event = merge(event, maybeLocationRequestObject);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a PartialReference object and, if all data is complete,
|
||||
* returns a VideoCallData object containing the meeting information.
|
||||
*
|
||||
* @param reference
|
||||
* @private
|
||||
*/
|
||||
private static bookingReferenceToVideoCallData(
|
||||
reference: PartialReference | null
|
||||
): VideoCallData | undefined {
|
||||
let isComplete = true;
|
||||
|
||||
if (!reference) {
|
||||
throw new Error("missing reference");
|
||||
}
|
||||
|
||||
switch (reference.type) {
|
||||
case "zoom_video":
|
||||
// Zoom meetings in our system should always have an ID, a password and a join URL. In the
|
||||
// future, it might happen that we consider making passwords for Zoom meetings optional.
|
||||
// Then, this part below (where the password existence is checked) needs to be adapted.
|
||||
isComplete =
|
||||
reference.meetingId !== undefined &&
|
||||
reference.meetingPassword !== undefined &&
|
||||
reference.meetingUrl !== undefined;
|
||||
break;
|
||||
default:
|
||||
isComplete = true;
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
return {
|
||||
type: reference.type,
|
||||
// The null coalescing operator should actually never be used here, because we checked if it's defined beforehand.
|
||||
id: reference.meetingId ?? "",
|
||||
password: reference.meetingPassword ?? "",
|
||||
url: reference.meetingUrl ?? "",
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally sends an email to the attendee.
|
||||
*
|
||||
* @param type
|
||||
* @param results
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
private static async sendAttendeeMail(
|
||||
type: "new" | "reschedule",
|
||||
results: Array<EventResult>,
|
||||
event: CalendarEvent
|
||||
) {
|
||||
if (
|
||||
!results.length ||
|
||||
!results.some((eRes) => (eRes.createdEvent || eRes.updatedEvent)?.disableConfirmationEmail)
|
||||
) {
|
||||
const metadata: AdditionInformation = {};
|
||||
if (results.length) {
|
||||
// TODO: Handle created event metadata more elegantly
|
||||
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
||||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||
}
|
||||
|
||||
event.additionInformation = metadata;
|
||||
|
||||
let attendeeMail;
|
||||
switch (type) {
|
||||
case "reschedule":
|
||||
attendeeMail = new EventAttendeeRescheduledMail(event);
|
||||
break;
|
||||
case "new":
|
||||
attendeeMail = new EventAttendeeMail(event);
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await attendeeMail.sendEmail();
|
||||
} catch (e) {
|
||||
console.error("attendeeMail.sendEmail failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import { TFunction } from "next-i18next";
|
||||
|
||||
import { buildMessageTemplate, VarType } from "../../emails/buildMessageTemplate";
|
||||
|
||||
export const forgotPasswordSubjectTemplate = (t: TFunction): string => {
|
||||
const text = t("forgot_your_password_calcom");
|
||||
return text;
|
||||
};
|
||||
|
||||
export const forgotPasswordMessageTemplate = (t: TFunction): string => {
|
||||
const text = `${t("hey_there")}
|
||||
|
||||
${t("use_link_to_reset_password")}
|
||||
{{link}}
|
||||
|
||||
${t("link_expires", { expiresIn: 6 })}
|
||||
|
||||
- Cal.com`;
|
||||
return text;
|
||||
};
|
||||
|
||||
export const buildForgotPasswordMessage = (vars: VarType) => {
|
||||
return buildMessageTemplate({
|
||||
subjectTemplate: forgotPasswordSubjectTemplate(vars.language),
|
||||
messageTemplate: forgotPasswordMessageTemplate(vars.language),
|
||||
vars,
|
||||
});
|
||||
};
|
|
@ -18,7 +18,6 @@ import { symmetricDecrypt } from "@lib/crypto";
|
|||
import logger from "@lib/logger";
|
||||
|
||||
import { IntegrationCalendar, CalendarApiAdapter, CalendarEvent } from "../../calendarClient";
|
||||
import { stripHtml } from "../../emails/helpers";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
|
@ -80,7 +79,7 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
start: this.convertDate(event.startTime),
|
||||
duration: this.getDuration(event.startTime, event.endTime),
|
||||
title: event.title,
|
||||
description: stripHtml(event.description ?? ""),
|
||||
description: event.description ?? "",
|
||||
location: event.location,
|
||||
organizer: { email: event.organizer.email, name: event.organizer.name },
|
||||
attendees: this.getAttendees(event.attendees),
|
||||
|
@ -138,7 +137,7 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
start: this.convertDate(event.startTime),
|
||||
duration: this.getDuration(event.startTime, event.endTime),
|
||||
title: event.title,
|
||||
description: stripHtml(event.description ?? ""),
|
||||
description: event.description ?? "",
|
||||
location: event.location,
|
||||
organizer: { email: event.organizer.email, name: event.organizer.name },
|
||||
attendees: this.getAttendees(event.attendees),
|
||||
|
|
|
@ -18,7 +18,6 @@ import { symmetricDecrypt } from "@lib/crypto";
|
|||
import logger from "@lib/logger";
|
||||
|
||||
import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "../../calendarClient";
|
||||
import { stripHtml } from "../../emails/helpers";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
|
@ -83,7 +82,7 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
start: this.convertDate(event.startTime),
|
||||
duration: this.getDuration(event.startTime, event.endTime),
|
||||
title: event.title,
|
||||
description: stripHtml(event.description ?? ""),
|
||||
description: event.description ?? "",
|
||||
location: event.location,
|
||||
organizer: { email: event.organizer.email, name: event.organizer.name },
|
||||
attendees: this.getAttendees(event.attendees),
|
||||
|
@ -142,7 +141,7 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
start: this.convertDate(event.startTime),
|
||||
duration: this.getDuration(event.startTime, event.endTime),
|
||||
title: event.title,
|
||||
description: stripHtml(event.description ?? ""),
|
||||
description: event.description ?? "",
|
||||
location: event.location,
|
||||
organizer: { email: event.organizer.email, name: event.organizer.name },
|
||||
attendees: this.getAttendees(event.attendees),
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { BASE_URL } from "@lib/config/constants";
|
||||
import { handleErrorsJson } from "@lib/errors";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import prisma from "@lib/prisma";
|
||||
import { VideoApiAdapter } from "@lib/videoClient";
|
||||
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
|
||||
|
||||
export interface DailyReturnType {
|
||||
/** Long UID string ie: 987b5eb5-d116-4a4e-8e2c-14fcb5710966 */
|
||||
|
@ -67,7 +69,7 @@ const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
|||
});
|
||||
}
|
||||
|
||||
async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent) {
|
||||
async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise<VideoCallData> {
|
||||
if (!event.uid) {
|
||||
throw new Error("We need need the booking uid to create the Daily reference in DB");
|
||||
}
|
||||
|
@ -89,7 +91,12 @@ const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
|||
},
|
||||
});
|
||||
|
||||
return dailyEvent;
|
||||
return Promise.resolve({
|
||||
type: "daily_video",
|
||||
id: dailyEvent.name,
|
||||
password: "",
|
||||
url: BASE_URL + "/call/" + event.uid,
|
||||
});
|
||||
}
|
||||
|
||||
const translateEvent = (event: CalendarEvent) => {
|
||||
|
@ -133,15 +140,20 @@ const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
|||
getAvailability: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
createMeeting: async (event: CalendarEvent) => createOrUpdateMeeting("/rooms", event),
|
||||
deleteMeeting: (uid: string) =>
|
||||
fetch("https://api.daily.co/v1/rooms/" + uid, {
|
||||
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> =>
|
||||
createOrUpdateMeeting("/rooms", event),
|
||||
deleteMeeting: async (uid: string): Promise<void> => {
|
||||
await fetch("https://api.daily.co/v1/rooms/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + dailyApiToken,
|
||||
},
|
||||
}).then(handleErrorsJson),
|
||||
updateMeeting: (uid: string, event: CalendarEvent) => createOrUpdateMeeting("/rooms/" + uid, event),
|
||||
}).then(handleErrorsJson);
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
updateMeeting: (bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> =>
|
||||
createOrUpdateMeeting("/rooms/" + bookingRef.uid, event),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
255
lib/integrations/GoogleCalendar/GoogleCalendarApiAdapter.ts
Normal file
255
lib/integrations/GoogleCalendar/GoogleCalendarApiAdapter.ts
Normal file
|
@ -0,0 +1,255 @@
|
|||
import { Credential, Prisma } from "@prisma/client";
|
||||
import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
|
||||
import { Auth, calendar_v3, google } from "googleapis";
|
||||
|
||||
import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "@lib/calendarClient";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export interface ConferenceData {
|
||||
createRequest: calendar_v3.Schema$CreateConferenceRequest;
|
||||
}
|
||||
|
||||
const googleAuth = (credential: Credential) => {
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS!).web;
|
||||
const myGoogleAuth = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
|
||||
const googleCredentials = credential.key as Auth.Credentials;
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
|
||||
// FIXME - type errors IDK Why this is a protected method ¯\_(ツ)_/¯
|
||||
const isExpired = () => myGoogleAuth.isTokenExpiring();
|
||||
|
||||
const refreshAccessToken = () =>
|
||||
myGoogleAuth
|
||||
// FIXME - type errors IDK Why this is a protected method ¯\_(ツ)_/¯
|
||||
.refreshToken(googleCredentials.refresh_token)
|
||||
.then((res: GetTokenResponse) => {
|
||||
const token = res.res?.data;
|
||||
googleCredentials.access_token = token.access_token;
|
||||
googleCredentials.expiry_date = token.expiry_date;
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: googleCredentials as Prisma.InputJsonValue,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error refreshing google token", err);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
|
||||
return {
|
||||
getToken: () => (!isExpired() ? Promise.resolve(myGoogleAuth) : refreshAccessToken()),
|
||||
};
|
||||
};
|
||||
|
||||
export const GoogleCalendarApiAdapter = (credential: Credential): CalendarApiAdapter => {
|
||||
const auth = googleAuth(credential);
|
||||
const integrationType = "google_calendar";
|
||||
|
||||
return {
|
||||
getAvailability: (dateFrom, dateTo, selectedCalendars) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === integrationType)
|
||||
.map((e) => e.externalId);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
(selectedCalendarIds.length === 0
|
||||
? calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
)
|
||||
.then((calsIds) => {
|
||||
calendar.freebusy.query(
|
||||
{
|
||||
requestBody: {
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id: id })),
|
||||
},
|
||||
},
|
||||
(err, apires) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(Object.values(apires.data.calendars).flatMap((item) => item["busy"]));
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
),
|
||||
createEvent: (event: CalendarEvent) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: event.description,
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: false,
|
||||
overrides: [{ method: "email", minutes: 10 }],
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = event.location;
|
||||
}
|
||||
|
||||
if (event.conferenceData && event.location === "integrations:google:meet") {
|
||||
payload["conferenceData"] = event.conferenceData;
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: "primary",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err || !event?.data) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
updateEvent: (uid: string, event: CalendarEvent) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: event.description,
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: false,
|
||||
overrides: [{ method: "email", minutes: 10 }],
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = event.location;
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.update(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
requestBody: payload,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
deleteEvent: (uid: string) =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.delete(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
),
|
||||
listCalendars: () =>
|
||||
new Promise((resolve, reject) =>
|
||||
auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => {
|
||||
resolve(
|
||||
cals.data.items?.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No id",
|
||||
integration: integrationType,
|
||||
name: cal.summary ?? "No name",
|
||||
primary: cal.primary ?? false,
|
||||
};
|
||||
return calendar;
|
||||
}) || []
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,216 @@
|
|||
import { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
|
||||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "@lib/calendarClient";
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export type BufferedBusyTime = {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
type O365AuthCredentials = {
|
||||
expiry_date: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
const o365Auth = (credential: Credential) => {
|
||||
const isExpired = (expiryDate: number) => expiryDate < Math.round(+new Date() / 1000);
|
||||
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
||||
|
||||
const refreshAccessToken = (refreshToken: string) => {
|
||||
return fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||
client_id: process.env.MS_GRAPH_CLIENT_ID!,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
client_secret: process.env.MS_GRAPH_CLIENT_SECRET!,
|
||||
}),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody) => {
|
||||
o365AuthCredentials.access_token = responseBody.access_token;
|
||||
o365AuthCredentials.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in);
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: o365AuthCredentials,
|
||||
},
|
||||
})
|
||||
.then(() => o365AuthCredentials.access_token);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
getToken: () =>
|
||||
!isExpired(o365AuthCredentials.expiry_date)
|
||||
? Promise.resolve(o365AuthCredentials.access_token)
|
||||
: refreshAccessToken(o365AuthCredentials.refresh_token),
|
||||
};
|
||||
};
|
||||
|
||||
export const Office365CalendarApiAdapter = (credential: Credential): CalendarApiAdapter => {
|
||||
const auth = o365Auth(credential);
|
||||
|
||||
const translateEvent = (event: CalendarEvent) => {
|
||||
return {
|
||||
subject: event.title,
|
||||
body: {
|
||||
contentType: "HTML",
|
||||
content: event.description,
|
||||
},
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees.map((attendee) => ({
|
||||
emailAddress: {
|
||||
address: attendee.email,
|
||||
name: attendee.name,
|
||||
},
|
||||
type: "required",
|
||||
})),
|
||||
location: event.location ? { displayName: event.location } : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const integrationType = "office365_calendar";
|
||||
|
||||
function listCalendars(): Promise<IntegrationCalendar[]> {
|
||||
return auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendars", {
|
||||
method: "get",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody: { value: OfficeCalendar[] }) => {
|
||||
return responseBody.value.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No Id",
|
||||
integration: integrationType,
|
||||
name: cal.name ?? "No calendar name",
|
||||
primary: cal.isDefaultCalendar ?? false,
|
||||
};
|
||||
return calendar;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getAvailability: (dateFrom, dateTo, selectedCalendars) => {
|
||||
const filter = `?startdatetime=${encodeURIComponent(dateFrom)}&enddatetime=${encodeURIComponent(
|
||||
dateTo
|
||||
)}`;
|
||||
return auth
|
||||
.getToken()
|
||||
.then((accessToken) => {
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === integrationType)
|
||||
.map((e) => e.externalId)
|
||||
.filter(Boolean);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return (
|
||||
selectedCalendarIds.length === 0
|
||||
? listCalendars().then((cals) => cals.map((e) => e.externalId).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
).then((ids) => {
|
||||
const requests = ids.map((calendarId, id) => ({
|
||||
id,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Prefer: 'outlook.timezone="Etc/GMT"',
|
||||
},
|
||||
url: `/me/calendars/${calendarId}/calendarView${filter}`,
|
||||
}));
|
||||
|
||||
type BatchResponse = {
|
||||
responses: SubResponse[];
|
||||
};
|
||||
type SubResponse = {
|
||||
body: { value: { start: { dateTime: string }; end: { dateTime: string } }[] };
|
||||
};
|
||||
|
||||
return fetch("https://graph.microsoft.com/v1.0/$batch", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ requests }),
|
||||
})
|
||||
.then(handleErrorsJson)
|
||||
.then((responseBody: BatchResponse) =>
|
||||
responseBody.responses.reduce(
|
||||
(acc: BufferedBusyTime[], subResponse) =>
|
||||
acc.concat(
|
||||
subResponse.body.value.map((evt) => {
|
||||
return {
|
||||
start: evt.start.dateTime + "Z",
|
||||
end: evt.end.dateTime + "Z",
|
||||
};
|
||||
})
|
||||
),
|
||||
[]
|
||||
)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return Promise.reject([]);
|
||||
});
|
||||
},
|
||||
createEvent: (event: CalendarEvent) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendar/events", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsJson)
|
||||
),
|
||||
deleteEvent: (uid: string) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
}).then(handleErrorsRaw)
|
||||
),
|
||||
updateEvent: (uid: string, event: CalendarEvent) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsRaw)
|
||||
),
|
||||
listCalendars,
|
||||
};
|
||||
};
|
|
@ -2,11 +2,13 @@ import { Credential } from "@prisma/client";
|
|||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import prisma from "@lib/prisma";
|
||||
import { VideoApiAdapter } from "@lib/videoClient";
|
||||
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
|
||||
|
||||
/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate */
|
||||
export interface ZoomEventResult {
|
||||
password: string;
|
||||
created_at: string;
|
||||
duration: number;
|
||||
host_id: string;
|
||||
|
@ -168,37 +170,56 @@ const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
|||
return [];
|
||||
});
|
||||
},
|
||||
createMeeting: (event: CalendarEvent) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://api.zoom.us/v2/users/me/meetings", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsJson)
|
||||
),
|
||||
deleteMeeting: (uid: string) =>
|
||||
auth.getToken().then((accessToken) =>
|
||||
fetch("https://api.zoom.us/v2/meetings/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
}).then(handleErrorsRaw)
|
||||
),
|
||||
updateMeeting: (uid: string, event: CalendarEvent) =>
|
||||
auth.getToken().then((accessToken: string) =>
|
||||
fetch("https://api.zoom.us/v2/meetings/" + uid, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsRaw)
|
||||
),
|
||||
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> => {
|
||||
const accessToken = await auth.getToken();
|
||||
|
||||
const result = await fetch("https://api.zoom.us/v2/users/me/meetings", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsJson);
|
||||
|
||||
return Promise.resolve({
|
||||
type: "zoom_video",
|
||||
id: result.id as string,
|
||||
password: result.password ?? "",
|
||||
url: result.join_url,
|
||||
});
|
||||
},
|
||||
deleteMeeting: async (uid: string): Promise<void> => {
|
||||
const accessToken = await auth.getToken();
|
||||
|
||||
await fetch("https://api.zoom.us/v2/meetings/" + uid, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
}).then(handleErrorsRaw);
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> => {
|
||||
const accessToken = await auth.getToken();
|
||||
|
||||
await fetch("https://api.zoom.us/v2/meetings/" + bookingRef.uid, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(translateEvent(event)),
|
||||
}).then(handleErrorsRaw);
|
||||
|
||||
return Promise.resolve({
|
||||
type: "zoom_video",
|
||||
id: bookingRef.meetingId as string,
|
||||
password: bookingRef.meetingPassword as string,
|
||||
url: bookingRef.meetingUrl as string,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -2,17 +2,12 @@ import { Credential } from "@prisma/client";
|
|||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import CalEventParser from "@lib/CalEventParser";
|
||||
import "@lib/emails/EventMail";
|
||||
import { getIntegrationName } from "@lib/emails/helpers";
|
||||
import { getUid } from "@lib/CalEventParser";
|
||||
import { EventResult } from "@lib/events/EventManager";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
import { AdditionInformation, CalendarEvent, EntryPoint } from "./calendarClient";
|
||||
import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail";
|
||||
import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail";
|
||||
import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail";
|
||||
import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail";
|
||||
import { CalendarEvent } from "./calendarClient";
|
||||
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
|
||||
import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
|
||||
import { Ensure } from "./types/utils";
|
||||
|
@ -31,9 +26,9 @@ export interface VideoCallData {
|
|||
type EventBusyDate = Record<"start" | "end", Date>;
|
||||
|
||||
export interface VideoApiAdapter {
|
||||
createMeeting(event: CalendarEvent): Promise<any>;
|
||||
createMeeting(event: CalendarEvent): Promise<VideoCallData>;
|
||||
|
||||
updateMeeting(uid: string, event: CalendarEvent): Promise<any>;
|
||||
updateMeeting(bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData>;
|
||||
|
||||
deleteMeeting(uid: string): Promise<unknown>;
|
||||
|
||||
|
@ -65,8 +60,7 @@ const createMeeting = async (
|
|||
credential: Credential,
|
||||
calEvent: Ensure<CalendarEvent, "language">
|
||||
): Promise<EventResult> => {
|
||||
const parser: CalEventParser = new CalEventParser(calEvent);
|
||||
const uid: string = parser.getUid();
|
||||
const uid: string = getUid(calEvent);
|
||||
|
||||
if (!credential) {
|
||||
throw new Error(
|
||||
|
@ -92,98 +86,41 @@ const createMeeting = async (
|
|||
};
|
||||
}
|
||||
|
||||
const videoCallData: VideoCallData = {
|
||||
type: credential.type,
|
||||
id: createdMeeting.id,
|
||||
password: createdMeeting.password,
|
||||
url: createdMeeting.join_url,
|
||||
};
|
||||
|
||||
if (credential.type === "daily_video") {
|
||||
videoCallData.type = "Daily.co Video";
|
||||
videoCallData.id = createdMeeting.name;
|
||||
videoCallData.url = process.env.BASE_URL + "/call/" + uid;
|
||||
}
|
||||
|
||||
const entryPoint: EntryPoint = {
|
||||
entryPointType: getIntegrationName(videoCallData),
|
||||
uri: videoCallData.url,
|
||||
label: calEvent.language("enter_meeting"),
|
||||
pin: videoCallData.password,
|
||||
};
|
||||
|
||||
const additionInformation: AdditionInformation = {
|
||||
entryPoints: [entryPoint],
|
||||
};
|
||||
|
||||
calEvent.additionInformation = additionInformation;
|
||||
calEvent.videoCallData = videoCallData;
|
||||
|
||||
try {
|
||||
const organizerMail = new VideoEventOrganizerMail(calEvent);
|
||||
await organizerMail.sendEmail();
|
||||
} catch (e) {
|
||||
console.error("organizerMail.sendEmail failed", e);
|
||||
}
|
||||
|
||||
if (!createdMeeting || !createdMeeting.disableConfirmationEmail) {
|
||||
try {
|
||||
const attendeeMail = new VideoEventAttendeeMail(calEvent);
|
||||
await attendeeMail.sendEmail();
|
||||
} catch (e) {
|
||||
console.error("attendeeMail.sendEmail failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
createdEvent: createdMeeting,
|
||||
originalEvent: calEvent,
|
||||
videoCallData: videoCallData,
|
||||
};
|
||||
};
|
||||
|
||||
const updateMeeting = async (
|
||||
credential: Credential,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRefUid: string | null
|
||||
bookingRef: PartialReference | null
|
||||
): Promise<EventResult> => {
|
||||
const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
|
||||
|
||||
if (!credential) {
|
||||
throw new Error(
|
||||
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
|
||||
);
|
||||
}
|
||||
const uid = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
|
||||
|
||||
let success = true;
|
||||
|
||||
const [firstVideoAdapter] = getVideoAdapters([credential]);
|
||||
const updatedMeeting =
|
||||
credential && bookingRefUid
|
||||
? await firstVideoAdapter.updateMeeting(bookingRefUid, calEvent).catch((e) => {
|
||||
credential && bookingRef
|
||||
? await firstVideoAdapter.updateMeeting(bookingRef, calEvent).catch((e) => {
|
||||
log.error("updateMeeting failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
const organizerMail = new EventOrganizerRescheduledMail(calEvent);
|
||||
await organizerMail.sendEmail();
|
||||
} catch (e) {
|
||||
console.error("organizerMail.sendEmail failed", e);
|
||||
}
|
||||
|
||||
if (!updatedMeeting.disableConfirmationEmail) {
|
||||
try {
|
||||
const attendeeMail = new EventAttendeeRescheduledMail(calEvent);
|
||||
await attendeeMail.sendEmail();
|
||||
} catch (e) {
|
||||
console.error("attendeeMail.sendEmail failed", e);
|
||||
}
|
||||
if (!updatedMeeting) {
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -4,8 +4,8 @@ import timezone from "dayjs/plugin/timezone";
|
|||
import utc from "dayjs/plugin/utc";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import sendEmail from "@lib/emails/sendMail";
|
||||
import { buildForgotPasswordMessage } from "@lib/forgot-password/messaging/forgot-password";
|
||||
import { sendPasswordResetEmail } from "@lib/emails/email-manager";
|
||||
import { PasswordReset, PASSWORD_RESET_EXPIRY_HOURS } from "@lib/emails/templates/forgot-password-email";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
@ -51,7 +51,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
if (maybePreviousRequest && maybePreviousRequest?.length >= 1) {
|
||||
passwordRequest = maybePreviousRequest[0];
|
||||
} else {
|
||||
const expiry = dayjs().add(6, "hours").toDate();
|
||||
const expiry = dayjs().add(PASSWORD_RESET_EXPIRY_HOURS, "hours").toDate();
|
||||
const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({
|
||||
data: {
|
||||
email: rawEmail,
|
||||
|
@ -61,20 +61,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
passwordRequest = createdResetPasswordRequest;
|
||||
}
|
||||
|
||||
const passwordResetLink = `${process.env.BASE_URL}/auth/forgot-password/${passwordRequest.id}`;
|
||||
const { subject, message } = buildForgotPasswordMessage({
|
||||
const passwordEmail: PasswordReset = {
|
||||
language: t,
|
||||
user: {
|
||||
name: maybeUser.name,
|
||||
email: rawEmail,
|
||||
},
|
||||
link: passwordResetLink,
|
||||
});
|
||||
resetLink: `${process.env.BASE_URL}/auth/forgot-password/${passwordRequest.id}`,
|
||||
};
|
||||
|
||||
await sendEmail({
|
||||
to: rawEmail,
|
||||
subject: subject,
|
||||
text: message,
|
||||
});
|
||||
await sendPasswordResetEmail(passwordEmail);
|
||||
|
||||
return res.status(201).json({ message: "Reset Requested" });
|
||||
} catch (reason) {
|
||||
|
|
|
@ -4,9 +4,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { refund } from "@ee/lib/stripe/server";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import EventRejectionMail from "@lib/emails/EventRejectionMail";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/calendarClient";
|
||||
import { sendDeclinedEmails } from "@lib/emails/email-manager";
|
||||
import { sendScheduledEmails } from "@lib/emails/email-manager";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
import { BookingConfirmBody } from "@lib/types/booking";
|
||||
|
||||
|
@ -38,6 +40,8 @@ const authorized = async (
|
|||
return false;
|
||||
};
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
|
||||
const t = await getTranslation(req.body.language ?? "en", "common");
|
||||
|
||||
|
@ -63,6 +67,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -124,6 +129,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const eventManager = new EventManager(currentUser.credentials);
|
||||
const scheduleResult = await eventManager.create(evt);
|
||||
|
||||
const results = scheduleResult.results;
|
||||
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
const error = {
|
||||
errorCode: "BookingCreatingMeetingFailed",
|
||||
message: "Booking failed",
|
||||
};
|
||||
|
||||
log.error(`Booking ${currentUser.username} failed`, error, results);
|
||||
} else {
|
||||
const metadata: AdditionInformation = {};
|
||||
|
||||
if (results.length) {
|
||||
// TODO: Handle created event metadata more elegantly
|
||||
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
||||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||
}
|
||||
await sendScheduledEmails({ ...evt, additionInformation: metadata });
|
||||
}
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: bookingId,
|
||||
|
@ -149,8 +175,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
status: BookingStatus.REJECTED,
|
||||
},
|
||||
});
|
||||
const attendeeMail = new EventRejectionMail(evt);
|
||||
await attendeeMail.sendEmail();
|
||||
|
||||
await sendDeclinedEmails(evt);
|
||||
|
||||
res.status(204).end();
|
||||
}
|
||||
|
|
|
@ -11,11 +11,16 @@ import { v5 as uuidv5 } from "uuid";
|
|||
|
||||
import { handlePayment } from "@ee/lib/stripe/server";
|
||||
|
||||
import { CalendarEvent, getBusyCalendarTimes } from "@lib/calendarClient";
|
||||
import EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail";
|
||||
import { CalendarEvent, AdditionInformation, getBusyCalendarTimes } from "@lib/calendarClient";
|
||||
import {
|
||||
sendScheduledEmails,
|
||||
sendRescheduledEmails,
|
||||
sendOrganizerRequestEmail,
|
||||
} from "@lib/emails/email-manager";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getEventName } from "@lib/event";
|
||||
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
|
||||
import { BufferedBusyTime } from "@lib/integrations/Office365Calendar/Office365CalendarApiAdapter";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
import { BookingCreateBody } from "@lib/types/booking";
|
||||
|
@ -25,13 +30,6 @@ import getSubscribers from "@lib/webhooks/subscriptions";
|
|||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
||||
export interface DailyReturnType {
|
||||
name: string;
|
||||
url: string;
|
||||
id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
dayjs.extend(dayjsBusinessTime);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(isBetween);
|
||||
|
@ -40,7 +38,7 @@ dayjs.extend(timezone);
|
|||
const translator = short();
|
||||
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
||||
|
||||
type BufferedBusyTimes = { start: string; end: string }[];
|
||||
type BufferedBusyTimes = BufferedBusyTime[];
|
||||
|
||||
/**
|
||||
* Refreshes a Credential with fresh data from the database.
|
||||
|
@ -285,7 +283,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
attendees: attendeesList,
|
||||
location: reqBody.location, // Will be processed by the EventManager later.
|
||||
language: t,
|
||||
uid,
|
||||
};
|
||||
|
||||
if (eventType.schedulingType === SchedulingType.COLLECTIVE) {
|
||||
|
@ -439,10 +436,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
if (rescheduleUid) {
|
||||
// Use EventManager to conditionally use all needed integrations.
|
||||
const updateResults = await eventManager.update(evt, rescheduleUid);
|
||||
const updateManager = await eventManager.update(evt, rescheduleUid);
|
||||
|
||||
results = updateResults.results;
|
||||
referencesToCreate = updateResults.referencesToCreate;
|
||||
results = updateManager.results;
|
||||
referencesToCreate = updateManager.referencesToCreate;
|
||||
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
const error = {
|
||||
|
@ -451,15 +448,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
};
|
||||
|
||||
log.error(`Booking ${user.name} failed`, error, results);
|
||||
} else {
|
||||
const metadata: AdditionInformation = {};
|
||||
|
||||
if (results.length) {
|
||||
// TODO: Handle created event metadata more elegantly
|
||||
metadata.hangoutLink = results[0].updatedEvent?.hangoutLink;
|
||||
metadata.conferenceData = results[0].updatedEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].updatedEvent?.entryPoints;
|
||||
}
|
||||
|
||||
await sendRescheduledEmails({ ...evt, additionInformation: metadata });
|
||||
}
|
||||
// If it's not a reschedule, doesn't require confirmation and there's no price,
|
||||
// Create a booking
|
||||
} else if (!eventType.requiresConfirmation && !eventType.price) {
|
||||
// Use EventManager to conditionally use all needed integrations.
|
||||
const createResults = await eventManager.create(evt);
|
||||
const createManager = await eventManager.create(evt);
|
||||
|
||||
results = createResults.results;
|
||||
referencesToCreate = createResults.referencesToCreate;
|
||||
results = createManager.results;
|
||||
referencesToCreate = createManager.referencesToCreate;
|
||||
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
const error = {
|
||||
|
@ -468,11 +476,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
};
|
||||
|
||||
log.error(`Booking ${user.username} failed`, error, results);
|
||||
} else {
|
||||
const metadata: AdditionInformation = {};
|
||||
|
||||
if (results.length) {
|
||||
// TODO: Handle created event metadata more elegantly
|
||||
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
||||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||
}
|
||||
await sendScheduledEmails({ ...evt, additionInformation: metadata });
|
||||
}
|
||||
}
|
||||
|
||||
if (eventType.requiresConfirmation && !rescheduleUid) {
|
||||
await new EventOrganizerRequestMail(evt).sendEmail();
|
||||
await sendOrganizerRequestEmail(evt);
|
||||
}
|
||||
|
||||
if (typeof eventType.price === "number" && eventType.price > 0) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { refund } from "@ee/lib/stripe/server";
|
|||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { CalendarEvent, deleteEvent } from "@lib/calendarClient";
|
||||
import { sendCancelledEmails } from "@lib/emails/email-manager";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import prisma from "@lib/prisma";
|
||||
import { deleteMeeting } from "@lib/videoClient";
|
||||
|
@ -101,6 +102,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return retObj;
|
||||
}),
|
||||
uid: bookingToDelete?.uid,
|
||||
location: bookingToDelete?.location,
|
||||
language: t,
|
||||
};
|
||||
|
||||
|
@ -189,7 +191,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
await Promise.all([apiDeletes, attendeeDeletes, bookingReferenceDeletes]);
|
||||
|
||||
//TODO Perhaps send emails to user and client to tell about the cancellation
|
||||
await sendCancelledEmails(evt);
|
||||
|
||||
res.status(204).end();
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import dayjs from "dayjs";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import EventOrganizerRequestReminderMail from "@lib/emails/EventOrganizerRequestReminderMail";
|
||||
import { sendOrganizerRequestReminderEmail } from "@lib/emails/email-manager";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
@ -90,7 +90,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
language: t,
|
||||
};
|
||||
|
||||
await new EventOrganizerRequestReminderMail(evt).sendEmail();
|
||||
await sendOrganizerRequestReminderEmail(evt);
|
||||
|
||||
await prisma.reminderMail.create({
|
||||
data: {
|
||||
referenceId: booking.id,
|
||||
|
|
|
@ -2,7 +2,9 @@ import { randomBytes } from "crypto";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { createInvitationEmail } from "@lib/emails/invitation";
|
||||
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 { getTranslation } from "@server/lib/i18n";
|
||||
|
@ -72,13 +74,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
});
|
||||
|
||||
if (session?.user?.name && team?.name) {
|
||||
createInvitationEmail({
|
||||
const teamInviteEvent: TeamInvite = {
|
||||
language: t,
|
||||
toEmail: req.body.usernameOrEmail,
|
||||
from: session.user.name,
|
||||
to: req.body.usernameOrEmail,
|
||||
teamName: team.name,
|
||||
token,
|
||||
});
|
||||
joinLink: `${BASE_URL}/auth/signup?token=${token}&callbackUrl=${BASE_URL + "/settings/teams"}`,
|
||||
};
|
||||
|
||||
await sendTeamInviteEmail(teamInviteEvent);
|
||||
}
|
||||
|
||||
return res.status(201).json({});
|
||||
|
@ -106,12 +110,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
// inform user of membership by email
|
||||
if (req.body.sendEmailInvitation && session?.user?.name && team?.name) {
|
||||
createInvitationEmail({
|
||||
const teamInviteEvent: TeamInvite = {
|
||||
language: t,
|
||||
toEmail: invitee.email,
|
||||
from: session.user.name,
|
||||
to: req.body.usernameOrEmail,
|
||||
teamName: team.name,
|
||||
});
|
||||
joinLink: BASE_URL + "/settings/teams",
|
||||
};
|
||||
|
||||
await sendTeamInviteEmail(teamInviteEvent);
|
||||
}
|
||||
|
||||
res.status(201).json({});
|
||||
|
|
BIN
public/emails/calendarCircle@2x.png
Executable file
BIN
public/emails/calendarCircle@2x.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
public/emails/checkCircle@2x.png
Executable file
BIN
public/emails/checkCircle@2x.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
public/emails/xCircle@2x.png
Normal file
BIN
public/emails/xCircle@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
|
@ -1,12 +1,24 @@
|
|||
{
|
||||
"accept_invitation": "Accept Invitation",
|
||||
"calcom_explained": "Cal.com is the open source Calendly alternative putting you in control of your own data, workflow and appearance.",
|
||||
"have_any_questions": "Have questions? We're here to help.",
|
||||
"reset_password_subject": "Cal.com: Reset password instructions",
|
||||
"event_declined_subject": "Declined: {{eventType}} with {{name}} at {{date}}",
|
||||
"event_cancelled_subject": "Cancelled: {{eventType}} with {{name}} at {{date}}",
|
||||
"event_request_declined": "Your event request has been declined",
|
||||
"event_request_cancelled": "Your scheduled event was cancelled",
|
||||
"organizer": "Organizer",
|
||||
"need_to_reschedule_or_cancel": "Need to reschedule or cancel?",
|
||||
"manage_this_event": "Manage this event",
|
||||
"your_event_has_been_scheduled": "Your event has been scheduled",
|
||||
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
|
||||
"remove_banner_instructions": "To remove this banner, please open your .env file and change the <1>NEXT_PUBLIC_LICENSE_CONSENT</1> variable to '{{agree}}'.",
|
||||
"error_message": "The error message was: '{{errorMessage}}'",
|
||||
"refund_failed_subject": "Refund failed: {{userName}} - {{date}} - {{eventType}}",
|
||||
"refund_failed_subject": "Refund failed: {{name}} - {{date}} - {{eventType}}",
|
||||
"refund_failed": "The refund for the event {{eventType}} with {{userName}} on {{date}} failed.",
|
||||
"check_with_provider_and_user": "Please check with your payment provider and {{userName}} how to handle this.",
|
||||
"check_with_provider_and_user": "Please check with your payment provider and {{user}} how to handle this.",
|
||||
"a_refund_failed": "A refund failed",
|
||||
"awaiting_payment": "Awaiting Payment: {{eventType}} with {{organizerName}} on {{date}}",
|
||||
"awaiting_payment_subject": "Awaiting Payment: {{eventType}} with {{name}} on {{date}}",
|
||||
"meeting_awaiting_payment": "Your meeting is awaiting payment",
|
||||
"help": "Help",
|
||||
"price": "Price",
|
||||
|
@ -28,16 +40,21 @@
|
|||
"no_more_results": "No more results",
|
||||
"load_more_results": "Load more results",
|
||||
"integration_meeting_id": "{{integrationName}} meeting ID: {{meetingId}}",
|
||||
"confirmed_event_type_subject": "Confirmed: {{eventType}} with {{name}} on {{date}}",
|
||||
"confirmed_event_type_subject": "Confirmed: {{eventType}} with {{name}} at {{date}}",
|
||||
"new_event_request": "New event request: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"confirm_or_reject_booking": "Confirm or reject the booking",
|
||||
"confirm_or_reject_request": "Confirm or reject the request",
|
||||
"check_bookings_page_to_confirm_or_reject": "Check your bookings page to confirm or reject the booking.",
|
||||
"event_awaiting_approval": "A new event is waiting for your approval",
|
||||
"event_awaiting_approval": "An event is waiting for your approval",
|
||||
"someone_requested_an_event": "Someone has requested to schedule an event on your calendar.",
|
||||
"someone_requested_password_reset": "Someone has requested a link to change your password.",
|
||||
"password_reset_instructions": "If you didn't request this, you can safely ignore this email and your password will not be changed.",
|
||||
"event_awaiting_approval_subject": "Awaiting Approval: {{eventType}} with {{name}} at {{date}}",
|
||||
"event_still_awaiting_approval": "An event is still waiting for your approval",
|
||||
"your_meeting_has_been_booked": "Your meeting has been booked",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Your {{eventType}} with {{name}} has been rescheduled to {{time}} ({{timeZone}}) on {{date}}.",
|
||||
"event_has_been_rescheduled": "Your event has been rescheduled.",
|
||||
"hi_user_name": "Hi {{userName}}",
|
||||
"organizer_ics_event_title": "{{eventType}} with {{attendeeName}}",
|
||||
"event_has_been_rescheduled": "Updated - Your event has been rescheduled",
|
||||
"hi_user_name": "Hi {{name}}",
|
||||
"ics_event_title": "{{eventType}} with {{name}}",
|
||||
"new_event_subject": "New event: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Join by {{entryPoint}}",
|
||||
"notes": "Notes",
|
||||
|
@ -53,14 +70,13 @@
|
|||
"meeting_password": "Meeting Password",
|
||||
"meeting_url": "Meeting URL",
|
||||
"meeting_request_rejected": "Your meeting request has been rejected",
|
||||
"rescheduled_event_type_with_organizer": "Rescheduled: {{eventType}} with {{organizerName}} on {{date}}",
|
||||
"rescheduled_event_type_with_attendee": "Rescheduled event: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"rescheduled_event_type_subject": "Rescheduled: {{eventType}} with {{name}} at {{date}}",
|
||||
"rejected_event_type_with_organizer": "Rejected: {{eventType}} with {{organizer}} on {{date}}",
|
||||
"hi": "Hi",
|
||||
"join_team": "Join team",
|
||||
"request_another_invitation_email": "If you prefer not to use {{toEmail}} as your Cal.com email or already have a Cal.com account, please request another invitation to that email.",
|
||||
"you_have_been_invited": "You have been invited to join the team {{teamName}}",
|
||||
"user_invited_you": "{{user}} invited you to join the team {{teamName}}",
|
||||
"user_invited_you": "{{user}} invited you to join the team {{team}} on Cal.com",
|
||||
"link_expires": "p.s. It expires in {{expiresIn}} hours.",
|
||||
"use_link_to_reset_password": "Use the link below to reset your password",
|
||||
"hey_there": "Hey there,",
|
||||
|
@ -113,6 +129,7 @@
|
|||
"rejected": "Rejected",
|
||||
"unconfirmed": "Unconfirmed",
|
||||
"guests": "Guests",
|
||||
"guest": "Guest",
|
||||
"web_conferencing_details_to_follow": "Web conferencing details to follow.",
|
||||
"the_username": "The username",
|
||||
"username": "Username",
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { expect, it } from "@jest/globals";
|
||||
|
||||
import { html, text, Invitation } from "@lib/emails/invitation";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
||||
it("email text rendering should strip tags and add new lines", () => {
|
||||
const result = text("<p>hello world</p><br /><div>welcome to the brave <span>new</span> world");
|
||||
expect(result).toEqual("hello world\nwelcome to the brave new world");
|
||||
});
|
||||
|
||||
it("email html should render invite email", async () => {
|
||||
const t = await getTranslation("en", "common");
|
||||
const invitation = {
|
||||
language: t,
|
||||
from: "Huxley",
|
||||
toEmail: "hello@example.com",
|
||||
teamName: "Calendar Lovers",
|
||||
token: "invite-token",
|
||||
} as Invitation;
|
||||
const result = html(invitation);
|
||||
expect(result).toContain(
|
||||
`<br />${t("user_invited_you", { user: invitation.from, teamName: invitation.teamName })}<br />`
|
||||
);
|
||||
expect(result).toContain("/auth/signup?token=invite-token&");
|
||||
expect(result).toContain(`${t("request_another_invitation_email", { toEmail: invitation.toEmail })}`);
|
||||
});
|
Loading…
Reference in a new issue