rallly/packages/emails/src/send-email.tsx
2024-01-29 15:57:21 +07:00

186 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as aws from "@aws-sdk/client-ses";
import { defaultProvider } from "@aws-sdk/credential-provider-node";
import { renderAsync } from "@react-email/render";
import { createTransport, Transporter } from "nodemailer";
import type Mail from "nodemailer/lib/mailer";
import previewEmail from "preview-email";
import React from "react";
import * as templates from "./templates";
import { EmailContext } from "./templates/_components/email-context";
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];
type SendEmailOptions<T extends TemplateName> = {
to: string;
subject: string;
props: TemplateProps<T>;
attachments?: Mail.Options["attachments"];
};
type EmailProviderConfig =
| {
name: "ses";
// config defined through env vars
}
| {
name: "smtp";
// config defined through env vars
};
export type SupportedEmailProviders = EmailProviderConfig["name"];
type EmailClientConfig = {
/**
* Whether to open previews of each email in the browser
*/
openPreviews?: boolean;
/**
* Whether to send emails to the test server
*/
useTestServer: boolean;
/**
* Email provider config
*/
provider: EmailProviderConfig;
/**
* Mail config
*/
mail: {
from: {
name: string;
address: string;
};
};
/**
* Context to pass to each email
*/
context: EmailContext;
};
export class EmailClient {
private config: EmailClientConfig;
private cachedTransport?: Transporter;
constructor(config: EmailClientConfig) {
this.config = config;
}
async sendTemplate<T extends TemplateName>(
templateName: T,
options: SendEmailOptions<T>,
) {
const Template = templates[templateName] as TemplateComponent<T>;
const component = (
<Template
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(options.props as any)}
ctx={this.config.context}
/>
);
const [html, text] = await Promise.all([
renderAsync(component),
renderAsync(component, { plainText: true }),
]);
try {
await this.sendEmail({
from: this.config.mail.from,
to: options.to,
subject: options.subject,
html,
text,
attachments: options.attachments,
});
} catch (e) {
console.error("Error sending email", templateName, e);
}
}
async sendEmail(options: Mail.Options) {
if (this.config.openPreviews) {
previewEmail(options, {
openSimulator: false,
});
}
if (!process.env["SUPPORT_EMAIL"]) {
console.info(" SUPPORT_EMAIL not configured - skipping email send");
return;
}
try {
await this.transport.sendMail(options);
return;
} catch (e) {
console.error("Error sending email", e);
}
}
private get transport() {
if (this.cachedTransport) {
// Reuse the transport if it exists
return this.cachedTransport;
}
if (this.config.useTestServer) {
this.cachedTransport = createTransport({
port: 4025,
});
return this.cachedTransport;
}
switch (this.config.provider.name) {
case "ses": {
const ses = new aws.SES({
region: process.env["AWS" + "_REGION"] as string,
credentialDefaultProvider: defaultProvider,
});
this.cachedTransport = createTransport({
SES: {
ses,
aws,
sendingRate: 10,
},
});
break;
}
case "smtp": {
const hasAuth = process.env["SMTP_USER"] || process.env["SMTP_PWD"];
this.cachedTransport = createTransport({
host: process.env["SMTP_HOST"],
port: process.env["SMTP_PORT"]
? parseInt(process.env["SMTP_PORT"])
: undefined,
secure: process.env["SMTP_SECURE"] === "true",
auth: hasAuth
? {
user: process.env["SMTP_USER"],
pass: process.env["SMTP_PWD"],
}
: undefined,
tls:
process.env["SMTP_TLS_ENABLED"] === "true"
? {
ciphers: "SSLv3",
rejectUnauthorized: false,
}
: undefined,
});
break;
}
}
return this.cachedTransport;
}
}