diff --git a/index.d.ts b/index.d.ts index a826ecb..0621eff 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,7 +1,15 @@ -declare module '@autotelic/fastify-mail' { - import { FastifyInstance, FastifyPluginCallback } from 'fastify'; - import { Transporter } from 'nodemailer'; +import { FastifyPluginCallback } from 'fastify'; +import { Transporter } from 'nodemailer'; +type FastifyMail = FastifyPluginCallback; + +declare module 'fastify' { + interface FastifyInstance { + mail: fastifyMail.FastifyMailDecorator; + } +} + +declare namespace fastifyMail { // Define the options for the plugin export interface FastifyMailOptions { pov?: { @@ -13,7 +21,7 @@ declare module '@autotelic/fastify-mail' { } // Define the shape of the mail decorator - interface FastifyMailDecorator { + export interface FastifyMailDecorator { sendMail: (message: MailMessage, opts?: SendMailOptions) => Promise; createMessage: (message: MailMessage, templatePath: string, context: any) => Promise; validateMessage: (message: MailMessage) => string[]; @@ -46,5 +54,11 @@ declare module '@autotelic/fastify-mail' { // The exported plugin function const fastifyMail: FastifyPluginCallback; - export default fastifyMail; + export { fastifyMail as default }; } + +declare function fastifyMail( + ...params: Parameters +): ReturnType; + +export = fastifyMail; diff --git a/index.js b/index.js index 5e9c598..a6e9a55 100644 --- a/index.js +++ b/index.js @@ -56,39 +56,51 @@ const fastifyMail = async (fastify, opts) => { // Creates the message object that will be sent to nodemailer. // It will either render the templates or use the data in the message object as is createMessage: async function (message, templatePath, context) { - const from = message.from - const to = message.to - const subject = message.subject - const replyTo = message.replyTo - const cc = message.cc - const bcc = message.bcc - const html = message.html - const text = message.text - - const [ - renderedHtml, - renderedText - ] = await Promise.all([ - await renderTemplate('html'), - await renderTemplate('text') - ]) - - return { - from, - to, - cc, - bcc, - replyTo, - subject, - html: renderedHtml || html, - text: renderedText || text + const formattedMessage = { + from: message.from, + to: message.to, + subject: message.subject, + replyTo: message.replyTo, + cc: message.cc, + bcc: message.bcc, + html: message.html, + text: message.text } - // renders a template with the given context based on the templateName which - // should be found in the path provided by templates. Returns "" if the promise is rejected. + if (templatePath) { + const [ + { template: renderedHtml, error: htmlError }, + { template: renderedText, error: textError } + ] = await Promise.all([ + await renderTemplate('html'), + await renderTemplate('text') + ]) + + if (!renderedHtml && !renderedText) { + fastify.log.error(`fastify-mail: ${htmlError}`) + fastify.log.error(`fastify-mail: ${textError}`) + } + + if (renderedHtml) { + formattedMessage.html = renderedHtml + } + + if (renderedText) { + formattedMessage.text = renderedText + } + } + + return formattedMessage + + // renders a template with the given context based on the templateName & templatePath, + // if it fails to render the template, it returns an error message instead. async function renderTemplate (templateName) { - return await fastify[propertyName](join(templatePath, templateName), context) - .catch(() => { return null }) + try { + const template = await fastify[propertyName](join(templatePath, templateName), context) + return { template } + } catch (error) { + return { error: error.message } + } } }, // validates the message object includes to, from, subject and returns an error message if it does not diff --git a/index.test.js b/index.test.js index eb29298..3a0f3bb 100644 --- a/index.test.js +++ b/index.test.js @@ -68,7 +68,7 @@ test('view decorator does not exist if the engine is not provided', async ({ tea notOk(fastify.hasDecorator('view')) }) -test('throws an error if point-of-view is not registered', async ({ teardown, notOk, rejects, ok, equal }) => { +test('throws an error if point-of-view is not registered', async ({ teardown, notOk, rejects }) => { teardown(() => fastify.close()) const fastify = Fastify() fastify.register(fastifyMail, { transporter: { jsonTransport: true } }) @@ -77,7 +77,7 @@ test('throws an error if point-of-view is not registered', async ({ teardown, no notOk(fastify.hasDecorator('view')) }) -test('throws an error if an invalid transporter is given', async ({ teardown, rejects, ok, equal }) => { +test('throws an error if an invalid transporter is given', async ({ teardown, rejects }) => { teardown(() => fastify.close()) const fastify = Fastify() fastify.register(fastifyMail, { pov: { engine: { nunjucks } }, transporter: 'error' }) @@ -133,6 +133,10 @@ test('fastify-mail uses string variables (for text and html) when a template is const fastify = Fastify() fastify.register(require('@fastify/view'), povConfig) + + const loggedErrors = [] + fastify.log.error = (msg) => { loggedErrors.push(msg) } + fastify.after(() => { fastify.register(fastifyMail, { pov: { propertyName: 'foo' }, transporter: { jsonTransport: true } }) }) @@ -144,6 +148,7 @@ test('fastify-mail uses string variables (for text and html) when a template is ok(fastify.hasDecorator('foo')) same(sendMailStub.args[0], [testMessage]) + equal(loggedErrors.length, 0) equal(sendMailStub.args.length, 1) }) @@ -166,6 +171,10 @@ test('fastify-mail uses text template when available but defaults to provided ht const fastify = Fastify() fastify.register(require('@fastify/view'), povConfig) + + const loggedErrors = [] + fastify.log.error = (msg) => { loggedErrors.push(msg) } + fastify.after(() => { fastify.register(fastifyMail, { pov: { propertyName: 'foo' }, transporter: { jsonTransport: true } }) }) @@ -177,6 +186,7 @@ test('fastify-mail uses text template when available but defaults to provided ht ok(fastify.hasDecorator('foo')) same(sendMailStub.args[0][0].html, testHtml) + equal(loggedErrors.length, 0) equal(sendMailStub.args.length, 1) }) @@ -199,6 +209,10 @@ test('fastify-mail uses html template when available but defaults to provided te const fastify = Fastify() fastify.register(require('@fastify/view'), povConfig) + + const loggedErrors = [] + fastify.log.error = (msg) => { loggedErrors.push(msg) } + fastify.after(() => { fastify.register(fastifyMail, { pov: { propertyName: 'foo' }, transporter: { jsonTransport: true } }) }) @@ -211,9 +225,44 @@ test('fastify-mail uses html template when available but defaults to provided te ok(fastify.hasDecorator('foo')) same(sendMailStub.args[0][0].html, testHtml) same(sendMailStub.args[0][0].text, 'This is a plain text email message.') + equal(loggedErrors.length, 0) equal(sendMailStub.args.length, 1) }) +test('fastify-mail will throw errors if templatePath is defined, but does not exist', async ({ teardown, testdir, equal }) => { + teardown(() => { + fastify.close() + sendMailStub.restore() + }) + + const testTemplates = testdir({}) + + const povConfig = { + propertyName: 'foo', + engine: { nunjucks }, + includeViewExtension: true, + options: { filename: resolve('templates') } + } + + const fastify = Fastify() + fastify.register(require('@fastify/view'), povConfig) + + const loggedErrors = [] + fastify.log.error = (msg) => { loggedErrors.push(msg) } + + fastify.after(() => { + fastify.register(fastifyMail, { pov: { propertyName: 'foo' }, transporter: { jsonTransport: true } }) + }) + await fastify.ready() + + const sendMailStub = sinon.stub(fastify.nodemailer, 'sendMail') + + await fastify.mail.sendMail(testMessage, { templatePath: relative(__dirname, testTemplates), context: testContext }) + + equal(loggedErrors[0], 'fastify-mail: template not found: .tap/fixtures/.-index.test.js-fastify-mail-will-throw-errors-if-templatePath-is-defined-but-does-not-exist/html.njk') + equal(loggedErrors[1], 'fastify-mail: template not found: .tap/fixtures/.-index.test.js-fastify-mail-will-throw-errors-if-templatePath-is-defined-but-does-not-exist/text.njk') +}) + test('fastify.mail.sendMail calls nodemailer.sendMail with correct arguments', async ({ teardown, testdir, fixture, same, equal }) => { teardown(() => { fastify.close()