UNPKG

@adaptivestone/framework-module-email

Version:
206 lines (205 loc) 7.9 kB
import fs from 'node:fs'; import path from 'node:path'; import * as url from 'node:url'; import { promisify } from 'node:util'; import nodemailer from 'nodemailer'; import stub from 'nodemailer-stub-transport'; import pug from 'pug'; import juice from 'juice'; import { convert } from 'html-to-text'; import merge from 'deepmerge'; import defaultMailConfig from "./config/mail.js"; const mailTransports = { stub, smtp: (data) => data, }; class Mail { /** * Adaptive stone framework application */ app = null; /** * Template full path */ template = ''; /** * Data to render in template. Object with value that available inside template * @type {object} */ templateData = {}; /** * Locale to render template * @type {string} */ locale = 'en'; /** * i18n object. Fallback if you have no real i18n object */ i18n = { t: (str) => str, language: 'en', // todo change it to config }; /** * Construct mail class * @param {TMinimalI18n} app * @param {string} template template name * @param {object} [templateData={}] data to render in template. Object with value that available inside template * @param {object} [i18n] data to render in template */ constructor(app, template, templateData = {}, i18n = null) { this.app = app; const dirname = url.fileURLToPath(new URL('.', import.meta.url)); this.template = template; if (!path.isAbsolute(this.template)) { // first go to emails folder if (fs.existsSync(`${this.app.foldersConfig.emails}/${path.basename(template)}`)) { this.template = `${this.app.foldersConfig.emails}/${path.basename(template)}`; } else if (this.app.frameworkFolder && fs.existsSync(`${this.app.frameworkFolder}/services/messaging/email/templates/${path.basename(template)}`)) { this.template = `${this.app.frameworkFolder}/services/messaging/email/templates/${path.basename(template)}`; } else if ( // now try to find in templates folder locally fs.existsSync(path.join(dirname, `/templates/${path.basename(template)}`))) { this.template = path.join(dirname, `/templates/${path.basename(template)}`); } else { // looks like we have no template. Using empty template this.template = path.join(dirname, `/templates/emptyTemplate`); this.app.logger.error(`Template '${template}' not found. Using 'emptyTemplate' as a fallback`); } } this.templateData = templateData; if (i18n) { this.i18n = i18n; this.locale = this.i18n?.language; } } /** * Render template * @param {object} type and fullpath * @param {object} templateData * @returns string */ async #renderTemplateFile(template, templateData = {}) { if (!template || !template.type || !template.fullPath) { return null; } switch (template.type) { case 'html': case 'text': case 'css': return fs.promises.readFile(template.fullPath, { encoding: 'utf8' }); case 'pug': { const compiledFunction = pug.compileFile(template.fullPath); return compiledFunction(templateData); } default: throw new Error(`Template type ${template.type} is not supported`); } } /** * Render template * @return {Promise} */ async renderTemplate() { const files = await fs.promises.readdir(this.template); const templates = {}; for (const file of files) { const [name, extension] = file.split('.'); templates[name] = { type: extension, fullPath: path.join(this.template, file), }; } if (!templates.html || !templates.subject) { throw new Error('Template HTML and Subject must be provided. Please follow documentation for details https://framework.adaptivestone.com/docs/email'); } const mailConfig = Mail.getConfig(this.app); const templateDataToRender = { locale: this.locale, t: this.i18n.t.bind(this.i18n), ...mailConfig.globalVariablesToTemplates, ...this.templateData, }; const [htmlRendered, subjectRendered, textRendered, extraCss] = await Promise.all([ this.#renderTemplateFile(templates.html, templateDataToRender), this.#renderTemplateFile(templates.subject, templateDataToRender), this.#renderTemplateFile(templates.text, templateDataToRender), this.#renderTemplateFile(templates.style), ]); if (!htmlRendered) { throw new Error('HTML template cant be rendered'); } // @ts-ignore juice.tableElements = ['TABLE']; const juiceResourcesAsync = promisify(juice.juiceResources); const inlinedHTML = await juiceResourcesAsync(htmlRendered, { preserveImportant: true, webResources: mailConfig.webResources, extraCss: extraCss ?? '', }); return { htmlRaw: htmlRendered ?? '', subject: subjectRendered ?? '', text: textRendered ?? '', inlinedHTML, }; } /** * Send email * @param {string | Array<string>} to email send to * @param {string} [from = mailConfig.from] * @param {object} [aditionalNodemailerOptions = {}] additional option to nodemailer * @return {Promise} */ async send(to, from = '', aditionalNodemailerOptions = {}) { const { subject, text, inlinedHTML } = await this.renderTemplate(); return Mail.sendRaw(this.app, to, subject, inlinedHTML, text, from, aditionalNodemailerOptions); } /** * Send provided text (html) to email. Low level function. All data should be prepared before sending (like inline styles) * @param {TMinimalI18n} app application * @param {string | Array<string>} to send to * @param {string} subject email topic * @param {string} html hmlt body of emain * @param {string} [text] if not provided will be generated from html string * @param {string} [from = mailConfig.from] from. If not provided will be grabbed from config * @param {object} [additionalNodeMailerOption = {}] any otipns to pass to nodemailer https://nodemailer.com/message/ */ static async sendRaw(app, to, subject, html, text = '', from = '', additionalNodeMailerOption = {}) { if (!app || !to || !subject || !html) { throw new Error('App, to, subject and html is required fields.'); } const mailConfig = Mail.getConfig(app); if (!from) { from = mailConfig.from; } if (!text) { text = convert(html, { selectors: [{ selector: 'img', format: 'skip' }], }); } const transportConfig = mailConfig.transports[mailConfig.transport]; const transport = mailTransports[mailConfig.transport]; const transporter = nodemailer.createTransport(transport(transportConfig)); return transporter.sendMail({ from, to, subject, text, html, ...additionalNodeMailerOption, }); } /** * Get final config. Method to get final config. * @param {TMinimalI18n} app application */ static getConfig(app) { const mailConfig = app.getConfig('mail'); return merge(defaultMailConfig, mailConfig || {}); } } export default Mail;