swaf/src/mail/Mail.ts

152 lines
4.8 KiB
TypeScript
Raw Normal View History

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 {Environment} from 'nunjucks';
2020-04-22 15:52:17 +02:00
import * as util from "util";
import {WrappingError} from "../Utils";
2020-04-22 15:52:17 +02:00
import mjml2html from "mjml";
import {logger} from "../Logger";
import Controller from "../Controller";
import {ParsedUrlQueryInput} from "querystring";
2020-04-22 15:52:17 +02:00
export default class Mail {
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'),
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: {
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-04-22 15:52:17 +02:00
}
public static end(): void {
if (this.transporter) this.transporter.close();
2020-04-22 15:52:17 +02:00
}
public static parse(
environment: Environment,
template: string,
data: { [p: string]: unknown },
textOnly: boolean,
): string {
2020-04-22 15:52:17 +02:00
data.text = textOnly;
const nunjucksResult = environment.render(template, data);
2020-04-22 15:52:17 +02:00
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 = {},
) {
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;
2021-01-21 17:12:59 +01:00
this.data.mail_link = config.get<string>('public_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
2021-04-22 18:01:52 +02:00
logger.debug(`Send mail from ${this.options.from.address} to ${this.options.to}`);
2020-04-22 15:52:17 +02:00
// 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);
2020-04-22 15:52:17 +02:00
// 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;
2020-04-22 15:52:17 +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;
}
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 {
public constructor(message: string = 'An error occurred while sending mail.', cause?: Error) {
2020-04-22 15:52:17 +02:00
super(message, cause);
}
}