├── .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 |
43 |
${email}
44 | ${renderLinks(email)}
45 |
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 |
--------------------------------------------------------------------------------