2020-04-22 15:52:17 +02:00
|
|
|
import nodemailer, {SentMessageInfo, Transporter} from "nodemailer";
|
|
|
|
import config from "config";
|
|
|
|
import {Options} from "nodemailer/lib/mailer";
|
|
|
|
import nunjucks from 'nunjucks';
|
|
|
|
import * as util from "util";
|
|
|
|
import {WrappingError} from "./Utils";
|
|
|
|
import mjml2html from "mjml";
|
|
|
|
import Logger from "./Logger";
|
2020-07-15 11:42:49 +02:00
|
|
|
import Controller from "./Controller";
|
2020-09-25 23:42:15 +02:00
|
|
|
import {ParsedUrlQueryInput} from "querystring";
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
export default class Mail {
|
2020-09-25 23:42:15 +02:00
|
|
|
private static transporter?: Transporter;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
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'),
|
2020-07-20 11:29:10 +02:00
|
|
|
requireTLS: config.get('mail.secure'), // STARTTLS
|
2020-04-22 15:52:17 +02:00
|
|
|
auth: {
|
|
|
|
user: config.get('mail.username'),
|
|
|
|
pass: config.get('mail.password'),
|
|
|
|
},
|
|
|
|
tls: {
|
2020-09-25 23:42:15 +02:00
|
|
|
rejectUnauthorized: !config.get('mail.allow_invalid_tls'),
|
|
|
|
},
|
2020-04-22 15:52:17 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
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')}`);
|
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public static end(): void {
|
2020-06-05 14:32:39 +02:00
|
|
|
if (this.transporter) this.transporter.close();
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public static parse(template: string, data: { [p: string]: unknown }, textOnly: boolean): string {
|
2020-04-22 15:52:17 +02:00
|
|
|
data.text = textOnly;
|
|
|
|
const nunjucksResult = nunjucks.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 template: MailTemplate;
|
|
|
|
private readonly options: Options = {};
|
2020-09-25 23:42:15 +02:00
|
|
|
private readonly data: ParsedUrlQueryInput;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor(template: MailTemplate, data: ParsedUrlQueryInput = {}) {
|
2020-04-22 15:52:17 +02:00
|
|
|
this.template = template;
|
|
|
|
this.data = data;
|
|
|
|
this.options.subject = this.template.getSubject(data);
|
|
|
|
|
|
|
|
this.verifyData();
|
|
|
|
}
|
|
|
|
|
|
|
|
private verifyData() {
|
|
|
|
for (const forbiddenField of [
|
|
|
|
'to',
|
|
|
|
]) {
|
|
|
|
if (this.data[forbiddenField] !== undefined) {
|
|
|
|
throw new MailError(`Can't use reserved data.${forbiddenField}.`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async send(...to: string[]): Promise<SentMessageInfo[]> {
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
for (const destEmail of to) {
|
|
|
|
// Reset options
|
|
|
|
this.options.html = this.options.text = undefined;
|
|
|
|
|
|
|
|
// Set options
|
|
|
|
this.options.to = destEmail;
|
2020-06-27 18:06:39 +02:00
|
|
|
this.options.from = {
|
|
|
|
name: config.get('mail.from_name'),
|
|
|
|
address: config.get('mail.from'),
|
|
|
|
};
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
// Set data
|
|
|
|
this.data.mail_subject = this.options.subject;
|
|
|
|
this.data.mail_to = this.options.to;
|
2020-09-25 23:42:15 +02:00
|
|
|
this.data.mail_link = config.get<string>('base_url') +
|
|
|
|
Controller.route('mail', [this.template.template], this.data);
|
2020-06-27 18:06:39 +02:00
|
|
|
this.data.app = config.get('app');
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
// Log
|
|
|
|
Logger.dev('Send mail', this.options);
|
|
|
|
|
|
|
|
// Render email
|
|
|
|
this.options.html = Mail.parse('mails/' + this.template.template + '.mjml.njk', this.data, false);
|
|
|
|
this.options.text = Mail.parse('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;
|
2020-09-25 23:42:15 +02:00
|
|
|
private readonly subject: (data: { [p: string]: unknown }) => string;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor(template: string, subject: (data: { [p: string]: unknown }) => string) {
|
2020-04-22 15:52:17 +02:00
|
|
|
this._template = template;
|
|
|
|
this.subject = subject;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get template(): string {
|
|
|
|
return this._template;
|
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public getSubject(data: { [p: string]: unknown }): string {
|
2020-06-27 18:06:39 +02:00
|
|
|
return `${config.get('app.name')} - ${this.subject(data)}`;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class MailError extends WrappingError {
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor(message: string = 'An error occurred while sending mail.', cause?: Error) {
|
2020-04-22 15:52:17 +02:00
|
|
|
super(message, cause);
|
|
|
|
}
|
2020-09-25 23:42:15 +02:00
|
|
|
}
|