From ba8ca4aeb30e09444eee345b1fb5ae290edb62aa Mon Sep 17 00:00:00 2001 From: Sebastian Landwehr Date: Sun, 13 Feb 2022 13:44:49 +0000 Subject: [PATCH 1/8] feat: allow arbitrary emails on server side --- package.json | 7 +- src/index.js | 56 +- src/index.spec.js | 447 ++++++--- src/options.js | 0 src/options.js.template | 1 + src/{plugin.js => plugin.client.js} | 4 +- src/plugin.server.js | 9 + src/send.js | 33 + yarn.lock | 1336 +++++++++++++++++---------- 9 files changed, 1244 insertions(+), 649 deletions(-) create mode 100644 src/options.js create mode 100644 src/options.js.template rename src/{plugin.js => plugin.client.js} (63%) create mode 100644 src/plugin.server.js create mode 100644 src/send.js diff --git a/package.json b/package.json index 820f520..bfe0f07 100644 --- a/package.json +++ b/package.json @@ -44,18 +44,17 @@ }, "devDependencies": { "@dword-design/base": "^8.0.0", - "@dword-design/proxyquire": "^2.0.0", "@dword-design/puppeteer": "^5.0.0", "@dword-design/tester": "^2.0.0", - "@dword-design/tester-plugin-nodemailer-mock": "^1.0.0", "@dword-design/tester-plugin-puppeteer": "^2.0.0", + "@dword-design/tester-plugin-tmp-dir": "^2.1.5", "@nuxtjs/axios": "^5.13.1", "axios": "^0.25.0", "depcheck-package-name": "^2.0.0", - "nodemailer-mock": "^1.5.4", + "mailparser": "^3.4.0", "nuxt": "^2.15.3", "output-files": "^2.0.0", - "with-local-tmp-dir": "^4.0.0" + "smtp-server": "^3.9.0" }, "engines": { "node": ">=12" diff --git a/src/index.js b/src/index.js index 6f6a1fe..25fb6d1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,49 +1,30 @@ -import { findIndex, omit, some } from '@dword-design/functions' +import { some } from '@dword-design/functions' import express from 'express' import nodemailer from 'nodemailer' import nuxtPushPlugins from 'nuxt-push-plugins' +import P from 'path' + +import send from './send' export default function (moduleOptions) { - const options = { ...this.options.mail, ...moduleOptions } + const options = { message: [], ...this.options.mail, ...moduleOptions } if (!options.smtp) { throw new Error('SMTP config is missing.') } - if ( - (Array.isArray(options.message) && options.message.length === 0) || - !options.message - ) { - throw new Error('You have to provide at least one config.') - } - if (!Array.isArray(options.message)) { + if (typeof options.message === 'object' && !Array.isArray(options.message)) { options.message = [options.message] } if (options.message |> some(c => !c.to && !c.cc && !c.bcc)) { throw new Error('You have to provide to/cc/bcc in all configs.') } - const app = express() - const transport = nodemailer.createTransport(options.smtp) + + const app = express() app.use(express.json()) app.post('/send', async (req, res) => { - req.body = { config: 0, ...req.body } try { - if (typeof req.body.config === 'string') { - const configIndex = - options.message |> findIndex(_ => _.name === req.body.config) - if (configIndex === -1) { - throw new Error( - `Message config with name '${req.body.config}' not found.` - ) - } - req.body.config = configIndex - } else if (!options.message[req.body.config]) { - throw new Error(`Message config not found at index ${req.body.config}.`) - } - await transport.sendMail({ - ...(req.body |> omit(['config', 'to', 'cc', 'bcc'])), - ...(options.message[req.body.config] |> omit(['name'])), - }) + await send(req.body, { ...options, forceConfig: true, transport }) } catch (error) { return res.status(400).send(error.message) } @@ -51,5 +32,22 @@ export default function (moduleOptions) { return res.sendStatus(200) }) this.addServerMiddleware({ handler: app, path: '/mail' }) - nuxtPushPlugins(this, require.resolve('./plugin')) + this.addTemplate({ + fileName: P.join('nuxt-mail', 'options.js'), + options, + src: require.resolve('./options.js.template'), + }) + this.addTemplate({ + fileName: P.join('nuxt-mail', 'send.js'), + options, + src: require.resolve('./send'), + }) + nuxtPushPlugins(this, { + fileName: P.join('nuxt-mail', 'plugin.client.js'), + src: require.resolve('./plugin.client'), + }) + nuxtPushPlugins(this, { + fileName: P.join('nuxt-mail', 'plugin.server.js'), + src: require.resolve('./plugin.server'), + }) } diff --git a/src/index.spec.js b/src/index.spec.js index 1ff16cf..c544e6c 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -1,58 +1,165 @@ -import { endent, mapValues } from '@dword-design/functions' +import { endent, includes, map, pick } from '@dword-design/functions' import tester from '@dword-design/tester' -import testerPluginNodemailerMock from '@dword-design/tester-plugin-nodemailer-mock' import testerPluginPuppeteer from '@dword-design/tester-plugin-puppeteer' +import testerPluginTmpDir from '@dword-design/tester-plugin-tmp-dir' import axios from 'axios' import packageName from 'depcheck-package-name' -import nodemailerMock from 'nodemailer-mock' +import { simpleParser } from 'mailparser' import { Builder, Nuxt } from 'nuxt' import outputFiles from 'output-files' -import withLocalTmpDir from 'with-local-tmp-dir' +import { SMTPServer } from 'smtp-server' -const runTest = config => { - config = { options: {}, test: () => {}, ...config } +const testerPluginEmail = options => { + options = { mapEmail: x => x, port: 3001, ...options } - return function () { - return withLocalTmpDir(async () => { - await outputFiles({ - 'modules/module.js': endent` - import proxyquire from '${packageName`@dword-design/proxyquire`}' - import nodemailerMock from '${packageName`nodemailer-mock`}' - - export default proxyquire('./../../src', { - nodemailer: nodemailerMock - }) - - `, - ...config.files, + return { + after() { + this.smtpServer.close() + }, + before() { + this.smtpServer = new SMTPServer({ + authOptional: true, + onData: async (stream, session, callback) => { + const message = options.mapEmail(await simpleParser(stream)) + this.sentEmails.push(message) + callback() + }, }) + this.smtpServer.listen(options.port) + }, + beforeEach() { + this.sentEmails = [] + }, + } +} - const nuxt = new Nuxt({ - createRequire: 'native', - dev: false, - modules: [ - packageName`@nuxtjs/axios`, - ['~/modules/module', config.options], - ], - }) - if (config.error) { - await expect(new Builder(nuxt).build()).rejects.toThrow(config.error) - } else { - await new Builder(nuxt).build() - await nuxt.listen() - try { - await config.test.call(this) - } finally { - await nuxt.close() - } +const waitForError = (page, errorMessage) => + new Promise(resolve => + page.on('console', async msg => { + const messages = await Promise.all( + msg.args() + |> map(arg => + arg + .executionContext() + .evaluate( + _arg => (_arg instanceof Error ? _arg.message : undefined), + arg + ) + ) + ) + if (messages |> includes(errorMessage)) { + resolve() } }) - } -} + ) export default tester( { - bcc: { + 'client side: no config': { + files: { + 'pages/index.vue': endent` + + + + + `, + }, + options: { + smtp: { + port: 3001, + tls: { + rejectUnauthorized: false, + }, + }, + }, + async test() { + await this.page.goto('http://localhost:3000') + + const button = await this.page.waitForSelector('button') + await button.click() + await waitForError(this.page, 'Message config not found at index 0.') + expect(this.sentEmails).toEqual([]) + }, + }, + 'client side: valid': { + files: { + 'pages/index.vue': endent` + + + + + `, + }, + options: { + message: { to: 'johndoe@gmail.com' }, + smtp: { + port: 3001, + tls: { + rejectUnauthorized: false, + }, + }, + }, + async test() { + await this.page.goto('http://localhost:3000') + + const button = await this.page.waitForSelector('button') + await button.click() + await this.page.waitForSelector('button.sent') + expect(this.sentEmails).toEqual([ + { + from: 'john@doe.de', + subject: 'Incredible', + text: 'This is an incredible test message\n', + to: 'johndoe@gmail.com', + }, + ]) + }, + }, + 'no recipients': { + error: 'You have to provide to/cc/bcc in all configs.', + options: { + message: {}, + smtp: { + port: 3001, + tls: { + rejectUnauthorized: false, + }, + }, + }, + }, + 'no smtp config': { + error: 'SMTP config is missing.', + }, + 'server side: bcc': { files: { 'pages/index.vue': endent`