-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from Thomascogez/feature/layout
Feature/layout
- Loading branch information
Showing
28 changed files
with
744 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
name: Pull request 🧪 | ||
|
||
on: [pull_request] | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: "Tests:checkout" | ||
uses: actions/checkout@v2 | ||
|
||
- name: "Tests:run" | ||
run: docker-compose run tests yarn test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,23 @@ | ||
# nodemailer-mjml | ||
|
||
**nodemailer-mjml** is a plug-and-play solution for sending **MJML** mail using **nodemailer**. It not only bring a compatibility layer between **MJML** and **nodemailer** it also allow to render dynamic content using **mustache** templating | ||
<h1 align="center"> | ||
<br> | ||
Nodemailer-mjml | ||
<br> | ||
</h1> | ||
|
||
<h4 align="center"> | ||
<b>nodemailer-mjml</b> is a plug-and-play solution for sending <a href="https://github.com/mjmlio/mjml"><b>MJML</b></a> mail using <a href="https://github.com/nodemailer/nodemailer"><b>nodemailer</b></a>. It not only bring a compatibility layer between <b>MJML</b> and <b>nodemailer<b> it also allow to render dynamic content using <b>mustache</b> templating | ||
|
||
</h4> | ||
|
||
<p align="center"> | ||
<a href="https://badge.fury.io/js/nodemailer-mjml"> | ||
<img src="https://badge.fury.io/js/nodemailer-mjml.svg" alt="nodemailer-mjml"> | ||
</a> | ||
<a href="https://github.com/Thomascogez/nodemailer-mjml/actions/workflows/publish.yml"><img src="https://github.com/Thomascogez/nodemailer-mjml/actions/workflows/publish.yml/badge.svg"></a> | ||
</p> | ||
|
||
--- | ||
|
||
## Installation | ||
|
||
|
@@ -9,6 +26,8 @@ yarn add nodemailer-mjml | |
# or using npm install nodemailer-mjml | ||
``` | ||
|
||
## Basic usage | ||
|
||
```ts | ||
import { createTransport } from "nodemailer"; | ||
import { nodemailerMjmlPlugin } from "nodemailer-mjml"; | ||
|
@@ -20,71 +39,153 @@ transport.use('compile', nodemailerMjmlPlugin({/*Pass desired plugin options her | |
|
||
``` | ||
|
||
## Docs | ||
## Usage examples | ||
|
||
### Plugin options | ||
### With template | ||
|
||
> Plugin options are defined by the **IPluginOptions** interface | ||
 | ||
> using `nodemailer-mjml` with a template is the simplest to start | ||
| option | type | description | default | | ||
| -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | ||
| templateFolder | string | Path of the dir containing your **MJML** template | undefined | | ||
| mjmlOptions? | MJMLParsingOptions | Options that would be passed to **MJML** compiler (see more) [mjml doc](https://github.com/mjmlio/mjml) | {validationLevel: "strict"} | | ||
| minifyHtmlOutput? | boolean | use to enable/disable html minification using **html-minifier** | true | | ||
| htmlMinifierOptions? | Options | Options that would be passed to **html-minifier** (see more) [html-minifier doc](https://github.com/kangax/html-minifier) | undefined | | ||
```ts | ||
import { createTransport } from "nodemailer"; | ||
import { nodemailerMjmlPlugin } from "../src/index"; | ||
import { join } from "path"; | ||
|
||
### Send mail options | ||
const transport = createTransport({ | ||
host: "localhost", | ||
port: 25 | ||
}); | ||
|
||
> **nodemailer-mjml** bring two new params to the `sendMail` function | ||
transport.use( | ||
"compile", | ||
nodemailerMjmlPlugin({ templateFolder: join(__dirname, "mailTemplates") }) | ||
); | ||
|
||
| options | type | description | default | | ||
| ------------- | ------ | ----------------------------------------------------------------------- | --------- | | ||
| templateName? | string | Name of the file (without extension) corresponding to your template | undefined | | ||
| templateData? | Object | Object containing data that would be used by mustache template compiler | undefined | | ||
const sendTemplatedEmail = async () => { | ||
await transport.sendMail({ | ||
from: '"John doe" <[email protected]>', | ||
to: "[email protected]", | ||
subject: "Welcome", | ||
templateName: "simpleTemplate", // <- Targeted template name | ||
templateData: { // <- Data to be injected in the template | ||
companyLogoURL: "https://www.kadencewp.com/wp-content/uploads/2020/10/alogo-2.png", | ||
heroImageURL: "https://www.kadencewp.com/wp-content/uploads/2020/10/alogo-2.png", | ||
articles: [ | ||
{ | ||
articleImageURL: "https://api.lorem.space/image/watch?w=150&h=150", | ||
articleName: "Watch 1", | ||
articleDescription: "lorem ipsum dolor sit amet", | ||
}, | ||
{ | ||
articleImageURL: "https://api.lorem.space/image/watch?w=150&h=150", | ||
articleName: "Watch 2", | ||
articleDescription: "lorem ipsum dolor sit amet" | ||
}, | ||
{ | ||
articleImageURL: "https://api.lorem.space/image/watch?w=150&h=150", | ||
articleName: "Watch 3", | ||
articleDescription: "lorem ipsum dolor sit amet" | ||
}, | ||
] | ||
}, | ||
}); | ||
}; | ||
|
||
## Tests | ||
sendTemplatedEmail(); | ||
``` | ||
|
||
Run test | ||
> This complete example can be found in the [examples](./examples/simpleTemplate.ts) folder | ||
```sh | ||
# watch mode | ||
docker compose run --rm tests yarn test:watch | ||
#single run | ||
docker compose run --rm tests yarn test | ||
``` | ||
### With layout template | ||
|
||
## Example usage | ||
 | ||
> Template layout allow to reuse the same layout for multiple templates | ||
```ts | ||
import { createTransport } from "nodemailer"; | ||
import { nodemailerMjmlPlugin } from "nodemailer-mjml"; | ||
import { nodemailerMjmlPlugin } from "../src/index"; | ||
import { join } from "path"; | ||
|
||
const transport = createTransport({}); | ||
const transport = createTransport({ | ||
host: "localhost", | ||
port: 25 | ||
}); | ||
|
||
// Register nodemailer-mjml to your nodemailer transport | ||
transport.use( | ||
"compile", | ||
nodemailerMjmlPlugin({ templateFolder: join(__dirname, "mailTemplates") }) | ||
"compile", | ||
nodemailerMjmlPlugin({ templateFolder: join(__dirname, "mailTemplates") }) | ||
); | ||
|
||
const sendTemplatedEmail = async () => { | ||
await transport.sendMail({ | ||
from: '"John doe" <[email protected]>', | ||
to: "[email protected]", | ||
subject: "Welcome", | ||
templateName: "hello", | ||
templateData: { | ||
userName: "John doe", | ||
}, | ||
}); | ||
await transport.sendMail({ | ||
from: '"John doe" <[email protected]>', | ||
to: "[email protected]", | ||
subject: "Welcome", | ||
templateLayoutName: "layoutTemplate", | ||
templateLayoutSlots: { | ||
header: "partials/header", | ||
content: "partials/content", | ||
footer: "partials/footer", | ||
}, | ||
templateData: { | ||
content: { | ||
imageURL: "http://5vph.mj.am/img/5vph/b/1g8pi/068ys.png" | ||
} | ||
} | ||
}); | ||
}; | ||
|
||
sendTemplatedEmail(); | ||
``` | ||
> If you want to try the above example check the **examples** folder | ||
|
||
# Contributing | ||
> This complete example can be found in the [examples](./examples/layoutTemplate.ts) folder | ||
All contributions are welcome 🫡 | ||
## Documentation | ||
|
||
### Plugin options | ||
|
||
> Plugin options are defined by the **IPluginOptions** interface | ||
| option | type | description | default | | ||
| ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | ||
| templateFolder | string | Path of the dir containing your **MJML** template | undefined | | ||
| templatePartialsFolder? | string | Path relative to **templateFolder**, if defined when using a template layout it will be folder where **nodemailer-mjml** while try to find fallback slots if one or more is undefined | undefined | | ||
| mjmlOptions? | MJMLParsingOptions | Options that would be passed to **MJML** compiler (see more) [mjml doc](https://github.com/mjmlio/mjml) | {validationLevel: "strict"} | | ||
| minifyHtmlOutput? | boolean | use to enable/disable html minification using **html-minifier** | true | | ||
| htmlMinifierOptions? | Options | Options that would be passed to **html-minifier** (see more) [html-minifier doc](https://github.com/kangax/html-minifier) | undefined | | ||
|
||
### Send mail options | ||
|
||
> **nodemailer-mjml** bring 4 new params to the `sendMail` function | ||
| options | type | description | default | | ||
| -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | ||
| templateName? | string | Name of the file relative to **templateFolder** (without extension) corresponding to your template | undefined | | ||
| templateLayoutName? | string | Name of the file relative to **templateFolder** (without extension) corresponding to your template layout file | undefined | | ||
| templateLayoutSlots? | Object | Object containing path of partial file relative to **templateFolder** (without extension) that will be injected to the corresponding slot | undefined | | ||
| templateData? | Object | Object containing data that would be used by mustache template compiler | undefined | | ||
|
||
## Tests | ||
|
||
> This plugin, have multiple tests suites (unit, integration) to ensure that everything is working as expected | ||
> You can run the tests locally by running the following command | ||
```sh | ||
# watch mode | ||
docker compose run --rm tests yarn test:watch | ||
#single run | ||
docker compose run --rm tests yarn test | ||
``` | ||
|
||
## Credit | ||
|
||
This software uses the following open source packages: | ||
|
||
- [MJML](https://github.com/mjmlio/) | ||
- [nodemailer](https://github.com/nodemailer/nodemailer) | ||
- [mustache](https://www.npmjs.com/package/mustache) | ||
- [html-minifier](https://www.npmjs.com/package/html-minifier) | ||
|
||
## Contributing | ||
|
||
All contributions are welcome 🫡 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,9 @@ | ||
import { join } from "path"; | ||
import { buildNodemailerTransport } from "../helpers/buildNodemailerClient"; | ||
import mjml2html from 'mjml'; | ||
import { readFile } from "fs/promises"; | ||
import { minify } from "html-minifier"; | ||
import { render } from "mustache"; | ||
import { join } from "path"; | ||
import supertest from "supertest"; | ||
import { MAILDEV_API_ENDPOINT } from "../constants/mailDev"; | ||
import { buildMjmlTemplate } from "../../src"; | ||
import { MAILDEV_API_ENDPOINT } from "../constants/mailDev"; | ||
import { buildNodemailerTransport } from "../helpers/buildNodemailerClient"; | ||
|
||
describe("Nodemailer mjml", () => { | ||
it("should fail if template does not exist", async () => { | ||
|
@@ -42,9 +39,10 @@ describe("Nodemailer mjml", () => { | |
}); | ||
|
||
it("should send mail", async () => { | ||
const expectedOutput = await buildMjmlTemplate({ | ||
templateFolder: join(__dirname, "../resources") | ||
}, "test"); | ||
const expectedOutput = await buildMjmlTemplate( | ||
{ templateFolder: join(__dirname, "../resources") }, | ||
{ templateName: "test" } | ||
); | ||
|
||
const nodeMailerTransport = buildNodemailerTransport({ | ||
templateFolder: join(__dirname, "../resources") | ||
|
@@ -72,15 +70,19 @@ describe("Nodemailer mjml", () => { | |
nestedKey: "nestedKey" | ||
} | ||
}; | ||
|
||
const expectedOutput = await buildMjmlTemplate({ | ||
templateFolder: join(__dirname, "../resources") | ||
}, "test-mustache", { | ||
testKey: "testKey", | ||
testKeyNested: { | ||
nestedKey: "nestedKey" | ||
} | ||
}); | ||
|
||
const expectedOutput = await buildMjmlTemplate( | ||
{ templateFolder: join(__dirname, "../resources") }, | ||
{ | ||
templateName: "test-mustache", | ||
templateData: { | ||
testKey: "testKey", | ||
testKeyNested: { | ||
nestedKey: "nestedKey" | ||
}, | ||
} | ||
|
||
}); | ||
|
||
const nodeMailerTransport = buildNodemailerTransport({ | ||
templateFolder: join(__dirname, "../resources") | ||
|
@@ -115,4 +117,89 @@ describe("Nodemailer mjml", () => { | |
templateName: "test-include/test-include" | ||
}); | ||
}); | ||
|
||
describe("Layout", () => { | ||
it("should fail if layout does not exist", async () => { | ||
const nodeMailerTransport = buildNodemailerTransport({ | ||
templateFolder: join(__dirname, "../resources") | ||
}); | ||
|
||
await expect( | ||
nodeMailerTransport.sendMail({ | ||
from: '"John doe" <[email protected]>', | ||
to: "[email protected]", | ||
subject: "Hello ✔", | ||
text: "Hello world?", | ||
templateLayoutName: "layout/layoutThatDoesNotExist" | ||
}) | ||
).rejects.toThrow(); | ||
}); | ||
|
||
it("should send an email with a layout and fallback header", async () => { | ||
|
||
const expectedOutput = await buildMjmlTemplate({ | ||
templateFolder: join(__dirname, "../resources"), | ||
templatePartialsFolder: "/include" | ||
}, { | ||
templateLayoutName: "layout/layout-single-slot" | ||
}); | ||
|
||
const nodeMailerTransport = buildNodemailerTransport({ | ||
templateFolder: join(__dirname, "../resources"), | ||
templatePartialsFolder: "/include" | ||
}); | ||
|
||
await nodeMailerTransport.sendMail({ | ||
from: '"John doe" <[email protected]>', | ||
to: "[email protected]", | ||
subject: "Hello ✔", | ||
text: "Hello world?", | ||
templateLayoutName: "layout/layout-single-slot" | ||
}); | ||
|
||
const receivedMailResponse = await supertest(MAILDEV_API_ENDPOINT).get("/email"); | ||
expect(receivedMailResponse.status).toBe(200); | ||
|
||
const latestReceivedMail = receivedMailResponse.body.pop(); | ||
expect(minify(latestReceivedMail.html.toLowerCase())).toBe(expectedOutput.toLowerCase()); | ||
}); | ||
|
||
it("should send an email with rendered layout slots", async () => { | ||
const templateData = { | ||
headerTitle: "Header title", | ||
content: "Content", | ||
footerText: "Footer text" | ||
}; | ||
|
||
const templateLayoutSlots = { | ||
customHeader: "include/header-mustache", | ||
customContent: "include/content-mustache", | ||
customFooter: "include/footer-mustache", | ||
}; | ||
|
||
const nodeMailerTransport = buildNodemailerTransport({ | ||
templateFolder: join(__dirname, "../resources"), | ||
templatePartialsFolder: "/include" | ||
}); | ||
|
||
await nodeMailerTransport.sendMail({ | ||
from: '"John doe" <[email protected]>', | ||
to: "[email protected]", | ||
subject: "Hello ✔", | ||
text: "Hello world?", | ||
templateLayoutName: "layout/layout-multiple-slots", | ||
templateLayoutSlots, | ||
templateData | ||
}); | ||
|
||
const receivedMailResponse = await supertest(MAILDEV_API_ENDPOINT).get("/email"); | ||
expect(receivedMailResponse.status).toBe(200); | ||
|
||
const latestReceivedMail = receivedMailResponse.body.pop(); | ||
|
||
expect(latestReceivedMail.html).toContain(templateData.content); | ||
expect(latestReceivedMail.html).toContain(templateData.footerText); | ||
expect(latestReceivedMail.html).toContain(templateData.headerTitle); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<mj-button>{{ content }}</mj-button> |
Oops, something went wrong.