Skip to content

Commit

Permalink
Merge pull request #13 from Thomascogez/feature/layout
Browse files Browse the repository at this point in the history
Feature/layout
  • Loading branch information
Thomascogez authored Dec 1, 2022
2 parents d5ece34 + d83d6af commit aaad817
Show file tree
Hide file tree
Showing 28 changed files with 744 additions and 113 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/pr.yml
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
187 changes: 144 additions & 43 deletions README.md
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

Expand All @@ -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";
Expand All @@ -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
![Template-overview](./assets/template-overview.png)
> 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
![Layout-overview](./assets/template-layout-overview.png)
> 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 🫡
123 changes: 105 additions & 18 deletions __tests__/integration/nodemailer_mjml.spec.ts
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 () => {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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);
});
});
});
1 change: 1 addition & 0 deletions __tests__/resources/include/content-mustache.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mj-button>{{ content }}</mj-button>
Loading

0 comments on commit aaad817

Please sign in to comment.