From 05069b15d8ee01eb8dea192c9e642088533c7b74 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Wed, 28 Apr 2021 14:53:46 +0200 Subject: [PATCH] Properly use promises in ViewEngine.render(), use ViewEngine for mail and add NunjucksViewEngine --- src/Application.ts | 16 ++- src/Mails.ts | 2 +- src/TestApp.ts | 12 +- src/auth/AccountController.ts | 2 +- src/auth/AuthGuard.ts | 6 +- src/auth/magic_link/MagicLinkAuthMethod.ts | 2 +- src/auth/magic_link/MagicLinkController.ts | 9 +- src/components/FrontendToolsComponent.ts | 44 +++++- src/components/MailComponent.ts | 96 ++++++++++++- src/components/NunjucksComponent.ts | 7 +- src/frontend/MailViewEngine.ts | 27 ++++ src/frontend/NunjucksViewEngine.ts | 34 +++++ src/frontend/SvelteViewEngine.ts | 5 +- src/frontend/ViewEngine.ts | 17 ++- src/helpers/BackendController.ts | 10 +- src/mail/Mail.ts | 132 ++---------------- src/mail/MailController.ts | 5 +- src/mail/MailError.ts | 7 + src/mail/MailTemplate.ts | 19 +++ ...ce.mjml.njk => account_review_notice.mnjk} | 4 +- .../{add_email.mjml.njk => add_email.mnjk} | 4 +- ...{base_layout.mjml.njk => base_layout.mnjk} | 0 .../{magic_link.mjml.njk => magic_link.mnjk} | 4 +- ...w.mjml.njk => pending_account_review.mnjk} | 4 +- ...password.mjml.njk => remove_password.mnjk} | 2 +- 25 files changed, 295 insertions(+), 175 deletions(-) create mode 100644 src/frontend/MailViewEngine.ts create mode 100644 src/frontend/NunjucksViewEngine.ts create mode 100644 src/mail/MailError.ts create mode 100644 src/mail/MailTemplate.ts rename views/mails/{account_review_notice.mjml.njk => account_review_notice.mnjk} (95%) rename views/mails/{add_email.mjml.njk => add_email.mnjk} (93%) rename views/mails/{base_layout.mjml.njk => base_layout.mnjk} (100%) rename views/mails/{magic_link.mjml.njk => magic_link.mnjk} (97%) rename views/mails/{pending_account_review.mjml.njk => pending_account_review.mnjk} (92%) rename views/mails/{remove_password.mjml.njk => remove_password.mnjk} (94%) diff --git a/src/Application.ts b/src/Application.ts index b6bdfce..ed55dc1 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -263,9 +263,21 @@ export default abstract class Application implements Extendable>[]; @@ -145,12 +145,12 @@ export default class AuthGuard { } if (User.isApprovalMode()) { - await new Mail(this.app.as(NunjucksComponent).getEnvironment(), PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, { + await this.app.as(MailComponent).sendMail(new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, { username: user.asOptional(UserNameComponent)?.getName() || (await user.mainEmail.get())?.getOrFail('email') || 'Could not find an identifier', link: config.get('public_url') + Controller.route('accounts-approval'), - }).send(config.get('app.contact_email')); + }), config.get('app.contact_email')); } } diff --git a/src/auth/magic_link/MagicLinkAuthMethod.ts b/src/auth/magic_link/MagicLinkAuthMethod.ts index 75a0e96..99a11ed 100644 --- a/src/auth/magic_link/MagicLinkAuthMethod.ts +++ b/src/auth/magic_link/MagicLinkAuthMethod.ts @@ -8,12 +8,12 @@ import Controller from "../../Controller"; import geoip from "geoip-lite"; import MagicLinkController from "./MagicLinkController"; import Application from "../../Application"; -import {MailTemplate} from "../../mail/Mail"; import AuthMagicLinkActionType from "./AuthMagicLinkActionType"; import Validator, {EMAIL_REGEX} from "../../db/Validator"; import ModelFactory from "../../db/ModelFactory"; import UserNameComponent from "../models/UserNameComponent"; import {Session} from "express-session"; +import MailTemplate from "../../mail/MailTemplate"; export default class MagicLinkAuthMethod implements AuthMethod { public constructor( diff --git a/src/auth/magic_link/MagicLinkController.ts b/src/auth/magic_link/MagicLinkController.ts index 5b0d77c..3747203 100644 --- a/src/auth/magic_link/MagicLinkController.ts +++ b/src/auth/magic_link/MagicLinkController.ts @@ -3,12 +3,11 @@ import {Request, Response} from "express"; import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener"; import {BadRequestError, NotFoundHttpError} from "../../HttpError"; import Throttler from "../../Throttler"; -import Mail, {MailTemplate} from "../../mail/Mail"; +import Mail from "../../mail/Mail"; import MagicLink from "../models/MagicLink"; import config from "config"; import Application from "../../Application"; import {ParsedUrlQueryInput} from "querystring"; -import NunjucksComponent from "../../components/NunjucksComponent"; import User from "../models/User"; import AuthComponent, {AuthMiddleware} from "../AuthComponent"; import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard"; @@ -19,6 +18,8 @@ import UserNameComponent from "../models/UserNameComponent"; import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent"; import {logger} from "../../Logger"; import UserPasswordComponent from "../password/UserPasswordComponent"; +import MailTemplate from "../../mail/MailTemplate"; +import MailComponent from "../../components/MailComponent"; export default class MagicLinkController extends Controller { public static async sendMagicLink( @@ -46,12 +47,12 @@ export default class MagicLinkController extends Controll await link.save(); // Send email - await new Mail(app.as(NunjucksComponent).getEnvironment(), mailTemplate, Object.assign(data, { + await app.as(MailComponent).sendMail(new Mail(mailTemplate, Object.assign(data, { link: `${config.get('public_url')}${Controller.route('magic_link', undefined, { id: link.id, token: token, })}`, - })).send(email); + })), email); } public static async checkAndAuth(req: Request, res: Response, magicLink: MagicLink): Promise { diff --git a/src/components/FrontendToolsComponent.ts b/src/components/FrontendToolsComponent.ts index 1ae09f7..30a6555 100644 --- a/src/components/FrontendToolsComponent.ts +++ b/src/components/FrontendToolsComponent.ts @@ -7,15 +7,21 @@ import "svelte/register"; import ViewEngine from "../frontend/ViewEngine"; import {readdirRecursively} from "../Utils"; import FileCache from "../utils/FileCache"; +import Controller, {RouteParams} from "../Controller"; +import * as querystring from "querystring"; +import {ParsedUrlQueryInput} from "querystring"; +import util from "util"; export default class FrontendToolsComponent extends ApplicationComponent { private readonly publicAssetsCache: FileCache = new FileCache(); + private readonly viewEngines: ViewEngine[]; public constructor( private readonly publicAssetsDir: string, - private readonly viewEngine: ViewEngine, + ...viewEngines: ViewEngine[] ) { super(); + this.viewEngines = viewEngines; } public async start(app: Express): Promise { @@ -31,11 +37,20 @@ export default class FrontendToolsComponent extends ApplicationComponent { } // Setup express view engine - this.viewEngine.setup(app, true); + let main = true; + for (const viewEngine of this.viewEngines) { + viewEngine.setup(app, main); + main = false; + } + + // Add util globals + this.setupGlobals(); } public async stop(): Promise { - await this.viewEngine.stop(); + for (const viewEngine of this.viewEngines) { + await viewEngine.stop(); + } } public async handle(router: Router): Promise { @@ -59,6 +74,27 @@ export default class FrontendToolsComponent extends ApplicationComponent { } public async preCompileViews(watch: boolean): Promise { - await this.viewEngine.preCompileAll(watch); + for (const viewEngine of this.viewEngines) { + await viewEngine.preCompileAll(watch); + } + } + + public setupGlobals(): void { + ViewEngine.setGlobal('route', ( + route: string, + params: RouteParams = [], + query: ParsedUrlQueryInput = {}, + absolute: boolean = false, + ) => Controller.route(route, params, query, absolute)); + ViewEngine.setGlobal('app_version', this.getApp().getVersion()); + ViewEngine.setGlobal('core_version', this.getApp().getCoreVersion()); + ViewEngine.setGlobal('querystring', querystring); + ViewEngine.setGlobal('app', config.get('app')); + ViewEngine.setGlobal('dump', (val: unknown) => { + return util.inspect(val); + }); + ViewEngine.setGlobal('hex', (v: number) => { + return v.toString(16); + }); } } diff --git a/src/components/MailComponent.ts b/src/components/MailComponent.ts index 73c3e93..750a7eb 100644 --- a/src/components/MailComponent.ts +++ b/src/components/MailComponent.ts @@ -1,10 +1,24 @@ import ApplicationComponent from "../ApplicationComponent"; import {Express} from "express"; -import Mail from "../mail/Mail"; import config from "config"; import SecurityError from "../SecurityError"; +import nodemailer, {SentMessageInfo, Transporter} from "nodemailer"; +import MailError from "../mail/MailError"; +import util from "util"; +import {logger} from "../Logger"; +import Controller from "../Controller"; +import Mail from "../mail/Mail"; +import MailViewEngine from "../frontend/MailViewEngine"; +import ViewEngine from "../frontend/ViewEngine"; export default class MailComponent extends ApplicationComponent { + private transporter?: Transporter; + + public constructor( + private readonly viewEngine: MailViewEngine, + ) { + super(); + } public async checkSecuritySettings(): Promise { if (!config.get('mail.secure')) { @@ -15,12 +29,86 @@ export default class MailComponent extends ApplicationComponent { } } - public async start(_app: Express): Promise { - await this.prepare('Mail connection', () => Mail.prepare()); + public async start(app: Express): Promise { + await this.prepare('Mail connection', async () => { + const transporter = nodemailer.createTransport({ + host: config.get('mail.host'), + port: config.get('mail.port'), + requireTLS: config.get('mail.secure'), // STARTTLS + auth: { + user: config.get('mail.username'), + pass: config.get('mail.password'), + }, + tls: { + rejectUnauthorized: !config.get('mail.allow_invalid_tls'), + }, + }); + + try { + await util.promisify(transporter.verify)(); + this.transporter = transporter; + } catch (e) { + throw new MailError('Connection to mail service unsuccessful.', e); + } + + logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`); + }); + + await this.viewEngine.setup(app, false); } public async stop(): Promise { - Mail.end(); + if (this.transporter) { + this.transporter.close(); + this.transporter = undefined; + } + await this.viewEngine.stop(); } + public async sendMail(mail: Mail, ...to: string[]): Promise { + if (to.length === 0) throw new Error('Cannot send an email to recipient. (to is empty)'); + + const results = []; + + for (const destEmail of to) { + const template = mail.getTemplate(); + const locals = mail.getData(); + const options = mail.getOptions(); + + // Reset options + options.html = options.text = undefined; + + // Set options + options.to = destEmail; + options.from = { + name: config.get('mail.from_name'), + address: config.get('mail.from'), + }; + + // Set locals + locals.mail_subject = options.subject; + locals.mail_to = options.to; + locals.mail_link = config.get('public_url') + + Controller.route('mail', [template.template], locals); + Object.assign(locals, ViewEngine.getGlobals()); + + // Log + logger.debug(`Send mail from ${options.from.address} to ${options.to}`); + + // Render email + + options.html = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, false); + options.text = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, true); + + // Send email + results.push(await this.getTransporter().sendMail(options)); + } + + return results; + } + + private getTransporter(): Transporter { + if (!this.transporter) throw new MailError('Mail system was not prepared.'); + return this.transporter; + } } diff --git a/src/components/NunjucksComponent.ts b/src/components/NunjucksComponent.ts index d494135..ae28f13 100644 --- a/src/components/NunjucksComponent.ts +++ b/src/components/NunjucksComponent.ts @@ -1,14 +1,17 @@ import nunjucks, {Environment} from "nunjucks"; import config from "config"; -import {Express, NextFunction, Request, Response, Router} from "express"; +import {Express} from "express"; import ApplicationComponent from "../ApplicationComponent"; import Controller, {RouteParams} from "../Controller"; import * as querystring from "querystring"; import {ParsedUrlQueryInput} from "querystring"; import * as util from "util"; import * as path from "path"; -import Middleware from "../Middleware"; +/** + * @deprecated + * TODO: turn into a proper ViewEngine + */ export default class NunjucksComponent extends ApplicationComponent { private readonly viewsPath: string[]; private environment?: Environment; diff --git a/src/frontend/MailViewEngine.ts b/src/frontend/MailViewEngine.ts new file mode 100644 index 0000000..d44afe7 --- /dev/null +++ b/src/frontend/MailViewEngine.ts @@ -0,0 +1,27 @@ +import mjml2html from "mjml"; +import MailError from "../mail/MailError"; +import NunjucksViewEngine from "./NunjucksViewEngine"; + +export default class MailViewEngine extends NunjucksViewEngine { + public getExtension(): string { + return 'mnjk'; + } + + public async render( + file: string, + locals: Record, + textOnly: boolean = false, + ): Promise { + locals.text = textOnly; + const nunjucksResult = await super.render(file, locals); + if (textOnly) return nunjucksResult; + + const mjmlResult = mjml2html(nunjucksResult, {}); + + if (mjmlResult.errors.length > 0) { + throw new MailError(`Error while parsing mail template ${file}: ${JSON.stringify(mjmlResult.errors, null, 4)}`); + } + + return mjmlResult.html; + } +} diff --git a/src/frontend/NunjucksViewEngine.ts b/src/frontend/NunjucksViewEngine.ts new file mode 100644 index 0000000..1df17dc --- /dev/null +++ b/src/frontend/NunjucksViewEngine.ts @@ -0,0 +1,34 @@ +import ViewEngine from "./ViewEngine"; +import config from "config"; +import nunjucks, {Environment} from "nunjucks"; + +export default class NunjucksViewEngine extends ViewEngine { + private readonly environment: Environment; + + public constructor(devWatchedViewDir: string, ...additionalViewPaths: string[]) { + super(devWatchedViewDir, ...additionalViewPaths); + + const opts = { + autoescape: true, + noCache: !config.get('view.cache'), + throwOnUndefined: true, + }; + this.environment = new nunjucks.Environment([ + ...this.viewPaths.map(path => new nunjucks.FileSystemLoader(path, opts)), + ], opts); + } + + public getExtension(): string { + return 'njk'; + } + + public async render(file: string, locals: Record): Promise { + return await new Promise((resolve, reject) => { + this.environment.render(file, locals, (err, res) => { + if (err) return reject(err); + else if (res === null) reject('Null response from nunjucks environment.render()'); + else return resolve(res); + }); + }); + } +} diff --git a/src/frontend/SvelteViewEngine.ts b/src/frontend/SvelteViewEngine.ts index 4f52ca8..824cc46 100644 --- a/src/frontend/SvelteViewEngine.ts +++ b/src/frontend/SvelteViewEngine.ts @@ -63,8 +63,7 @@ export default class SvelteViewEngine extends ViewEngine { public async render( file: string, locals: Record, - callback: (err: Error | null, output?: string) => void, - ): Promise { + ): Promise { const canonicalViewName = this.toCanonicalName(file); // View @@ -140,7 +139,7 @@ export default class SvelteViewEngine extends ViewEngine { } } - callback(null, output); + return output; } public async stop(): Promise { diff --git a/src/frontend/ViewEngine.ts b/src/frontend/ViewEngine.ts index 21db594..349fe5a 100644 --- a/src/frontend/ViewEngine.ts +++ b/src/frontend/ViewEngine.ts @@ -9,7 +9,7 @@ export default abstract class ViewEngine { private static readonly globals: Record = {}; public static getGlobals(): Record { - return this.globals; + return {...this.globals}; } public static setGlobal(key: string, value: unknown): void { @@ -44,21 +44,26 @@ export default abstract class ViewEngine { public abstract render( file: string, locals: Record, - callback: (err: Error | null, output?: string) => void, - ): Promise; + ): Promise; public setup(app: Express, main: boolean): void { app.engine(this.getExtension(), (path, options, callback) => { // Props (locals) - const locals = Object.assign(ViewEngine.getGlobals(), options); + const locals = Object.assign(options, ViewEngine.getGlobals()); - this.render(path, locals, callback) + this.render(path, locals) + .then(value => callback(null, value)) .catch(err => callback(err)); }); const existingViewPaths = app.get('views'); app.set('views', existingViewPaths ? - [...existingViewPaths, ...this.getViewPaths()] : + [...new Set([ + ...typeof existingViewPaths === 'string' ? + [existingViewPaths] : + existingViewPaths, + ...this.getViewPaths(), + ])] : this.getViewPaths()); if (main) { diff --git a/src/helpers/BackendController.ts b/src/helpers/BackendController.ts index 71a4d83..f293556 100644 --- a/src/helpers/BackendController.ts +++ b/src/helpers/BackendController.ts @@ -8,9 +8,9 @@ import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails"; import UserEmail from "../auth/models/UserEmail"; import UserApprovedComponent from "../auth/models/UserApprovedComponent"; import {RequireAdminMiddleware, RequireAuthMiddleware} from "../auth/AuthComponent"; -import NunjucksComponent from "../components/NunjucksComponent"; import ModelFactory from "../db/ModelFactory"; import UserNameComponent from "../auth/models/UserNameComponent"; +import MailComponent from "../components/MailComponent"; export default class BackendController extends Controller { private static readonly menu: BackendMenuElement[] = []; @@ -78,10 +78,10 @@ export default class BackendController extends Controller { await account.save(); if (email && email.email) { - await new Mail(this.getApp().as(NunjucksComponent).getEnvironment(), ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, { + await this.getApp().as(MailComponent).sendMail(new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, { approved: true, link: config.get('public_url') + Controller.route('auth'), - }).send(email.email); + }), email.email); } req.flash('success', `Account successfully approved.`); @@ -94,9 +94,9 @@ export default class BackendController extends Controller { await account.delete(); if (email && email.email) { - await new Mail(this.getApp().as(NunjucksComponent).getEnvironment(), ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, { + await this.getApp().as(MailComponent).sendMail(new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, { approved: false, - }).send(email.email); + }), email.email); } req.flash('success', `Account successfully deleted.`); diff --git a/src/mail/Mail.ts b/src/mail/Mail.ts index 4ad2303..40175ef 100644 --- a/src/mail/Mail.ts +++ b/src/mail/Mail.ts @@ -1,78 +1,15 @@ -import nodemailer, {SentMessageInfo, Transporter} from "nodemailer"; -import config from "config"; import {Options} from "nodemailer/lib/mailer"; -import {Environment} from 'nunjucks'; -import * as util from "util"; -import {WrappingError} from "../Utils"; -import mjml2html from "mjml"; -import {logger} from "../Logger"; -import Controller from "../Controller"; import {ParsedUrlQueryInput} from "querystring"; +import MailError from "./MailError"; +import MailTemplate from "./MailTemplate"; export default class Mail { - private static transporter?: Transporter; - - private static getTransporter(): Transporter { - if (!this.transporter) throw new MailError('Mail system was not prepared.'); - return this.transporter; - } - - public static async prepare(): Promise { - const transporter = nodemailer.createTransport({ - host: config.get('mail.host'), - port: config.get('mail.port'), - requireTLS: config.get('mail.secure'), // STARTTLS - auth: { - user: config.get('mail.username'), - pass: config.get('mail.password'), - }, - tls: { - rejectUnauthorized: !config.get('mail.allow_invalid_tls'), - }, - }); - - try { - await util.promisify(transporter.verify)(); - this.transporter = transporter; - } catch (e) { - throw new MailError('Connection to mail service unsuccessful.', e); - } - - logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`); - } - - public static end(): void { - if (this.transporter) this.transporter.close(); - } - - public static parse( - environment: Environment, - template: string, - data: { [p: string]: unknown }, - textOnly: boolean, - ): string { - data.text = textOnly; - const nunjucksResult = environment.render(template, data); - if (textOnly) return nunjucksResult; - - const mjmlResult = mjml2html(nunjucksResult, {}); - - if (mjmlResult.errors.length > 0) { - throw new MailError(`Error while parsing mail template ${template}: ${JSON.stringify(mjmlResult.errors, null, 4)}`); - } - - return mjmlResult.html; - } - private readonly options: Options = {}; public constructor( - private readonly environment: Environment, private readonly template: MailTemplate, private readonly data: ParsedUrlQueryInput = {}, ) { - this.template = template; - this.data = data; this.options.subject = this.template.getSubject(data); this.verifyData(); @@ -88,64 +25,15 @@ export default class Mail { } } - public async send(...to: string[]): Promise { - const results = []; + public getTemplate(): MailTemplate { + return this.template; + } - for (const destEmail of to) { - // Reset options - this.options.html = this.options.text = undefined; + public getData(): ParsedUrlQueryInput { + return {...this.data}; + } - // Set options - this.options.to = destEmail; - this.options.from = { - name: config.get('mail.from_name'), - address: config.get('mail.from'), - }; - - // Set data - this.data.mail_subject = this.options.subject; - this.data.mail_to = this.options.to; - this.data.mail_link = config.get('public_url') + - Controller.route('mail', [this.template.template], this.data); - this.data.app = config.get('app'); - - // Log - logger.debug(`Send mail from ${this.options.from.address} to ${this.options.to}`); - - // Render email - this.options.html = Mail.parse(this.environment, 'mails/' + this.template.template + '.mjml.njk', - this.data, false); - this.options.text = Mail.parse(this.environment, 'mails/' + this.template.template + '.mjml.njk', - this.data, true); - - // Send email - results.push(await Mail.getTransporter().sendMail(this.options)); - } - - return results; - } -} - -export class MailTemplate { - private readonly _template: string; - private readonly subject: (data: { [p: string]: unknown }) => string; - - public constructor(template: string, subject: (data: { [p: string]: unknown }) => string) { - this._template = template; - this.subject = subject; - } - - public get template(): string { - return this._template; - } - - public getSubject(data: { [p: string]: unknown }): string { - return `${config.get('app.name')} - ${this.subject(data)}`; - } -} - -class MailError extends WrappingError { - public constructor(message: string = 'An error occurred while sending mail.', cause?: Error) { - super(message, cause); + public getOptions(): Options { + return {...this.options}; } } diff --git a/src/mail/MailController.ts b/src/mail/MailController.ts index f3b7c64..23016f6 100644 --- a/src/mail/MailController.ts +++ b/src/mail/MailController.ts @@ -1,7 +1,5 @@ import {Request, Response} from "express"; import Controller from "../Controller"; -import Mail from "./Mail"; -import NunjucksComponent from "../components/NunjucksComponent"; export default class MailController extends Controller { public routes(): void { @@ -10,7 +8,6 @@ export default class MailController extends Controller { protected async getMail(request: Request, response: Response): Promise { const template = request.params['template']; - response.send(Mail.parse(this.getApp().as(NunjucksComponent).getEnvironment(), - `mails/${template}.mjml.njk`, request.query, false)); + response.render(`mails/${template}.mnjk`, request.query); } } diff --git a/src/mail/MailError.ts b/src/mail/MailError.ts new file mode 100644 index 0000000..d08297c --- /dev/null +++ b/src/mail/MailError.ts @@ -0,0 +1,7 @@ +import {WrappingError} from "../Utils"; + +export default class MailError extends WrappingError { + public constructor(message: string = 'An error occurred while sending mail.', cause?: Error) { + super(message, cause); + } +} diff --git a/src/mail/MailTemplate.ts b/src/mail/MailTemplate.ts new file mode 100644 index 0000000..f3abae7 --- /dev/null +++ b/src/mail/MailTemplate.ts @@ -0,0 +1,19 @@ +import config from "config"; + +export default class MailTemplate { + private readonly _template: string; + private readonly subject: (data: { [p: string]: unknown }) => string; + + public constructor(template: string, subject: (data: { [p: string]: unknown }) => string) { + this._template = template; + this.subject = subject; + } + + public get template(): string { + return this._template; + } + + public getSubject(data: { [p: string]: unknown }): string { + return `${config.get('app.name')} - ${this.subject(data)}`; + } +} diff --git a/views/mails/account_review_notice.mjml.njk b/views/mails/account_review_notice.mnjk similarity index 95% rename from views/mails/account_review_notice.mjml.njk rename to views/mails/account_review_notice.mnjk index 43b1ae3..d572a96 100644 --- a/views/mails/account_review_notice.mjml.njk +++ b/views/mails/account_review_notice.mnjk @@ -1,4 +1,4 @@ -{% extends 'mails/base_layout.mjml.njk' %} +{% extends 'mails/base_layout.mnjk' %} {% block body %} @@ -38,4 +38,4 @@ If you believe that this is an error, please contact us via email. {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/views/mails/add_email.mjml.njk b/views/mails/add_email.mnjk similarity index 93% rename from views/mails/add_email.mjml.njk rename to views/mails/add_email.mnjk index fc5e8f0..295ff7b 100644 --- a/views/mails/add_email.mjml.njk +++ b/views/mails/add_email.mnjk @@ -1,4 +1,4 @@ -{% extends 'mails/base_layout.mjml.njk' %} +{% extends 'mails/base_layout.mnjk' %} {% block body %} @@ -24,4 +24,4 @@ Someone wants to add {{ mail_to }} to their account. To add this email address, please follow this link: {{ link|safe }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/views/mails/base_layout.mjml.njk b/views/mails/base_layout.mnjk similarity index 100% rename from views/mails/base_layout.mjml.njk rename to views/mails/base_layout.mnjk diff --git a/views/mails/magic_link.mjml.njk b/views/mails/magic_link.mnjk similarity index 97% rename from views/mails/magic_link.mjml.njk rename to views/mails/magic_link.mnjk index 208fa4e..e06b036 100644 --- a/views/mails/magic_link.mjml.njk +++ b/views/mails/magic_link.mnjk @@ -1,4 +1,4 @@ -{% extends 'mails/base_layout.mjml.njk' %} +{% extends 'mails/base_layout.mnjk' %} {% block body %} @@ -63,4 +63,4 @@ Location: {{ geo }} To authorize this log in, please follow this link: {{ link|safe }} {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/views/mails/pending_account_review.mjml.njk b/views/mails/pending_account_review.mnjk similarity index 92% rename from views/mails/pending_account_review.mjml.njk rename to views/mails/pending_account_review.mnjk index 0687756..e1b3dd5 100644 --- a/views/mails/pending_account_review.mjml.njk +++ b/views/mails/pending_account_review.mnjk @@ -1,4 +1,4 @@ -{% extends 'mails/base_layout.mjml.njk' %} +{% extends 'mails/base_layout.mnjk' %} {% block body %} @@ -23,4 +23,4 @@ Username: {{ username }} To review this account, please follow this link: {{ link|safe }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/views/mails/remove_password.mjml.njk b/views/mails/remove_password.mnjk similarity index 94% rename from views/mails/remove_password.mjml.njk rename to views/mails/remove_password.mnjk index 5681303..6bfa949 100644 --- a/views/mails/remove_password.mjml.njk +++ b/views/mails/remove_password.mnjk @@ -1,4 +1,4 @@ -{% extends 'mails/base_layout.mjml.njk' %} +{% extends 'mails/base_layout.mnjk' %} {% block body %}