Prepares the email system for Calendso Teams (#586)

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
This commit is contained in:
Alex van Andel 2021-09-06 10:06:33 +01:00 committed by GitHub
parent 19f5b9d6c6
commit d9aff72220
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 41 additions and 61 deletions

View file

@ -1,5 +1,4 @@
import EventOrganizerMail from "./emails/EventOrganizerMail";
import EventAttendeeMail from "./emails/EventAttendeeMail";
import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail";
import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail";
import prisma from "./prisma";
@ -120,6 +119,10 @@ export interface CalendarEvent {
startTime: string;
endTime: string;
description?: string;
team?: {
name: string;
members: string[];
};
location?: string;
organizer: Person;
attendees: Person[];
@ -574,25 +577,11 @@ const createEvent = async (
entryPoints: maybeEntryPoints,
});
const attendeeMail = new EventAttendeeMail(calEvent, uid, {
hangoutLink: maybeHangoutLink,
conferenceData: maybeConferenceData,
entryPoints: maybeEntryPoints,
});
try {
await organizerMail.sendEmail();
} catch (e) {
console.error("organizerMail.sendEmail failed", e);
}
if (!creationResult || !creationResult.disableConfirmationEmail) {
try {
await attendeeMail.sendEmail();
} catch (e) {
console.error("attendeeMail.sendEmail failed", e);
}
}
}
return {

View file

@ -65,7 +65,13 @@ export default class EventAttendeeMail extends EventMail {
</tr>
<tr>
<td>Who</td>
<td>${this.calEvent.organizer.name}<br /><small>${this.calEvent.organizer.email}</small></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>
<tr>
<td>Where</td>
@ -122,6 +128,10 @@ export default class EventAttendeeMail extends EventMail {
return ``;
}
protected getAdditionalFooter(): string {
return this.parser.getChangeEventFooterHtml();
}
/**
* Returns the payload object for the nodemailer.
*
@ -133,7 +143,7 @@ export default class EventAttendeeMail extends EventMail {
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `Confirmed: ${this.calEvent.type} with ${
this.calEvent.organizer.name
this.calEvent.team?.name || this.calEvent.organizer.name
} on ${this.getInviteeStart().format("LT dddd, LL")}`,
html: this.getHtmlRepresentation(),
text: this.getPlainTextRepresentation(),

View file

@ -13,8 +13,8 @@ export default class EventAttendeeRescheduledMail extends EventAttendeeMail {
Hi ${this.calEvent.attendees[0].name},<br />
<br />
Your ${this.calEvent.type} with ${
this.calEvent.organizer.name
} has been rescheduled to ${this.getInviteeStart().format("h:mma")}
this.calEvent.team?.name || this.calEvent.organizer.name
} has been rescheduled to ${this.getInviteeStart().format("h:mma")}
(${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format("dddd, LL")}.<br />
` +
this.getAdditionalFooter() +

View file

@ -69,7 +69,7 @@ export default abstract class EventMail {
/**
* Sends the email to the event attendant and returns a Promise.
*/
public sendEmail(): Promise<any> {
public sendEmail() {
new Promise((resolve, reject) =>
nodemailer
.createTransport(this.getMailerOptions().transport)
@ -90,7 +90,7 @@ export default abstract class EventMail {
*
* @protected
*/
protected getMailerOptions(): any {
protected getMailerOptions() {
return {
transport: serverConfig.transport,
from: serverConfig.from,
@ -135,12 +135,4 @@ export default abstract class EventMail {
protected getCancelLink(): string {
return this.parser.getCancelLink();
}
/**
* Defines a footer that will be appended to the email.
* @protected
*/
protected getAdditionalFooter(): string {
return this.parser.getChangeEventFooterHtml();
}
}

View file

@ -50,8 +50,10 @@ export default class EventOrganizerMail extends EventMail {
return "A new event has been scheduled.";
}
protected getBodyText(): string {
return "You and any other attendees have been emailed with this information.";
protected getAdditionalFooter(): string {
return `<p style="color: #4b5563; margin-top: 20px;">Need to make a change? <a href=${
process.env.BASE_URL + "/bookings"
} style="color: #161e2e;">Manage my bookings</a></p>`;
}
protected getImage(): string {
@ -94,7 +96,6 @@ export default class EventOrganizerMail extends EventMail {
>
${this.getImage()}
<h1 style="font-weight: 500; color: #161e2e;">${this.getBodyHeader()}</h1>
<p style="color: #4b5563; margin-bottom: 30px;">${this.getBodyText()}</p>
<hr />
<table style="border-spacing: 20px; color: #161e2e; margin-bottom: 10px;">
<colgroup>

View file

@ -6,6 +6,7 @@ import prisma from "@lib/prisma";
import { LocationType } from "@lib/location";
import { v5 as uuidv5 } from "uuid";
import merge from "lodash.merge";
import EventAttendeeMail from "@lib/emails/EventAttendeeMail";
export interface EventResult {
type: string;
@ -65,10 +66,25 @@ export default class EventManager {
// 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(event, isDedicated, maybeUid);
// If and only if event type is a dedicated meeting, create a dedicated video meeting as well.
if (isDedicated) {
results.push(await this.createVideoEvent(event, maybeUid));
} else {
if (!results.length || !results.some((eRes) => eRes.createdEvent.disableConfirmationEmail)) {
const metadata: { hangoutLink?: string; conferenceData?: unknown; entryPoints?: unknown } = {};
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;
}
const attendeeMail = new EventAttendeeMail(event, maybeUid, metadata);
try {
await attendeeMail.sendEmail();
} catch (e) {
console.error("attendeeMail.sendEmail failed", e);
}
}
}
const referencesToCreate: Array<PartialReference> = results.map((result) => {

View file

@ -5,7 +5,6 @@ import { CalendarEvent, getBusyCalendarTimes } from "@lib/calendarClient";
import { v5 as uuidv5 } from "uuid";
import short from "short-uuid";
import { getBusyVideoTimes } from "@lib/videoClient";
import EventAttendeeMail from "../../../lib/emails/EventAttendeeMail";
import { getEventName } from "@lib/event";
import dayjs from "dayjs";
import logger from "../../../lib/logger";
@ -15,7 +14,6 @@ import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import isBetween from "dayjs/plugin/isBetween";
import dayjsBusinessDays from "dayjs-business-days";
import { Exception } from "handlebars";
import EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail";
dayjs.extend(dayjsBusinessDays);
@ -81,25 +79,6 @@ function isOutOfBounds(
}
}
export async function handleLegacyConfirmationMail(
results: Array<EventResult>,
eventType: EventType,
evt: CalendarEvent,
hashUID: string
): Promise<{ error: Exception; message: string | null }> {
if (results.length === 0 && !eventType.requiresConfirmation) {
// Legacy as well, as soon as we have a separate email integration class. Just used
// to send an email even if there is no integration at all.
try {
const mail = new EventAttendeeMail(evt, hashUID);
await mail.sendEmail();
} catch (e) {
return { error: e, message: "Booking failed" };
}
}
return null;
}
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const { user } = req.query;
log.debug(`Booking ${user} started`);
@ -311,13 +290,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
results.length > 0 ? results[0].uid : translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
// TODO Should just be set to the true case as soon as we have a "bare email" integration class.
// UID generation should happen in the integration itself, not here.
const legacyMailError = await handleLegacyConfirmationMail(results, eventType, evt, hashUID);
if (legacyMailError) {
log.error("Sending legacy event mail failed", legacyMailError.error);
log.error(`Booking ${user} failed`);
res.status(500).json({ message: legacyMailError.message });
return;
}
try {
await prisma.booking.create({