- Email templates with React components
- MJML components that work across clients (Outlook!)
- Preview server with live reload for quick development
- Dev mode opens emails in your browser instead of sending
- Test mode for ensuring emails send and have the correct content
- Plays well with js frameworks like next.js, redwood.js, remix
- Written in TypeScript, inspired by Action Mailer from Ruby on Rails
We’re longtime users of Action Mailer and wanted something similar for our typescript/react apps. We didn’t find anything, so we decided to build Mailing. We added some features that we would’ve liked in Action Mailer, like a mobile toggle (with hotkeys), and the ability to send a test email from the browser while developing. We went all in on MJML so that we (almost) never have to think about email clients or nested tables :)
- Install mailing with yarn or npm:
yarn:
yarn add mailing mailing-core
npm:
npm install --save mailing mailing-core
Note: you may want to add mailing as a dev dependency instead if you don't plan to use the preview function outside of development. mailing-core exports buildSendMail, which returns the sendMail function
Note: mailing requires version 17 or 18 of react and react-dom, so if you're not already in a react-based app, you'll need to add the following:
yarn:
yarn add react react-dom
npm:
npm install --save react react-dom
- Run
npx mailing
to start the preview server and scaffold youremails
directory. This will create the following directory for all of your emails:
emails
├── TextEmail.tsx // a simple example email template
├── Welcome.tsx // a complicated example email template
├── components // shared components go in here
│ ├── BulletedList.tsx
│ ├── Footer.tsx
│ ├── Head.tsx
│ ├── Header.tsx
│ └── theme.ts
├── index.ts // this exports sendMail and is where your SMTP config goes
└── previews // use previews to develop and check templates
├── TextEmail.tsx
└── Welcome.tsx
- Configure your email transport and
defaultFrom
inemails/index.ts
. It defaults to nodemailer's SMTP transport, but you can read about others here.
Example SendGrid transport:
const transport = nodemailer.createTransport({
host: "smtp.sendgrid.net",
port: 587,
auth: {
user: "apikey",
pass: process.env.SEND_GRID_KEY,
},
});
- Finally, send your first email like so:
import { sendMail } from "emails";
import Welcome from "emails/Welcome";
sendMail({
subject: "My First Email",
to: "tester@example.com",
cc: "tester+cc@example.com",
bcc: ["tester+bcc@example.com", "tester+bcc2@example.com"],
component: <Welcome firstName="Amelita" />,
});
Mailing includes a development mode for working on your emails. Running mailing
in dev will boot the preview server on localhost:3883 and show you all previews in emails/previews
. The previews live reload when files in the emails directory change. Previews are just functions that return one of your emails loaded up with props. We recommend grouping all previews for the same email template in a file at emails/previews/TemplateName.tsx
.
For example, here's emails/previews/Welcome.tsx
:
import Welcome from "../Welcome";
export function toAmelita() {
return <Welcome name="Amelita" />;
}
It will show up in the index:
Clicking through shows you the email with a mobile/desktop toggle and live reload as you edit:
When it's nice, send it to yourself or your QA tool of choice for final testing (we like Litmus):
We ship with two templates to help you get started. We recommend using these as starting points and modifying them to fit your use case.
Welcome Template (link)
This template showcases a handful of MJML and Mailing features, including a responsive hero image, bulleted list, and custom Google font with fallbacks.
Transactional Template (link)
This is a simpler template for text-based transactional emails.
When NODE_ENV === "test"
, calling sendMail
pushes messages into a queue for later examination. The mail-core
package exports a couple of functions for testing that emails send with the correct content.
function getTestMailQueue(): Promise<Mail[]>
Retrieve the test message queue.
function clearTestMailQueue(): Promise<void>
Clear the test message queue. You probably want to call this before tests that use the queue.
Example:
import { sendMail } from "emails";
import { getTestMailQueue, clearTestMailQueue } from "mailing/core";
import IssueNotification from "emails/IssueNotification";
describe("Example API", () => {
it("sends an email when an issue is ready for review", () => {
await clearTestEmailQueue();
// SOMETHING THAT WILL SEND AN EMAIL e.g.
// sendMail({
// to: "someone@something.com",
// subject: "test",
// component: <TextEmail ... />,
// });
const emails = await getTestMailQueue();
expect(emails.length).toBe(1);
expect(emails[0].subject).toMatch("Re: An issue title");
expect(emails[0].html).toMatch("READY FOR REVIEW");
expect(emails[0].html).toMatch("ready for QA");
});
});
mailing init
initializes a project then starts the development server
mailing preview
launches the development server
mailing server build
builds the next app in .mailing
mailing server start
starts the next app built in .mailing/.next
mailing server
builds and starts it
mailing export-previews
exports template previews as plain html files
mailing
runs init then preview
mm
is a cute alias for mailing
Running mailing init
generates a mailing.config.js file that will be used as default options for the CLI commands. The default options are:
{
"typescript": ???, // ("true" if you have a tsconfig.json in your root, otherwise "false")
"emailsDir": "./emails",
"outDir": "./previews_html" // (directory for export-previews html output)
}
Append --help to your CLI command for a full list of supported options. Any of these options can be added to your config file.
With the REST API, you can use mailing for email templating even if most of your app is not written in TypeScript or JavaScript.
GET /api/render
takes a template name and props and returns your rendered HTML ready to be sent. Example
GET /api/previews
returns the list of previews. Example
GET /api/previews/[previewClass]/[previewFunction]
returns the rendered preview. Example
Want to improve Mailing? Incredible. Try it out, file an issue or open a PR!
git clone git@github.com:sofn-xyz/mailing.git
cd mailing
yarn
yarn dev
yarn dev
starts the cli in dev mode
For development, you may want to have a demo next app that pulls in your changes. We've had success using yalc[https://github.com/wclr/yalc] and the following flow:
- Register
mailing
as a local package withyalc
: in thepackages/cli
directory, runyalc add
. - Create a new next app in your projects directory by running
yarn create next-app --typescript
for a typescript app ORyarn create next-app
for a js app - In the next app, run
yalc add mailing
, this createsnode_modules/mailing
andnode_modules/.bin/mailing
. (Note:yarn link
does not add the bin file, which is whyyalc
is prefered) - Make your changes in
mailing
- Run
yarn build
in themailing
root directory to create newdist
files - Run
yalc push
in themailing
root directory to both publish your changes (yalc publish
) and pull them in to your next app (yalc update
)
- Start a mailing preview server on localhost:3883
- cd into packages/cli and run
yarn cypress run
The directory scripts/e2e_test
contains smoke tests targeting supported frameworks that should be run before every public npm release. Each test uses the yarn create
command to create new projects from their create-*
starter kits and then runs the cypress cli tests contained in packages/cli/cypress
. The frameworks currently covered by the tests are:
- Next.js (typescript)
- Next.js (javascript)
Initial test setup
- In the project root, run
asdf install
to install ruby 3.1.2 - In the directory
scripts/e2e_test
, runbundle install
to install the required ruby gems
Run the smoke tests
- In the directory
scripts/e2e_test
, runbundle exec ruby e2e_test.rb
The script supports some options for running:
--only=redwood_ts
to run the tests only on the specified framework--skip-build
to skip the yarn build part of the script, useful when debugging something unrelated to the build--rerun
to skip the framework install part of the script, useful when debugging something in your cypress tests unrelated to the build or the framework install. This will use the framework installs that are present in the runs/latest directory, i.e. the assumption is you've run a test against some framework(s) and you now want to re-running them after adjusting your cypress tests.
Cache the framework installs for faster runs
- In the directory
scripts/e2e_test
, runbundle exec ruby e2e_test.rb --save-cache --skip-build
to save each framework install (before mailing is added) to thecache
directory. Subsequent test runs will use the cache instead of runningyarn create
andyarn install
, which will speed things up 🏎