├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bin └── nextmail.js ├── cli ├── nextmail-build.js ├── nextmail-dev.js ├── nextmail-send.js └── nextmail-start.js ├── examples ├── latest │ ├── .nowignore │ ├── emails │ │ ├── initial-props.js │ │ ├── nested │ │ │ └── nested-demo.js │ │ ├── payload-only.js │ │ ├── src │ │ │ ├── components │ │ │ │ └── Header.js │ │ │ └── config.js │ │ └── with-image.js │ ├── index.js │ ├── nextmail.config.js │ ├── now.json │ ├── package-lock.json │ ├── package.json │ └── static │ │ └── bicycle.jpeg ├── package-consumer │ ├── package-lock.json │ ├── package.json │ └── render.test.js └── package │ ├── emails │ └── demo.js │ ├── index.js │ ├── package-lock.json │ └── package.json ├── index.js ├── lib ├── Renderer.js ├── babel-preset.js ├── build.js ├── config.js ├── debug.js ├── getEmails.js ├── requireTemplate.js └── sendEmail.js ├── mjml-react.js ├── package-lock.json ├── package.json └── server ├── handleRequest.js ├── routes ├── _index.js ├── preview.js ├── routes.js └── static.js └── startServer.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb'], 3 | plugins: ['cypress'], 4 | env: { 5 | "cypress/globals": true 6 | }, 7 | rules: { 8 | 'global-require': 'off', 9 | 'import/no-dynamic-require': 'off', 10 | 'max-len': ['error', 150, 2, { 11 | ignoreUrls: true, 12 | ignoreComments: false, 13 | ignoreStrings: true, 14 | ignoreTemplateLiterals: true, 15 | }], 16 | 'no-underscore-dangle': 'off', 17 | 'no-use-before-define': ['error', { functions: false, classes: false }], 18 | 'react/jsx-filename-extension': 'off', 19 | 'react/prop-types': 'off' 20 | }, 21 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .nextmail 64 | 65 | .DS_Store 66 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sean Connolly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Nextmail** makes it easy to leverage `React` and `MJML` to craft custom, dynamic email templates. 2 | 3 | - Declarative, component-based model with [React](https://reactjs.org/) 4 | - Responsive out of the box with [MJML](https://mjml.io/) 5 | - Iterate quickly with browser previews 6 | - Unit test with `jest` or however you choose, the same way you test your front end 7 | - Fetch asynchronous data with `getInitialProps`, inspired by [Next.js](https://nextjs.org/) 8 | - File based API, inspired by the `pages` model in [Next.js](https://nextjs.org/) 9 | 10 | ## How to use 11 | 12 | ### Setup 13 | 14 | Install it: 15 | 16 | ```bash 17 | npm install --save nextmail react react-dom 18 | ``` 19 | 20 | and add a script to your package.json like this: 21 | 22 | ```json 23 | { 24 | "scripts": { 25 | "dev": "nextmail dev", 26 | "build": "nextmail build" 27 | } 28 | } 29 | ``` 30 | 31 | After that, the file system is the main API. Every `.js` file becomes an email that gets automatically processed and rendered. 32 | 33 | Populate `./emails/demo.js` inside your project: 34 | 35 | ```jsx 36 | import React from 'react'; 37 | import { 38 | Mjml, MjmlBody, MjmlColumn, MjmlSection, MjmlText, 39 | } from 'nextmail/mjml-react'; 40 | 41 | function Demo() { 42 | return ( 43 | 44 | 45 | 46 | 47 | Welcome to Nextmail! 48 | 49 | 50 | 51 | 52 | ); 53 | } 54 | 55 | export default Demo; 56 | ``` 57 | 58 | and then just run `npm run dev` and go to `http://localhost:6100` to view a preview. 59 | 60 | ## API 61 | 62 | ### `Renderer` 63 | 64 | Generate email data with a single function call. 65 | ```javascript 66 | const renderer = new Renderer(); 67 | const { html, text, subject } = await renderEmail('demo', {}); 68 | ``` 69 | 70 | **Arguments** 71 | - **template** (string): The name of the email template. 72 | - **payload** (Object): Any dynamic data you need to interpolate in the email template (e.g. a user's name). 73 | 74 | **Returns** 75 | An `Object` with the following properties: 76 | - **html** (string): The fully rendered html. 77 | - **text** (string): The converted text of the email. `nextmail` uses [html-to-text](https://www.npmjs.com/package/html-to-text) for this conversion. 78 | - **subject** (string): The fully rendered subject line. 79 | 80 | ## Fetch asynchronous data with `getInitialProps` 81 | 82 | In similar fashion to [Next.js](https://nextjs.org), you can implement `getInitialProps` to asynchronously fetch data at render time. 83 | 84 | ```jsx 85 | function Demo() { 86 | ... 87 | } 88 | 89 | // payload = { userId: 1 } 90 | Demo.getInitialProps = async ({ payload }) => { 91 | const { userId } = payload; 92 | const resp = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`); 93 | return { user: resp.data }; 94 | }; 95 | ``` 96 | 97 | **Be careful** with this functionality. In many systems, emails are sent asynchronously. The underlying data store can change from when the email was triggered to when it is rendered. If you need the data to be locked in when the email is triggered, be sure that makes its way into the `payload`. If you are okay with using data that is later updated (e.g. the user changes their name), you can use `getInitialProps`. 98 | 99 | ## Render email subjects with `getSubject` 100 | 101 | Implement `getSubject` to hydrate the `subject` of the response to `renderEmail`. `getSubject` is called after `getInitialProps` and will have access to the initial props. 102 | 103 | ```jsx 104 | function Demo() { 105 | ... 106 | } 107 | 108 | // props = { user: { name: 'Leanne Graham' } } 109 | Demo.getSubject = async ({ props }) => { 110 | const { user } = props; 111 | return `${user.name}! Act now!`; // Leanne Graham! Act now! 112 | } 113 | ``` 114 | 115 | ## Custom components 116 | 117 | Custom `React` components allow you to implement reusable elements in your email templates. Since `Nextmail` is compiling emails in your `emails` directory, you need to place your components in a special directory called `src`. 118 | ``` 119 | /emails 120 | /src 121 | Header.js <---- This will compile 122 | demo.js 123 | /other 124 | NotCompiled.js <---- This will not 125 | ``` 126 | 127 | **Q**: Why not just include `Header.js` inside the `emails` directory? 128 | 129 | **A**: `Nextmail` needs the ability to distinguish email templates from other components. For example, the preview index lists all available email templates to preview. 130 | 131 | ## Previews 132 | 133 | The `nextmail dev` script allows you to view a preview of your email template. 134 | 135 | The preview route follows this format: `/preview/:format/:template` 136 | - `format` can be either `html` or `text` 137 | - `template` is the file path for your template, e.g. if your file is found at `./emails/demo.js`, the `template` would be `demo` 138 | 139 | ### Test payloads with query strings 140 | If your email template requires payload data, you can add the payload via query string: 141 | `http://localhost:6100/html/demo?firstName=Lisa` -> `{ firstName: 'Lisa'}` 142 | 143 | Nested objects and arrays are also supported. See [qs](https://www.npmjs.com/package/qs) for formatting options. 144 | 145 | ## Configuration 146 | You can configure `Nextmail` by adding a `nextmail.config.js` file to the root of your project directory and exporting a configuration object. 147 | ```javascript 148 | // nextmail.config.js 149 | module.exports = { ... }; 150 | ``` 151 | 152 | ### Configuring nodemailer for test sends 153 | Configure options for `nodemailer.createTransport(...)`. See [Sending Previews](#sending-previews). 154 | 155 | ```javascript 156 | // nextmail.config.js 157 | module.exports = { 158 | send: { 159 | smtpConfig: { 160 | host: 'smtp.mailtrap.io', 161 | port: 2525, 162 | auth: { 163 | user: '****', 164 | pass: '****', 165 | }, 166 | }, 167 | } 168 | }; 169 | ``` 170 | ### Configuring mailOptions for test sends 171 | Override the `mailOptions` sent to nodemailer's `transport.sendMail(...)` 172 | 173 | ```javascript 174 | // nextmail.config.js 175 | module.exports = { 176 | send: { 177 | mailOptions: { 178 | from: 'from@example.com', 179 | to: 'to@example.com', 180 | }, 181 | } 182 | }; 183 | ``` 184 | 185 | ### Configuring payloads for testing 186 | Test payloads are used when [Sending Previews](#sending-previews) and are also used to dynamically build links in the preview index page for convenience. 187 | 188 | ```javascript 189 | // nextmail.config.js 190 | 191 | // This configures 2 test payloads for the "demo" email 192 | module.exports = { 193 | payloads: { 194 | demo: { 195 | default: { 196 | userId: 1, 197 | }, 198 | user2: { 199 | userId: 2, 200 | }, 201 | }, 202 | }, 203 | }; 204 | ``` 205 | 206 | ## Sending Previews 207 | [Mailtrap](https://mailtrap.io) is an excellent service that helps you capture test emails. Once [configured](#configuring-nodemailer-for-test-sends), you can add an additional script to your `package.json`: `"send": "nodemailer send"`. Then run one of the following commands. 208 | ```bash 209 | # Sends the demo.js email with a default payload 210 | npm run send demo 211 | 212 | # Sends the demo.js email with the named "user2" payload 213 | npm run send demo user2 214 | ``` 215 | 216 | ## Static assets 217 | Add static assets to the `/static` directory: 218 | ``` 219 | /emails 220 | demo.js 221 | /static 222 | /bicycle.jpeg 223 | ``` 224 | 225 | Then you can reference them in your components: 226 | ``` 227 | 228 | ``` 229 | 230 | For production, you will need to publish your assets to a known host and use absolute URLs in your components. One way you can do this is to add a `config.js` file under your `src` directory. 231 | ``` 232 | const isProd = process.env.NODE_ENV === 'production'; 233 | 234 | export default { 235 | assetPrefix: isProd ? 'https://nextmail-latest.now.sh' : '', 236 | }; 237 | 238 | ``` 239 | 240 | Then reference the `config` in your component. 241 | ``` 242 | import config from '../config'; 243 | function WithImage() { 244 | ... 245 | 246 | ... 247 | } 248 | ``` 249 | 250 | See the [with-image](/examples/latest/emails/with-image.js) example. 251 | 252 | #### Exporting `nextmail` templates as a package 253 | 254 | You can build and export email templates as an `npm` package. In order to do this, you need to tell `nextmail` where to find the build templates with the `rootDirectory` option. 255 | ```javascript 256 | const path = require('path'); 257 | const { Renderer } = require('nextmail'); 258 | 259 | const renderer = new Renderer({ rootDirectory: path.resolve(__dirname) }); 260 | 261 | async function renderEmail(...args) { 262 | // You can also implement custom post-processing logic here too. 263 | return renderer.renderEmail(...args); 264 | } 265 | 266 | module.exports = { renderEmail }; 267 | ``` 268 | 269 | ## Debugging 270 | To see verbose log output, including captured payload, initial props, etc. when developing: `DEBUG=nextmail npm run dev` 271 | -------------------------------------------------------------------------------- /bin/nextmail.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { argv } = require('yargs'); 3 | 4 | (async () => { 5 | try { 6 | const command = argv._[0]; 7 | const forwardedArgs = argv._.slice(1); 8 | 9 | if (command === 'build') { 10 | await require('../cli/nextmail-build')(forwardedArgs); 11 | } else if (command === 'start') { 12 | await require('../cli/nextmail-start')(forwardedArgs); 13 | } else if (command === 'send') { 14 | await require('../cli/nextmail-send')(forwardedArgs); 15 | } else { 16 | await require('../cli/nextmail-dev')(forwardedArgs); 17 | } 18 | } catch (err) { 19 | console.error(err); // eslint-disable-line no-console 20 | } 21 | })(); 22 | -------------------------------------------------------------------------------- /cli/nextmail-build.js: -------------------------------------------------------------------------------- 1 | const { build } = require('../lib/build'); 2 | 3 | async function nextmailBuild() { 4 | await build(); 5 | } 6 | 7 | module.exports = nextmailBuild; 8 | -------------------------------------------------------------------------------- /cli/nextmail-dev.js: -------------------------------------------------------------------------------- 1 | const { buildWatch } = require('../lib/build'); 2 | const startServer = require('../server/startServer'); 3 | 4 | async function nextmailDev() { 5 | await buildWatch(); 6 | startServer(); 7 | } 8 | 9 | module.exports = nextmailDev; 10 | -------------------------------------------------------------------------------- /cli/nextmail-send.js: -------------------------------------------------------------------------------- 1 | const sendEmail = require('../lib/sendEmail'); 2 | 3 | async function nextmailSend(args) { 4 | return sendEmail(args); 5 | } 6 | 7 | module.exports = nextmailSend; 8 | -------------------------------------------------------------------------------- /cli/nextmail-start.js: -------------------------------------------------------------------------------- 1 | const startServer = require('../server/startServer'); 2 | 3 | function nextmailDev() { 4 | startServer(); 5 | } 6 | 7 | module.exports = nextmailDev; 8 | -------------------------------------------------------------------------------- /examples/latest/.nowignore: -------------------------------------------------------------------------------- 1 | !now.json 2 | !static -------------------------------------------------------------------------------- /examples/latest/emails/initial-props.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { 4 | Mjml, 5 | MjmlHead, 6 | MjmlTitle, 7 | MjmlBody, 8 | MjmlSection, 9 | MjmlColumn, 10 | MjmlText, 11 | } from 'nextmail/mjml-react'; 12 | import Header from './src/components/Header'; 13 | 14 | function InitialProps(props) { 15 | const { user } = props; 16 | 17 | return ( 18 | 19 | 20 | Nextmail Initial Props Demo 21 | 22 | 23 | 24 | 25 |
26 | 27 | {`Hello ${user.name}`} 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | InitialProps.getInitialProps = async ({ payload }) => { 37 | const { userId } = payload; 38 | const resp = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`); 39 | return { user: resp.data }; 40 | }; 41 | 42 | InitialProps.getSubject = async ({ payload, props }) => { 43 | const { userId } = payload; 44 | const { user } = props; 45 | 46 | return `${user.name}(${userId}), read this email!`; 47 | }; 48 | 49 | export default InitialProps; 50 | -------------------------------------------------------------------------------- /examples/latest/emails/nested/nested-demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Mjml, 5 | MjmlHead, 6 | MjmlTitle, 7 | MjmlBody, 8 | MjmlSection, 9 | MjmlColumn, 10 | MjmlText, 11 | } from 'mjml-react'; 12 | 13 | function NestedDemo() { 14 | return ( 15 | 16 | 17 | Nested Demo 18 | 19 | 20 | 21 | 22 | Nested Demo 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export default NestedDemo; 31 | -------------------------------------------------------------------------------- /examples/latest/emails/payload-only.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Mjml, 4 | MjmlHead, 5 | MjmlTitle, 6 | MjmlBody, 7 | MjmlSection, 8 | MjmlColumn, 9 | MjmlText, 10 | } from 'nextmail/mjml-react'; 11 | import Header from './src/components/Header'; 12 | 13 | function Demo(props) { 14 | const { firstName } = props; 15 | 16 | return ( 17 | 18 | 19 | Payload Only Demo 20 | 21 | 22 | 23 | 24 |
25 | 26 | {`Hello ${firstName}`} 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export default Demo; 36 | -------------------------------------------------------------------------------- /examples/latest/emails/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MjmlText } from 'nextmail/mjml-react'; 3 | 4 | const Header = () => ( 5 | 6 | Header 7 | 8 | ); 9 | 10 | export default Header; 11 | -------------------------------------------------------------------------------- /examples/latest/emails/src/config.js: -------------------------------------------------------------------------------- 1 | const isProd = process.env.NODE_ENV === 'production'; 2 | 3 | export default { 4 | assetPrefix: isProd ? 'https://nextmail-latest.now.sh' : '', 5 | }; 6 | -------------------------------------------------------------------------------- /examples/latest/emails/with-image.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Mjml, 4 | MjmlHead, 5 | MjmlTitle, 6 | MjmlBody, 7 | MjmlSection, 8 | MjmlColumn, 9 | MjmlImage, 10 | } from 'nextmail/mjml-react'; 11 | import config from './src/config'; 12 | 13 | function WithImage() { 14 | return ( 15 | 16 | 17 | With Image 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | WithImage.getSubject = () => 'With Image'; 31 | 32 | export default WithImage; 33 | -------------------------------------------------------------------------------- /examples/latest/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); 3 | const { Renderer } = require('nextmail'); 4 | 5 | const rootDirectory = path.resolve(__dirname); 6 | const renderer = new Renderer({ rootDirectory }); 7 | 8 | (async () => { 9 | // Sample data will come from here: https://jsonplaceholder.typicode.com/users/1 10 | console.log('renderEmail', await renderer.renderEmail('demo', { userId: 1 })); 11 | })(); 12 | -------------------------------------------------------------------------------- /examples/latest/nextmail.config.js: -------------------------------------------------------------------------------- 1 | const { MAILTRAP_USER, MAILTRAP_PASSWORD } = process.env; 2 | 3 | module.exports = { 4 | send: { 5 | smtpConfig: { 6 | host: 'smtp.mailtrap.io', 7 | port: 2525, 8 | auth: { 9 | user: MAILTRAP_USER, 10 | pass: MAILTRAP_PASSWORD, 11 | }, 12 | }, 13 | mailOptions: { 14 | from: 'from@example.com', 15 | to: 'to@example.com', 16 | }, 17 | }, 18 | payloads: { 19 | 'initial-props': { 20 | default: { 21 | userId: 1, 22 | }, 23 | user2: { 24 | userId: 2, 25 | }, 26 | }, 27 | 'payload-only': { 28 | default: { 29 | firstName: 'John', 30 | }, 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /examples/latest/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "public": true, 4 | "name": "nextmail-latest", 5 | "alias": "nextmail-latest", 6 | "builds": [ 7 | { "src": "./static/**", "use": "@now/static" } 8 | ], 9 | "routes": [ 10 | { "src": "/static/(.*)", "dest": "static/$1" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/latest/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "latest", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.19.0", 9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", 10 | "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", 11 | "requires": { 12 | "follow-redirects": "1.5.10", 13 | "is-buffer": "^2.0.2" 14 | } 15 | }, 16 | "debug": { 17 | "version": "3.1.0", 18 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 19 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 20 | "requires": { 21 | "ms": "2.0.0" 22 | } 23 | }, 24 | "follow-redirects": { 25 | "version": "1.5.10", 26 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 27 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 28 | "requires": { 29 | "debug": "=3.1.0" 30 | } 31 | }, 32 | "is-buffer": { 33 | "version": "2.0.3", 34 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", 35 | "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" 36 | }, 37 | "js-tokens": { 38 | "version": "4.0.0", 39 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 40 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 41 | }, 42 | "loose-envify": { 43 | "version": "1.4.0", 44 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 45 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 46 | "requires": { 47 | "js-tokens": "^3.0.0 || ^4.0.0" 48 | } 49 | }, 50 | "ms": { 51 | "version": "2.0.0", 52 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 53 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 54 | }, 55 | "nextmail": { 56 | "version": "file:../..", 57 | "requires": { 58 | "@babel/cli": "7.4.3", 59 | "@babel/core": "7.4.3", 60 | "@babel/plugin-transform-runtime": "7.4.3", 61 | "@babel/preset-env": "7.4.3", 62 | "@babel/preset-react": "7.0.0", 63 | "@babel/runtime": "7.4.3", 64 | "common-tags": "1.8.0", 65 | "debug": "4.1.1", 66 | "decache": "4.5.1", 67 | "html-to-text": "5.1.1", 68 | "mjml": "4.6.2", 69 | "mjml-react": "1.0.52", 70 | "nodemailer": "6.1.0", 71 | "qs": "6.7.0", 72 | "require-context": "1.1.0", 73 | "route-parser": "0.0.5", 74 | "yargs": "13.2.2" 75 | } 76 | }, 77 | "object-assign": { 78 | "version": "4.1.1", 79 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 80 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 81 | }, 82 | "prop-types": { 83 | "version": "15.7.2", 84 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", 85 | "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", 86 | "requires": { 87 | "loose-envify": "^1.4.0", 88 | "object-assign": "^4.1.1", 89 | "react-is": "^16.8.1" 90 | } 91 | }, 92 | "react": { 93 | "version": "16.8.6", 94 | "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", 95 | "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", 96 | "requires": { 97 | "loose-envify": "^1.1.0", 98 | "object-assign": "^4.1.1", 99 | "prop-types": "^15.6.2", 100 | "scheduler": "^0.13.6" 101 | } 102 | }, 103 | "react-dom": { 104 | "version": "16.8.6", 105 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", 106 | "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", 107 | "requires": { 108 | "loose-envify": "^1.1.0", 109 | "object-assign": "^4.1.1", 110 | "prop-types": "^15.6.2", 111 | "scheduler": "^0.13.6" 112 | } 113 | }, 114 | "react-is": { 115 | "version": "16.8.6", 116 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", 117 | "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" 118 | }, 119 | "scheduler": { 120 | "version": "0.13.6", 121 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", 122 | "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", 123 | "requires": { 124 | "loose-envify": "^1.1.0", 125 | "object-assign": "^4.1.1" 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/latest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "latest", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "nextmail build", 6 | "dev": "nextmail dev", 7 | "gen": "node index.js", 8 | "send": "nextmail send", 9 | "start": "NODE_ENV=production nextmail start" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "axios": "0.19.0", 14 | "nextmail": "../..", 15 | "react": "16.8.6", 16 | "react-dom": "16.8.6" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/latest/static/bicycle.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanconnollydev/nextmail/aa6bf6422985124203ac3c63a2ddf8f03281fa4f/examples/latest/static/bicycle.jpeg -------------------------------------------------------------------------------- /examples/package-consumer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-consumer", 3 | "version": "1.0.0", 4 | "description": "Consumes a package that leverages nextmail", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "author": "Sean Connolly", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "jest": "24.7.1" 13 | }, 14 | "dependencies": { 15 | "nextmail-package": "file:../package" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/package-consumer/render.test.js: -------------------------------------------------------------------------------- 1 | const { renderer } = require('nextmail-package'); 2 | 3 | test('renders a demo', async () => { 4 | const { html } = await renderer.renderEmail('demo'); 5 | expect(html).toBeDefined(); 6 | }); 7 | -------------------------------------------------------------------------------- /examples/package/emails/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Mjml, 5 | MjmlHead, 6 | MjmlTitle, 7 | MjmlBody, 8 | MjmlSection, 9 | MjmlColumn, 10 | MjmlText, 11 | MjmlRaw, 12 | } from 'nextmail/mjml-react'; 13 | 14 | function Demo(props) { 15 | const { firstName } = props; 16 | 17 | return ( 18 | 19 | 20 | Last Minute Offer 21 | 22 | 23 | 24 | 25 | {`Hello ${firstName}`} 26 | 27 |
But its ok
28 |
29 |
30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | export default Demo; 37 | -------------------------------------------------------------------------------- /examples/package/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { Renderer } = require('nextmail'); 3 | 4 | const rootDirectory = path.resolve(__dirname); 5 | const renderer = new Renderer({ rootDirectory }); 6 | 7 | module.exports = { renderer }; 8 | -------------------------------------------------------------------------------- /examples/package/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextmail-package", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "js-tokens": { 8 | "version": "4.0.0", 9 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 10 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 11 | }, 12 | "loose-envify": { 13 | "version": "1.4.0", 14 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 15 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 16 | "requires": { 17 | "js-tokens": "^3.0.0 || ^4.0.0" 18 | } 19 | }, 20 | "nextmail": { 21 | "version": "file:../..", 22 | "requires": { 23 | "@babel/core": "7.4.3", 24 | "@babel/plugin-transform-runtime": "7.4.3", 25 | "@babel/preset-env": "7.4.3", 26 | "@babel/preset-react": "7.0.0", 27 | "@babel/runtime": "7.4.3", 28 | "common-tags": "1.8.0", 29 | "html-to-text": "5.1.1", 30 | "mjml": "4.4.0-beta.1", 31 | "mjml-react": "1.0.52", 32 | "require-context": "1.1.0", 33 | "route-parser": "0.0.5", 34 | "yargs": "13.2.2" 35 | } 36 | }, 37 | "object-assign": { 38 | "version": "4.1.1", 39 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 40 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 41 | }, 42 | "prop-types": { 43 | "version": "15.7.2", 44 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", 45 | "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", 46 | "requires": { 47 | "loose-envify": "^1.4.0", 48 | "object-assign": "^4.1.1", 49 | "react-is": "^16.8.1" 50 | } 51 | }, 52 | "react": { 53 | "version": "16.8.6", 54 | "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", 55 | "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", 56 | "requires": { 57 | "loose-envify": "^1.1.0", 58 | "object-assign": "^4.1.1", 59 | "prop-types": "^15.6.2", 60 | "scheduler": "^0.13.6" 61 | } 62 | }, 63 | "react-dom": { 64 | "version": "16.8.6", 65 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", 66 | "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", 67 | "requires": { 68 | "loose-envify": "^1.1.0", 69 | "object-assign": "^4.1.1", 70 | "prop-types": "^15.6.2", 71 | "scheduler": "^0.13.6" 72 | } 73 | }, 74 | "react-is": { 75 | "version": "16.8.6", 76 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", 77 | "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" 78 | }, 79 | "scheduler": { 80 | "version": "0.13.6", 81 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", 82 | "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", 83 | "requires": { 84 | "loose-envify": "^1.1.0", 85 | "object-assign": "^4.1.1" 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextmail-package", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "build": "nextmail build" 7 | }, 8 | "license": "MIT", 9 | "dependencies": { 10 | "nextmail": "../..", 11 | "react": "16.8.6", 12 | "react-dom": "16.8.6" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Renderer = require('./lib/Renderer'); 2 | 3 | module.exports = { 4 | Renderer, 5 | }; 6 | -------------------------------------------------------------------------------- /lib/Renderer.js: -------------------------------------------------------------------------------- 1 | const { render } = require('mjml-react'); 2 | const htmlToText = require('html-to-text'); 3 | const debug = require('./debug'); 4 | const requireTemplate = require('./requireTemplate'); 5 | 6 | class Renderer { 7 | constructor(options = {}) { 8 | this.rootDirectory = options.rootDirectory || process.cwd(); 9 | debug('rootDirectory: %s', this.rootDirectory); 10 | } 11 | 12 | async renderEmail(name, payload) { 13 | debug('renderEmail name: %s', name); 14 | debug('renderEmail payload: %o', payload); 15 | 16 | const Template = requireTemplate(this.rootDirectory, name); 17 | debug('Template: %s', Template.displayName || Template.name); 18 | 19 | const initialProps = typeof Template.getInitialProps === 'function' 20 | ? await Template.getInitialProps({ payload }) 21 | : {}; 22 | debug('initialProps: %o', initialProps); 23 | 24 | const props = Object.assign({}, payload, initialProps); 25 | debug('props: %o', props); 26 | 27 | const subject = typeof Template.getSubject === 'function' 28 | ? await Template.getSubject({ payload, props }) 29 | : null; 30 | 31 | debug('subject: %s', subject); 32 | 33 | const { html } = render(Template(props)); 34 | const text = htmlToText.fromString(html); 35 | 36 | return { html, subject, text }; 37 | } 38 | } 39 | 40 | module.exports = Renderer; 41 | -------------------------------------------------------------------------------- /lib/babel-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-react', 4 | '@babel/preset-env', 5 | ], 6 | plugins: [ 7 | ['@babel/plugin-transform-runtime', { regenerator: true }], 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /lib/build.js: -------------------------------------------------------------------------------- 1 | const getOptions = require('@babel/cli/lib/babel/options').default; 2 | const babelCli = require('@babel/cli/lib/babel/dir').default; 3 | const babelOptions = require('./babel-preset'); 4 | 5 | const root = process.cwd(); 6 | 7 | async function build({ watch = false } = {}) { 8 | let options = getOptions([ 9 | '', 10 | '', 11 | `${root}/emails`, 12 | '--out-dir', 13 | `${root}/.nextmail`, 14 | '--delete-dir-on-start', 15 | ...(watch ? ['--watch'] : []), 16 | ]); 17 | options = Object.assign({}, options, { babelOptions }); 18 | 19 | await babelCli(options); 20 | } 21 | 22 | async function buildWatch() { 23 | return build({ watch: true }); 24 | } 25 | 26 | module.exports = { build, buildWatch }; 27 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const decache = require('decache'); 2 | const debug = require('./debug'); 3 | 4 | const { NODE_ENV } = process.env; 5 | const root = process.cwd(); 6 | 7 | const defaultConfig = { 8 | send: {}, 9 | payloads: {}, 10 | }; 11 | 12 | function getConfig() { 13 | const customConfig = _requireCustomConfig(); 14 | debug('Custom config: %o', customConfig); 15 | return Object.assign({}, defaultConfig, customConfig); 16 | } 17 | 18 | function _requireCustomConfig() { 19 | const filePath = `${root}/nextmail.config.js`; 20 | 21 | if (NODE_ENV !== 'production') { 22 | decache(filePath); 23 | } 24 | 25 | let module; 26 | try { 27 | module = require(filePath); 28 | } catch (e) { 29 | // Ignore 30 | } 31 | 32 | return module; 33 | } 34 | 35 | module.exports = { getConfig }; 36 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | /* 2 | Example usage: 3 | 4 | const debug = require('../lib/debug'); 5 | debug('value %s', value); 6 | */ 7 | module.exports = require('debug')('nextmail'); 8 | -------------------------------------------------------------------------------- /lib/getEmails.js: -------------------------------------------------------------------------------- 1 | const requireContext = require('require-context'); 2 | 3 | const root = process.cwd(); 4 | 5 | function getEmails() { 6 | const req = requireContext(`${root}/emails`, true, /\.js$/); 7 | return req.keys().filter(e => !e.startsWith('src')); 8 | } 9 | 10 | module.exports = getEmails; 11 | -------------------------------------------------------------------------------- /lib/requireTemplate.js: -------------------------------------------------------------------------------- 1 | const decache = require('decache'); 2 | 3 | const { NODE_ENV } = process.env; 4 | 5 | function requireTemplate(rootDirectory, name) { 6 | const filePath = `${rootDirectory}/.nextmail/${name}`; 7 | 8 | if (NODE_ENV !== 'production' && NODE_ENV !== 'test') { 9 | decache(filePath); 10 | } 11 | return require(filePath).default; 12 | } 13 | 14 | module.exports = requireTemplate; 15 | -------------------------------------------------------------------------------- /lib/sendEmail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | const debug = require('./debug'); 3 | const { getConfig } = require('./config'); 4 | const Renderer = require('./Renderer'); 5 | 6 | async function sendEmail(args) { 7 | const { 0: email, 1: payloadKey = 'default' } = args; 8 | if (!email) throw new Error('You must specify an email to send, e.g. nextmail send my-email'); 9 | 10 | const config = getConfig(); 11 | debug('Send config: %o', config.send); 12 | 13 | if (!config.send.smtpConfig) throw new Error('send.smtpConfig is required for sending.'); 14 | 15 | const payload = config.payloads ? config.payloads[email][payloadKey] || {} : {}; 16 | debug('Send payload: %o', payload); 17 | 18 | const renderer = new Renderer(); 19 | const { html, subject, text } = await renderer.renderEmail(email, payload); 20 | 21 | const transporter = nodemailer.createTransport(config.send.smtpConfig); 22 | const mailOptions = Object.assign({}, config.send.mailOptions, { html, subject, text }); 23 | 24 | // send mail with defined transport object 25 | const info = await transporter.sendMail(mailOptions); 26 | 27 | console.log('Message sent: %s', info.messageId); 28 | } 29 | 30 | module.exports = sendEmail; 31 | -------------------------------------------------------------------------------- /mjml-react.js: -------------------------------------------------------------------------------- 1 | module.exports = require('mjml-react'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextmail", 3 | "version": "0.4.3", 4 | "description": "Craft emails in React and MJML. Inspired by Next.js.", 5 | "keywords": "react, nextjs, email, mjml", 6 | "main": "index.js", 7 | "bin": { 8 | "nextmail": "./bin/nextmail.js" 9 | }, 10 | "scripts": { 11 | "lint": "eslint --ext .js .", 12 | "test": "test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/goldenshun/nextmail.git" 17 | }, 18 | "author": "Sean Connolly", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/goldenshun/nextmail/issues" 22 | }, 23 | "homepage": "https://github.com/goldenshun/nextmail#readme", 24 | "dependencies": { 25 | "@babel/cli": "7.4.3", 26 | "@babel/core": "7.4.3", 27 | "@babel/plugin-transform-runtime": "7.4.3", 28 | "@babel/preset-env": "7.4.3", 29 | "@babel/preset-react": "7.0.0", 30 | "@babel/runtime": "7.4.3", 31 | "common-tags": "1.8.0", 32 | "debug": "4.1.1", 33 | "decache": "4.5.1", 34 | "html-to-text": "5.1.1", 35 | "mjml": "4.6.2", 36 | "mjml-react": "1.0.52", 37 | "nodemailer": "6.1.0", 38 | "qs": "6.7.0", 39 | "require-context": "1.1.0", 40 | "route-parser": "0.0.5", 41 | "yargs": "13.2.2" 42 | }, 43 | "peerDependencies": { 44 | "react": "^16.0.0", 45 | "react-dom": "^16.0.0" 46 | }, 47 | "devDependencies": { 48 | "eslint": "5.5.0", 49 | "eslint-config-airbnb": "17.1.0", 50 | "eslint-plugin-cypress": "2.2.1", 51 | "eslint-plugin-import": "2.14.0", 52 | "eslint-plugin-jest": "21.17.0", 53 | "eslint-plugin-jsx-a11y": "6.1.1", 54 | "eslint-plugin-react": "7.11.1", 55 | "react": "16.8.6", 56 | "react-dom": "16.8.6" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/handleRequest.js: -------------------------------------------------------------------------------- 1 | const debug = require('../lib/debug'); 2 | const { findRoute } = require('./routes/routes'); 3 | 4 | const handleRequest = async (req, res) => { 5 | debug('handleRequest: %s', req.url); 6 | const route = findRoute(req.method.toLowerCase(), req.url.toLowerCase()); 7 | 8 | if (route) { 9 | try { 10 | return await route.handler(req, res, route.params); 11 | } catch (err) { 12 | console.error(err); // eslint-disable-line no-console 13 | return handle500(res); 14 | } 15 | } 16 | 17 | return handle404(res); 18 | }; 19 | 20 | function handle404(res) { 21 | res.writeHead(404, { 'Content-Type': 'text/html' }); 22 | res.write('404 Not Found\n'); 23 | res.end(); 24 | } 25 | 26 | function handle500(res) { 27 | res.writeHead(500, { 'Content-Type': 'text/html' }); 28 | res.write('500 Internal Server Error\n'); 29 | res.end(); 30 | } 31 | 32 | module.exports = handleRequest; 33 | -------------------------------------------------------------------------------- /server/routes/_index.js: -------------------------------------------------------------------------------- 1 | const qs = require('qs'); 2 | const { html } = require('common-tags'); 3 | const { getConfig } = require('../../lib/config'); 4 | const getEmails = require('../../lib/getEmails'); 5 | 6 | const config = getConfig(); 7 | 8 | const index = { 9 | method: 'get', 10 | url: '/', 11 | handler: async (req, res) => { 12 | res.writeHead(200, { 'Content-Type': 'text/html' }); 13 | 14 | const emails = getEmails().map(e => e.slice(0, -3)); 15 | 16 | const str = html` 17 | 18 | 19 | 20 | 39 | 40 | 41 | ${emails.map(email => ` 42 | 46 | `)} 47 | 48 | 49 | `; 50 | 51 | res.write(str); 52 | res.end(); 53 | }, 54 | }; 55 | 56 | function renderLinks(email) { 57 | const payloads = config.payloads[email] || { default: {} }; 58 | 59 | /* eslint-disable indent */ 60 | return ` 61 |
    62 | ${Object.keys(payloads).map((key) => { 63 | const payload = payloads[key]; 64 | const query = qs.stringify(payload); 65 | return ` 66 |
  • 67 | 68 | ${key} 69 | 70 | 71 | (text) 72 | 73 |
  • 74 | `; 75 | })} 76 |
77 | `; 78 | /* eslint-disable indent */ 79 | } 80 | 81 | module.exports = index; 82 | -------------------------------------------------------------------------------- /server/routes/preview.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const qs = require('qs'); 3 | const debug = require('../../lib/debug'); 4 | const Renderer = require('../../lib/Renderer'); 5 | 6 | const renderer = new Renderer(); 7 | 8 | const preview = { 9 | method: 'get', 10 | url: '/preview/:format/*template', 11 | handler: async (req, res, params) => { 12 | debug('/preview url: %o:', req.url); 13 | const { query } = url.parse(req.url); 14 | 15 | const payload = qs.parse(query); 16 | debug('payload: %o:', payload); 17 | 18 | const { html, text } = await renderer.renderEmail(params.template, payload); 19 | 20 | if (params.format === 'text') { 21 | return handleText(res, text); 22 | } 23 | 24 | return handleHtml(res, html); 25 | }, 26 | }; 27 | 28 | function handleText(res, text) { 29 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 30 | res.write(text); 31 | res.end(); 32 | } 33 | 34 | function handleHtml(res, html) { 35 | res.writeHead(200, { 'Content-Type': 'text/html' }); 36 | res.write(html); 37 | res.end(); 38 | } 39 | 40 | module.exports = preview; 41 | -------------------------------------------------------------------------------- /server/routes/routes.js: -------------------------------------------------------------------------------- 1 | const Route = require('route-parser'); 2 | const index = require('./_index'); 3 | const staticRoute = require('./static'); 4 | const preview = require('./preview'); 5 | 6 | const routes = [staticRoute, preview, index].map(rt => Object.assign({}, rt, { 7 | url: new Route(rt.url), 8 | })); 9 | 10 | const findRoute = (method, url) => { 11 | const route = routes.find(rt => rt.method === method && rt.url.match(url)); 12 | 13 | if (!route) return null; 14 | 15 | return { handler: route.handler, params: route.url.match(url) }; 16 | }; 17 | 18 | module.exports = { findRoute }; 19 | -------------------------------------------------------------------------------- /server/routes/static.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const debug = require('../../lib/debug'); 4 | 5 | const root = process.cwd(); 6 | 7 | // maps file extention to MIME types 8 | const mimeType = { 9 | '.ico': 'image/x-icon', 10 | '.png': 'image/png', 11 | '.jpg': 'image/jpeg', 12 | '.svg': 'image/svg+xml', 13 | '.pdf': 'application/pdf', 14 | '.doc': 'application/msword', 15 | '.eot': 'appliaction/vnd.ms-fontobject', 16 | '.ttf': 'aplication/font-sfnt', 17 | }; 18 | 19 | const staticRoute = { 20 | method: 'get', 21 | url: '/static/:path', 22 | handler: async (req, res, params) => { 23 | debug('/static url: %o:', req.url); 24 | 25 | // extract URL path 26 | // Avoid https://en.wikipedia.org/wiki/Directory_traversal_attack 27 | // e.g curl --path-as-is http://localhost:9000/../fileInDanger.txt 28 | // by limiting the path to current directory only 29 | const sanitizePath = path.normalize(params.path).replace(/^(\.\.[\/\\])+/, ''); // eslint-disable-line no-useless-escape 30 | let pathname = path.join(root, '/static', sanitizePath); 31 | 32 | debug('/static pathname: %o:', pathname); 33 | fs.exists(pathname, (exist) => { 34 | if (!exist) { 35 | // if the file is not found, return 404 36 | res.statusCode = 404; 37 | res.end(`File ${pathname} not found!`); 38 | return; 39 | } 40 | 41 | // if is a directory, then look for index.html 42 | if (fs.statSync(pathname).isDirectory()) { 43 | pathname += '/index.html'; 44 | } 45 | 46 | // read file from file system 47 | fs.readFile(pathname, (err, data) => { 48 | if (err) { 49 | res.statusCode = 500; 50 | res.end(`Error getting the file: ${err}.`); 51 | } else { 52 | // based on the URL path, extract the file extention. e.g. .js, .doc, ... 53 | const { ext } = path.parse(pathname); 54 | // if the file is found, set Content-type and send data 55 | res.setHeader('Content-type', mimeType[ext] || 'text/plain'); 56 | res.end(data); 57 | } 58 | }); 59 | }); 60 | }, 61 | }; 62 | 63 | module.exports = staticRoute; 64 | -------------------------------------------------------------------------------- /server/startServer.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const handleRequest = require('./handleRequest'); 4 | 5 | const startServer = async () => { 6 | process.on('unhandledRejection', (reason) => { 7 | console.log('Unhandled Rejection at:', reason.stack || reason); 8 | }); 9 | const server = http.createServer(handleRequest); 10 | 11 | await new Promise((resolve, reject) => { 12 | server.listen(6100, (err) => { 13 | if (err) { 14 | reject(err); 15 | } 16 | 17 | console.log('server is listening on 6100'); 18 | resolve(); 19 | }); 20 | }); 21 | }; 22 | 23 | module.exports = startServer; 24 | --------------------------------------------------------------------------------