234 lines
6.2 KiB
TypeScript
234 lines
6.2 KiB
TypeScript
import addressparser from "addressparser";
|
||
import invariant from "invariant";
|
||
import nodemailer, { Transporter } from "nodemailer";
|
||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||
import Oy from "oy-vey";
|
||
import env from "@server/env";
|
||
import Logger from "@server/logging/Logger";
|
||
import { trace } from "@server/logging/tracing";
|
||
import { baseStyles } from "./templates/components/EmailLayout";
|
||
|
||
const useTestEmailService = env.isDevelopment && !env.SMTP_USERNAME;
|
||
|
||
type SendMailOptions = {
|
||
to: string;
|
||
fromName?: string;
|
||
replyTo?: string;
|
||
subject: string;
|
||
previewText?: string;
|
||
text: string;
|
||
component: JSX.Element;
|
||
headCSS?: string;
|
||
unsubscribeUrl?: string;
|
||
};
|
||
|
||
/**
|
||
* Mailer class to send emails.
|
||
*/
|
||
@trace({
|
||
serviceName: "mailer",
|
||
})
|
||
export class Mailer {
|
||
transporter: Transporter | undefined;
|
||
|
||
constructor() {
|
||
if (env.SMTP_HOST) {
|
||
this.transporter = nodemailer.createTransport(this.getOptions());
|
||
}
|
||
if (useTestEmailService) {
|
||
Logger.info(
|
||
"email",
|
||
"SMTP_USERNAME not provided, generating test account…"
|
||
);
|
||
|
||
void this.getTestTransportOptions().then((options) => {
|
||
if (!options) {
|
||
Logger.info(
|
||
"email",
|
||
"Couldn't generate a test account with ethereal.email at this time – emails will not be sent."
|
||
);
|
||
return;
|
||
}
|
||
|
||
this.transporter = nodemailer.createTransport(options);
|
||
});
|
||
}
|
||
}
|
||
|
||
template = ({
|
||
title,
|
||
bodyContent,
|
||
headCSS = "",
|
||
bgColor = "#FFFFFF",
|
||
lang,
|
||
dir = "ltr" /* https://www.w3.org/TR/html4/struct/dirlang.html#blocklevel-bidi */,
|
||
}: Oy.CustomTemplateRenderOptions) => {
|
||
if (!title) {
|
||
throw new Error("`title` is a required option for `renderTemplate`");
|
||
} else if (!bodyContent) {
|
||
throw new Error(
|
||
"`bodyContent` is a required option for `renderTemplate`"
|
||
);
|
||
}
|
||
|
||
// the template below is a slightly modified form of https://github.com/revivek/oy/blob/master/src/utils/HTML4.js
|
||
return `
|
||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||
<html
|
||
${lang ? 'lang="' + lang + '"' : ""}
|
||
dir="${dir}"
|
||
xmlns="http://www.w3.org/1999/xhtml"
|
||
xmlns:v="urn:schemas-microsoft-com:vml"
|
||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||
<meta name="viewport" content="width=device-width"/>
|
||
|
||
<title>${title}</title>
|
||
|
||
<style type="text/css">
|
||
${headCSS}
|
||
|
||
#__bodyTable__ {
|
||
margin: 0;
|
||
padding: 0;
|
||
width: 100% !important;
|
||
}
|
||
</style>
|
||
|
||
<!--[if gte mso 9]>
|
||
<xml>
|
||
<o:OfficeDocumentSettings>
|
||
<o:AllowPNG/>
|
||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||
</o:OfficeDocumentSettings>
|
||
</xml>
|
||
<![endif]-->
|
||
</head>
|
||
<body bgcolor="${bgColor}" width="100%" style="-webkit-font-smoothing: antialiased; width:100% !important; background:${bgColor};-webkit-text-size-adjust:none; margin:0; padding:0; min-width:100%; direction: ${dir};">
|
||
${bodyContent}
|
||
</body>
|
||
</html>
|
||
`;
|
||
};
|
||
|
||
sendMail = async (data: SendMailOptions): Promise<void> => {
|
||
const { transporter } = this;
|
||
|
||
if (!transporter) {
|
||
Logger.info(
|
||
"email",
|
||
`Attempted to send email "${data.subject}" to ${data.to} but no transport configured.`
|
||
);
|
||
return;
|
||
}
|
||
|
||
const html = Oy.renderTemplate(
|
||
data.component,
|
||
{
|
||
title: data.subject,
|
||
headCSS: [baseStyles, data.headCSS].join(" "),
|
||
} as Oy.RenderOptions,
|
||
this.template
|
||
);
|
||
|
||
try {
|
||
Logger.info("email", `Sending email "${data.subject}" to ${data.to}`);
|
||
|
||
invariant(
|
||
env.SMTP_FROM_EMAIL,
|
||
"SMTP_FROM_EMAIL is required to send emails"
|
||
);
|
||
|
||
const from = addressparser(env.SMTP_FROM_EMAIL)[0];
|
||
|
||
const info = await transporter.sendMail({
|
||
from: data.fromName
|
||
? {
|
||
name: data.fromName,
|
||
address: from.address,
|
||
}
|
||
: env.SMTP_FROM_EMAIL,
|
||
replyTo: data.replyTo ?? env.SMTP_REPLY_EMAIL ?? env.SMTP_FROM_EMAIL,
|
||
to: data.to,
|
||
subject: data.subject,
|
||
html,
|
||
text: data.text,
|
||
list: data.unsubscribeUrl
|
||
? {
|
||
unsubscribe: {
|
||
url: data.unsubscribeUrl,
|
||
comment: "Unsubscribe from these emails",
|
||
},
|
||
}
|
||
: undefined,
|
||
attachments: env.isCloudHosted
|
||
? undefined
|
||
: [
|
||
{
|
||
filename: "header-logo.png",
|
||
path: process.cwd() + "/public/email/header-logo.png",
|
||
cid: "header-image",
|
||
},
|
||
],
|
||
});
|
||
|
||
if (useTestEmailService) {
|
||
Logger.info(
|
||
"email",
|
||
`Preview Url: ${nodemailer.getTestMessageUrl(info)}`
|
||
);
|
||
}
|
||
} catch (err) {
|
||
Logger.error(`Error sending email to ${data.to}`, err);
|
||
throw err; // Re-throw for queue to re-try
|
||
}
|
||
};
|
||
|
||
private getOptions(): SMTPTransport.Options {
|
||
return {
|
||
name: env.SMTP_NAME,
|
||
host: env.SMTP_HOST,
|
||
port: env.SMTP_PORT,
|
||
secure: env.SMTP_SECURE ?? env.isProduction,
|
||
auth: env.SMTP_USERNAME
|
||
? {
|
||
user: env.SMTP_USERNAME,
|
||
pass: env.SMTP_PASSWORD,
|
||
}
|
||
: undefined,
|
||
tls: env.SMTP_SECURE
|
||
? env.SMTP_TLS_CIPHERS
|
||
? {
|
||
ciphers: env.SMTP_TLS_CIPHERS,
|
||
}
|
||
: undefined
|
||
: {
|
||
rejectUnauthorized: false,
|
||
},
|
||
};
|
||
}
|
||
|
||
private async getTestTransportOptions(): Promise<
|
||
SMTPTransport.Options | undefined
|
||
> {
|
||
try {
|
||
const testAccount = await nodemailer.createTestAccount();
|
||
return {
|
||
host: "smtp.ethereal.email",
|
||
port: 587,
|
||
secure: false,
|
||
auth: {
|
||
user: testAccount.user,
|
||
pass: testAccount.pass,
|
||
},
|
||
};
|
||
} catch (err) {
|
||
return undefined;
|
||
}
|
||
}
|
||
}
|
||
|
||
export default new Mailer();
|