summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/email.ts101
-rw-r--r--src/lib/handlebars.ts97
2 files changed, 97 insertions, 101 deletions
diff --git a/src/lib/email.ts b/src/lib/email.ts
index e7243aa..57f69f5 100644
--- a/src/lib/email.ts
+++ b/src/lib/email.ts
@@ -1,13 +1,15 @@
import sgMail from "@sendgrid/mail";
+import sgHelpers from "@sendgrid/helpers";
+
import nodemailer, { Transporter } from "nodemailer";
import { getConfig } from "./config.js";
import SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
import { exitWithError } from "./process.js";
-import { renderTemplate } from "./handlebars.js";
-import { ExpressHandlebars } from "express-handlebars";
+import { HandlebarsSingleton } from "./handlebars.js";
+
const config = getConfig();
-type EmailTemplate =
+type EmailTemplateName =
| "addEventAttendee"
| "addEventComment"
| "createEvent"
@@ -16,6 +18,7 @@ type EmailTemplate =
| "deleteEvent"
| "editEvent"
| "eventGroupUpdated"
+ | "removeEventAttendee"
| "subscribed"
| "unattendEvent";
@@ -89,30 +92,6 @@ export const initEmailService = async (): Promise<boolean> => {
}
};
-export const sendTemplatedEmail = async (
- hbs: ExpressHandlebars,
- to: string,
- bcc: string,
- subject: string,
- template: string,
- data: object,
-): Promise<boolean> => {
- const [html, text] = await Promise.all([
- hbs.renderView(`./views/emails/${template}Html.handlebars`, {
- cache: true,
- layout: "email.handlebars",
- ...data,
- }),
- hbs.renderView(`./views/emails/${template}Text.handlebars`, {
- cache: true,
- layout: "email.handlebars",
- ...data,
- }),
- ]);
-
- return await sendEmail(to, bcc, subject, text, html);
-};
-
export const sendEmail = async (
to: string | string[],
bcc: string | string[] | undefined,
@@ -132,11 +111,11 @@ export const sendEmail = async (
html,
});
return true;
- } catch (e: Error) {
- if (e.response) {
- console.error(e.response.body);
+ } catch (e: unknown | sgHelpers.classes.ResponseError) {
+ if (e instanceof sgHelpers.classes.ResponseError) {
+ console.error('sendgrid error', e.response.body);
} else {
- console.error(e);
+ console.error('sendgrid error', e);
}
return false;
}
@@ -164,15 +143,10 @@ export const sendEmail = async (
nodemailer.createTransport(nodemailerConfig);
}
await nodemailerTransporter.sendMail({
- envelope: {
- from: config.general.email,
- to,
- bcc,
- },
from: config.general.email,
to,
bcc,
- subject,
+ subject: `${config.general.site_name}: ${subject}`,
text,
html,
});
@@ -187,25 +161,42 @@ export const sendEmail = async (
};
export const sendEmailFromTemplate = async (
- to: string,
- bcc: string,
+ to: string | string[],
+ bcc: string | string[] | undefined,
subject: string,
- template: EmailTemplate,
- templateData: Record<string, unknown>,
- req: Request,
+ templateName: EmailTemplateName,
+ templateData: object,
): Promise<boolean> => {
- const html = await renderTemplate(req, `${template}/${template}Html`, {
- siteName: config.general.site_name,
- siteLogo: config.general.email_logo_url,
- domain: config.general.domain,
- cache: true,
- layout: "email.handlebars",
- ...templateData,
- });
- const text = await renderTemplate(
- req,
- `${template}/${template}Text`,
- templateData,
- );
+ const [html, text] = await Promise.all([
+ HandlebarsSingleton.instance.renderView(
+ `./views/emails/${templateName}/${templateName}Html.handlebars`,
+ {
+ domain: config.general.domain,
+ contactEmail: config.general.email,
+ siteName: config.general.site_name,
+ mailService: config.general.mail_service,
+ siteLogo: config.general.email_logo_url,
+ isFederated: config.general.is_federated || true,
+ cache: true,
+ layout: "email.handlebars",
+ ...templateData,
+ }
+ ),
+ HandlebarsSingleton.instance.renderView(
+ `./views/emails/${templateName}/${templateName}Text.handlebars`,
+ {
+ domain: config.general.domain,
+ contactEmail: config.general.email,
+ siteName: config.general.site_name,
+ mailService: config.general.mail_service,
+ siteLogo: config.general.email_logo_url,
+ isFederated: config.general.is_federated || true,
+ cache: true,
+ layout: "email.handlebars",
+ ...templateData,
+ }
+ ),
+ ]);
+
return await sendEmail(to, bcc, subject, text, html);
};
diff --git a/src/lib/handlebars.ts b/src/lib/handlebars.ts
index 42f8010..6d4f796 100644
--- a/src/lib/handlebars.ts
+++ b/src/lib/handlebars.ts
@@ -1,50 +1,55 @@
-import { Request } from "express";
-import { ExpressHandlebars } from "express-handlebars";
+import hbs, { ExpressHandlebars } from "express-handlebars";
+import { RenderViewOptions } from "express-handlebars/types/index.js";
-export const renderTemplate = async (
- req: Request,
- templateName: string,
- data: Record<string, unknown>,
-): Promise<string> => {
- return new Promise<string>((resolve, reject) => {
- req.app
- .get("hbsInstance")
- .renderView(
- `./views/emails/${templateName}.handlebars`,
- data,
- (err: any, html: string) => {
- if (err) {
- console.error(err);
- reject(err);
- }
- resolve(html);
+export class HandlebarsSingleton {
+ static #instance: HandlebarsSingleton;
+ hbsInstance: hbs.ExpressHandlebars;
+
+ private constructor() {
+ this.hbsInstance = hbs.create({
+ defaultLayout: "main",
+ partialsDir: ["views/partials/"],
+ layoutsDir: "views/layouts/",
+ helpers: {
+ plural: function (number: number, text: string) {
+ const singular = number === 1;
+ // If no text parameter was given, just return a conditional s.
+ if (typeof text !== "string") return singular ? "" : "s";
+ // Split with regex into group1/group2 or group1(group3)
+ const match = text.match(/^([^()\/]+)(?:\/(.+))?(?:\((\w+)\))?/);
+ // If no match, just append a conditional s.
+ if (!match) return text + (singular ? "" : "s");
+ // We have a good match, so fire away
+ return (
+ (singular && match[1]) || // Singular case
+ match[2] || // Plural case: 'bagel/bagels' --> bagels
+ match[1] + (match[3] || "s")
+ ); // Plural case: 'bagel(s)' or 'bagel' --> bagels
+ },
+ json: function (context: object) {
+ return JSON.stringify(context);
},
- );
- });
-};
+ },
+ });
+ }
+
+ public static get instance(): HandlebarsSingleton {
+ if (!HandlebarsSingleton.#instance) {
+ HandlebarsSingleton.#instance = new HandlebarsSingleton();
+ }
+
+ return HandlebarsSingleton.#instance;
+ }
+
+ public get engine(): ExpressHandlebars["engine"] {
+ return this.hbsInstance.engine;
+ }
-export const renderEmail = async (
- hbsInstance: ExpressHandlebars,
- templateName: string,
- data: Record<string, unknown>,
-): Promise<{ html: string, text: string }> => {
- const [html, text] = await Promise.all([
- hbsInstance.renderView(
- `./views/emails/${templateName}Html.handlebars`,
- {
- cache: true,
- layout: "email.handlebars",
- ...data,
- }
- ),
- hbsInstance.renderView(
- `./views/emails/${templateName}Text.handlebars`,
- {
- cache: true,
- layout: "email.handlebars",
- ...data,
- }
- ),
- ]);
- return { html, text }
+ /**
+ * Finally, any singleton can define some business logic, which can be
+ * executed on its instance.
+ */
+ public renderView(viewPath: string, options: RenderViewOptions): Promise<string> {
+ return this.hbsInstance.renderView(viewPath, options);
+ }
}