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 * as querystring from "querystring"; import Logger from "./Logger"; export function mailRoute(template: string): string { return `/mail/${template}`; } 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'), secure: config.get('mail.secure'), 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() { this.transporter.close(); } public static parse(template: string, data: any, textOnly: boolean): string { 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 = {}; private readonly data: { [p: string]: any }; constructor(template: MailTemplate, data: { [p: string]: any } = {}) { 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 { const results = []; for (const destEmail of to) { // Reset options this.options.html = this.options.text = undefined; // Set options this.options.to = destEmail; // Set data this.data.mail_subject = this.options.subject; this.data.mail_to = this.options.to; this.data.mail_link = `${config.get('public_url')}${mailRoute(this.template.template)}?${querystring.stringify(this.data)}`; // 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; private readonly subject: (data: any) => string; constructor(template: string, subject: (data: any) => string) { this._template = template; this.subject = subject; } public get template(): string { return this._template; } public getSubject(data: any): string { return 'Watch My Stream - ' + this.subject(data); } } class MailError extends WrappingError { constructor(message: string = 'An error occurred while sending mail.', cause?: Error) { super(message, cause); } }