import config from "config"; import {Express} from "express"; import nodemailer, {SentMessageInfo, Transporter} from "nodemailer"; import util from "util"; import ApplicationComponent from "../ApplicationComponent.js"; import {route} from "../common/Routing.js"; import MailViewEngine from "../frontend/MailViewEngine.js"; import {logger} from "../Logger.js"; import Mail from "../mail/Mail.js"; import MailError from "../mail/MailError.js"; import SecurityError from "../SecurityError.js"; import LazyLocalsCoreComponent from "./core/LazyLocalsCoreComponent.js"; export default class MailComponent extends ApplicationComponent { private transporter?: Transporter; private readonly additionalLocals: Record = {}; public constructor( private readonly viewEngine: MailViewEngine, ) { super(); } public async checkSecuritySettings(): Promise { if (!config.get('mail.secure')) { throw new SecurityError('Cannot set mail.secure (starttls) to false'); } if (config.get('mail.allow_invalid_tls')) { throw new SecurityError('Cannot set mail.allow_invalid_tls (ignore tls failure) to true'); } } 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) { if (e instanceof Error) { throw new MailError('Connection to mail service unsuccessful.', e); } else { throw e; } } logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`); }); await this.viewEngine.setup(app, false, this.getApp().as(LazyLocalsCoreComponent)); } public async stop(): Promise { 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 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 const urlLocals = mail.getData(); urlLocals.mail_subject = options.subject; urlLocals.mail_to = options.to; urlLocals.mail_link = config.get('app.public_url') + route('mail', [template.template], urlLocals); const locals = {...this.additionalLocals, ...urlLocals}; // 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; } public setAdditionalLocal(key: string, value: unknown): void { this.additionalLocals[key] = value; } private getTransporter(): Transporter { if (!this.transporter) throw new MailError('Mail system was not prepared.'); return this.transporter; } }