Use message queue for emails (#1446)

This commit is contained in:
Luke Vella 2024-11-30 19:00:56 +00:00 committed by GitHub
parent 673fc79801
commit a452e5b764
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 150 additions and 35 deletions

View file

@ -1,5 +1,5 @@
import { previewEmailContext } from "../components/email-context";
import NewParticipantConfirmationEmail from "../templates/new-participant-confirmation";
import { NewParticipantConfirmationEmail } from "../templates/new-participant-confirmation";
export default function NewParticipantConfirmationPreview() {
return (

View file

@ -0,0 +1,9 @@
import { Client } from "@upstash/qstash";
export function createQstashClient() {
if (!process.env.QSTASH_TOKEN) {
return null;
}
return new Client({ token: process.env.QSTASH_TOKEN! });
}

View file

@ -1,6 +1,8 @@
import * as aws from "@aws-sdk/client-ses";
import { defaultProvider } from "@aws-sdk/credential-provider-node";
import { absoluteUrl } from "@rallly/utils/absolute-url";
import { renderAsync } from "@react-email/render";
import * as Sentry from "@sentry/nextjs";
import { waitUntil } from "@vercel/functions";
import type { Transporter } from "nodemailer";
import { createTransport } from "nodemailer";
@ -8,20 +10,9 @@ import type Mail from "nodemailer/lib/mailer";
import React from "react";
import { i18nDefaultConfig, i18nInstance } from "./i18n";
import * as templates from "./templates";
import type { EmailContext } from "./types";
type Templates = typeof templates;
type TemplateName = keyof typeof templates;
type TemplateProps<T extends TemplateName> = Omit<
React.ComponentProps<TemplateComponent<T>>,
"ctx"
>;
type TemplateComponent<T extends TemplateName> = Templates[T] & {
getSubject?: (props: TemplateProps<T>, ctx: EmailContext) => string;
};
import { createQstashClient } from "./queue";
import { templates } from "./templates";
import type { TemplateComponent, TemplateName, TemplateProps } from "./types";
type SendEmailOptions<T extends TemplateName> = {
to: string;
@ -81,11 +72,30 @@ export class EmailClient {
templateName: T,
options: SendEmailOptions<T>,
) {
return waitUntil(
(async () => {
const createEmailJob = async () => {
const client = createQstashClient();
if (client) {
const queue = client.queue({
queueName: "emails",
});
queue
.enqueueJSON({
url: absoluteUrl("/api/send-email"),
body: { templateName, options },
})
.catch(() => {
Sentry.captureException(new Error("Failed to queue email"));
// If there's an error queuing the email, send it immediately
this.sendTemplate(templateName, options);
});
} else {
this.sendTemplate(templateName, options);
})(),
);
}
};
waitUntil(createEmailJob());
}
async sendTemplate<T extends TemplateName>(

View file

@ -1,8 +1,26 @@
export * from "./templates/finalized-host";
export * from "./templates/finalized-participant";
export * from "./templates/login";
export * from "./templates/new-comment";
export * from "./templates/new-participant";
export * from "./templates/new-participant-confirmation";
export * from "./templates/new-poll";
export * from "./templates/register";
import { FinalizeHostEmail } from "./templates/finalized-host";
import { FinalizeParticipantEmail } from "./templates/finalized-participant";
import { LoginEmail } from "./templates/login";
import { NewCommentEmail } from "./templates/new-comment";
import { NewParticipantEmail } from "./templates/new-participant";
import { NewParticipantConfirmationEmail } from "./templates/new-participant-confirmation";
import { NewPollEmail } from "./templates/new-poll";
import { RegisterEmail } from "./templates/register";
import type { TemplateName } from "./types";
const templates = {
FinalizeHostEmail,
FinalizeParticipantEmail,
LoginEmail,
NewCommentEmail,
NewParticipantEmail,
NewParticipantConfirmationEmail,
NewPollEmail,
RegisterEmail,
};
export const emailTemplates = Object.keys(templates) as TemplateName[];
export type EmailTemplates = typeof templates;
export { templates };

View file

@ -15,7 +15,8 @@ interface NewParticipantConfirmationEmailProps {
editSubmissionUrl: string;
ctx: EmailContext;
}
export const NewParticipantConfirmationEmail = ({
const NewParticipantConfirmationEmail = ({
title,
editSubmissionUrl,
ctx,
@ -96,4 +97,4 @@ NewParticipantConfirmationEmail.getSubject = (
});
};
export default NewParticipantConfirmationEmail;
export { NewParticipantConfirmationEmail };

View file

@ -1,6 +1,7 @@
import type { TFunction } from "i18next";
import type { I18nInstance } from "./i18n";
import type { EmailTemplates } from "./templates";
export type EmailContext = {
logoUrl: string;
@ -10,3 +11,14 @@ export type EmailContext = {
i18n: I18nInstance;
t: TFunction;
};
export type TemplateName = keyof EmailTemplates;
export type TemplateProps<T extends TemplateName> = Omit<
React.ComponentProps<EmailTemplates[T]>,
"ctx"
>;
export type TemplateComponent<T extends TemplateName> = EmailTemplates[T] & {
getSubject?: (props: TemplateProps<T>, ctx: EmailContext) => string;
};