Add svelte as a view engine to swaf #33
@ -263,9 +263,21 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
||||
await MysqlConnectionManager.migrationCommand(mainCommandArgs);
|
||||
await this.stop();
|
||||
break;
|
||||
case 'pre-compile-views':
|
||||
await this.as(FrontendToolsComponent).preCompileViews(flags.watch);
|
||||
case 'pre-compile-views': {
|
||||
// Prepare migrations
|
||||
for (const migration of this.getMigrations()) {
|
||||
new migration().registerModels?.();
|
||||
}
|
||||
// Prepare routes
|
||||
for (const controller of this.controllers) {
|
||||
controller.setupRoutes();
|
||||
}
|
||||
|
||||
const frontendToolsComponent = this.as(FrontendToolsComponent);
|
||||
frontendToolsComponent.setupGlobals();
|
||||
await frontendToolsComponent.preCompileViews(flags.watch);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
logger.fatal('Unimplemented main command', mainCommand);
|
||||
break;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import config from "config";
|
||||
import {MailTemplate} from "./mail/Mail";
|
||||
import MailTemplate from "./mail/MailTemplate";
|
||||
|
||||
export const MAGIC_LINK_MAIL = new MailTemplate(
|
||||
'magic_link',
|
||||
|
@ -3,7 +3,6 @@ import Migration, {MigrationType} from "./db/Migration";
|
||||
import ExpressAppComponent from "./components/ExpressAppComponent";
|
||||
import RedisComponent from "./components/RedisComponent";
|
||||
import MysqlComponent from "./components/MysqlComponent";
|
||||
import NunjucksComponent from "./components/NunjucksComponent";
|
||||
import LogRequestsComponent from "./components/LogRequestsComponent";
|
||||
import MailComponent from "./components/MailComponent";
|
||||
import SessionComponent from "./components/SessionComponent";
|
||||
@ -34,6 +33,8 @@ import AddNameChangedAtToUsersMigration from "./auth/migrations/AddNameChangedAt
|
||||
import BackendController from "./helpers/BackendController";
|
||||
import FrontendToolsComponent from "./components/FrontendToolsComponent";
|
||||
import SvelteViewEngine from "./frontend/SvelteViewEngine";
|
||||
import MailViewEngine from "./frontend/MailViewEngine";
|
||||
import NunjucksViewEngine from "./frontend/NunjucksViewEngine";
|
||||
import packageJson = require('./package.json');
|
||||
|
||||
export const MIGRATIONS = [
|
||||
@ -76,13 +77,16 @@ export default class TestApp extends Application {
|
||||
this.use(new ServeStaticDirectoryComponent('public'));
|
||||
|
||||
// Dynamic views and routes
|
||||
this.use(new NunjucksComponent(['test/views', 'views']));
|
||||
this.use(new FrontendToolsComponent(new SvelteViewEngine('build/views', 'public', 'views')));
|
||||
this.use(new FrontendToolsComponent(
|
||||
'public',
|
||||
new SvelteViewEngine('build/views', 'public', 'views', 'test/views'),
|
||||
new NunjucksViewEngine('views', 'test/views'),
|
||||
));
|
||||
this.use(new PreviousUrlComponent());
|
||||
|
||||
// Services
|
||||
this.use(new MysqlComponent());
|
||||
this.use(new MailComponent());
|
||||
this.use(new MailComponent(new MailViewEngine('views', 'test/views')));
|
||||
|
||||
// Session
|
||||
this.use(new RedisComponent());
|
||||
|
@ -9,11 +9,11 @@ import User from "./models/User";
|
||||
import ModelFactory from "../db/ModelFactory";
|
||||
import UserEmail from "./models/UserEmail";
|
||||
import MagicLinkController from "./magic_link/MagicLinkController";
|
||||
import {MailTemplate} from "../mail/Mail";
|
||||
import {ADD_EMAIL_MAIL_TEMPLATE, REMOVE_PASSWORD_MAIL_TEMPLATE} from "../Mails";
|
||||
import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType";
|
||||
import UserNameComponent from "./models/UserNameComponent";
|
||||
import Time from "../Time";
|
||||
import MailTemplate from "../mail/MailTemplate";
|
||||
|
||||
export default class AccountController extends Controller {
|
||||
|
||||
|
@ -8,10 +8,10 @@ import Mail from "../mail/Mail";
|
||||
import Controller from "../Controller";
|
||||
import config from "config";
|
||||
import Application from "../Application";
|
||||
import NunjucksComponent from "../components/NunjucksComponent";
|
||||
import AuthMethod from "./AuthMethod";
|
||||
import {Session, SessionData} from "express-session";
|
||||
import UserNameComponent from "./models/UserNameComponent";
|
||||
import MailComponent from "../components/MailComponent";
|
||||
|
||||
export default class AuthGuard {
|
||||
private readonly authMethods: AuthMethod<AuthProof<User>>[];
|
||||
@ -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<string>('public_url') + Controller.route('accounts-approval'),
|
||||
}).send(config.get<string>('app.contact_email'));
|
||||
}), config.get<string>('app.contact_email'));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<MagicLink> {
|
||||
public constructor(
|
||||
|
@ -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<A extends Application> extends Controller {
|
||||
public static async sendMagicLink(
|
||||
@ -46,12 +47,12 @@ export default class MagicLinkController<A extends Application> 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<string>('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<User | null> {
|
||||
|
@ -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<void> {
|
||||
@ -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<void> {
|
||||
await this.viewEngine.stop();
|
||||
for (const viewEngine of this.viewEngines) {
|
||||
await viewEngine.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(router: Router): Promise<void> {
|
||||
@ -59,6 +74,27 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
||||
}
|
||||
|
||||
public async preCompileViews(watch: boolean): Promise<void> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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<void> {
|
||||
if (!config.get<boolean>('mail.secure')) {
|
||||
@ -15,12 +29,86 @@ export default class MailComponent extends ApplicationComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public async start(_app: Express): Promise<void> {
|
||||
await this.prepare('Mail connection', () => Mail.prepare());
|
||||
public async start(app: Express): Promise<void> {
|
||||
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<void> {
|
||||
Mail.end();
|
||||
if (this.transporter) {
|
||||
this.transporter.close();
|
||||
this.transporter = undefined;
|
||||
}
|
||||
await this.viewEngine.stop();
|
||||
}
|
||||
|
||||
public async sendMail(mail: Mail, ...to: string[]): Promise<SentMessageInfo[]> {
|
||||
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<string>('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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
27
src/frontend/MailViewEngine.ts
Normal file
27
src/frontend/MailViewEngine.ts
Normal file
@ -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<string, unknown>,
|
||||
textOnly: boolean = false,
|
||||
): Promise<string> {
|
||||
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;
|
||||
}
|
||||
}
|
34
src/frontend/NunjucksViewEngine.ts
Normal file
34
src/frontend/NunjucksViewEngine.ts
Normal file
@ -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<string, unknown>): Promise<string> {
|
||||
return await new Promise<string>((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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -63,8 +63,7 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
public async render(
|
||||
file: string,
|
||||
locals: Record<string, unknown>,
|
||||
callback: (err: Error | null, output?: string) => void,
|
||||
): Promise<void> {
|
||||
): Promise<string> {
|
||||
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<void> {
|
||||
|
@ -9,7 +9,7 @@ export default abstract class ViewEngine {
|
||||
private static readonly globals: Record<string, unknown> = {};
|
||||
|
||||
public static getGlobals(): Record<string, unknown> {
|
||||
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<string, unknown>,
|
||||
callback: (err: Error | null, output?: string) => void,
|
||||
): Promise<void>;
|
||||
): Promise<string>;
|
||||
|
||||
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) {
|
||||
|
@ -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<string>('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.`);
|
||||
|
132
src/mail/Mail.ts
132
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<void> {
|
||||
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<SentMessageInfo[]> {
|
||||
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<string>('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};
|
||||
}
|
||||
}
|
||||
|
@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
7
src/mail/MailError.ts
Normal file
7
src/mail/MailError.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
19
src/mail/MailTemplate.ts
Normal file
19
src/mail/MailTemplate.ts
Normal file
@ -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)}`;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
{% extends 'mails/base_layout.mjml.njk' %}
|
||||
{% extends 'mails/base_layout.mnjk' %}
|
||||
|
||||
{% block body %}
|
||||
<mj-section>
|
||||
@ -38,4 +38,4 @@
|
||||
|
||||
If you believe that this is an error, please contact us via email.
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{% extends 'mails/base_layout.mjml.njk' %}
|
||||
{% extends 'mails/base_layout.mnjk' %}
|
||||
|
||||
{% block body %}
|
||||
<mj-section>
|
||||
@ -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 %}
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{% extends 'mails/base_layout.mjml.njk' %}
|
||||
{% extends 'mails/base_layout.mnjk' %}
|
||||
|
||||
{% block body %}
|
||||
<mj-section>
|
||||
@ -63,4 +63,4 @@
|
||||
Location: {{ geo }}
|
||||
To authorize this log in, please follow this link: {{ link|safe }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{% extends 'mails/base_layout.mjml.njk' %}
|
||||
{% extends 'mails/base_layout.mnjk' %}
|
||||
|
||||
{% block body %}
|
||||
<mj-section>
|
||||
@ -23,4 +23,4 @@
|
||||
|
||||
Username: {{ username }}
|
||||
To review this account, please follow this link: {{ link|safe }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{% extends 'mails/base_layout.mjml.njk' %}
|
||||
{% extends 'mails/base_layout.mnjk' %}
|
||||
|
||||
{% block body %}
|
||||
<mj-section>
|
Loading…
Reference in New Issue
Block a user