DEV Community

loading...

How to send transactional HTML emails with Mailgun, Handlebars and Firebase

codechips profile image Ilia Mikhailov Originally published at codechips.me on ・6 min read

If you are building any serious app chances are big that you have to send some emails. I needed to send transactional emails for a web app I am building, so I reached for the trusted Mailgun.

The app itself is based on Firebase and Firebase Functions was an obvious choice for sending emails. What was not so obvious is how to put everything together. I went through the pain and finally figured it out.

Below are my notes on how I got everything to work with TypeScript using Mailgun, Handlebars templates for HTML emails and Firebase Functions.

Firebase Functions Setup

I assume here that you already have a Firebase project set up with the local Firebase emulator running. If not, you can follow my guide - Smooth local Firebase development setup with Firebase emulator and Snowpack. It targets Svelte, but you can skip those parts if you are using some other framework or technology.

Mailgun Setup

There are many transactional email providers, but I chose Mailgun. Mostly because of their cool name and logo, and also because they have a generous free quota (5K emails/month) and a nice Javascript SDK.

I won't go through setting and configuring Mailgun, DNS and all that jazz. They already have good documentation on how to do it. Instead, I will concentrate on wiring everything up in code.

There are several Mailgun libraries on NPM, but we will go with the official one. It's written in Javascript and if you are using Typescript you have to install the types.

$ npm add mailgun.js @types/mailgun-js
Enter fullscreen mode Exit fullscreen mode

Because Mailgun SDK is written in Javascript you have to add esModuleInterop to your tsconfig.json.

{
  "compilerOptions": {
    "module": "commonjs",
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "esModuleInterop": true,
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "target": "es2017"
  },
  "compileOnSave": true,
  "include": [
    "src"
  ]
}
Enter fullscreen mode Exit fullscreen mode

All right! We are ready to roll!

Writing the email sender function

To keep code clean we will keep the email sending logic in a separate file. Create a new email.ts file in the src directory.

// email.ts

import * as functions from 'firebase-functions';
import Mailgun from 'mailgun-js';

const apiKey = 'mailgun-api-key';
const domain = 'mg.example.com';

const mg = new Mailgun({ apiKey, domain });

export const send = (subscribers: string[]) =>
  mg.messages().send({
    from: 'Mailgun Test <noreply@mg.example.com>',
    to: subscribers,
    subject: 'Mailgun test',
    text: 'Hello! How are you today?',
    html: '<h1>Hello!</h1><p>How are you today?</p>'
  });
Enter fullscreen mode Exit fullscreen mode

So far, so good. With this code we can send an email to a bunch of subscribers whose email addresses are passed in as a list of strings.

Firebase Remote Config

Mailgun SDK requires an API key to send email. While you can hardcode it in your code, as we did in the example above, you should never do it. Instead, we can leverage Firebase Remote Config for this and keep our Maingun API key securely stored in it.

$ firebase functions:config:get > .runtimeconfig.json

Enter fullscreen mode Exit fullscreen mode

If you already don't have anything there you will get and empty JSON file. Let's add our Mailgun API key to it.

{
  "mg": { "key": "your-mailgun-api-key" }
}
Enter fullscreen mode Exit fullscreen mode

NOTE: to set this value in production later you can use the command below.

$ firebase functions:config:set mg.key="your-mailgun-api-key"
Enter fullscreen mode Exit fullscreen mode

Also, make sure to keep all your config keys lowercase or Firebase will complain!

Now, when the Firebase emulator starts it will pickup the local config. This means that we can get the key from the functions config.

// email.ts

import * as functions from 'firebase-functions';
// ...

const apiKey = functions.config().mg?.key;
Enter fullscreen mode Exit fullscreen mode

Our basic email function is now done. Let's continue with setting up our Handlebars templates.

HTML email templates with Handlebars

HTML email design is hard. It's like going back to the 90s again. Tables and all. Luckily there are plenty of apps that can help you with that. Maybe you even have an old copy of Adobe Dreamweaver laying around somewhere? If you a feeling adventurous.

Jokes aside, if you want to learn more about HTML email design I can highly recommend reading An Introduction To Building And Sending HTML Email For Web Developers on Smashing Magazine.

But, we are aiming for the MVP here as this article's focus is on the technical implementation and not on email design.

My goal was to keep the email templates separate from code and Handlebars is a good fit for that. With that said, it was not as straight forward as I first thought.

Handlebars.js is one of the templating languages that allows you to precompile your templates, so you can later import them in code. It took me a while to get it right, but here is how you do it.

First, let's install Handlebars and npm-run-all utility package that we will use for the precompile step.

$ npm add handlebars npm-run-all
Enter fullscreen mode Exit fullscreen mode

Next we need to create two Handlebars templates. One for html emails and one for text emails.

Create an emails folder in the root of the project and then create two files in it - html.handlebars and text.handlebars.

{{!-- html.handlebars --}}

<h1>{{title}}</h1>

<p>{{body}}</p>

<p>All the best, John</p>
Enter fullscreen mode Exit fullscreen mode

Text template is needed for email clients that can't display HTML. I am looking at you Mutt!

{{!-- text.handlebars --}}

{{title}}

{{body}}

All the best, John
Enter fullscreen mode Exit fullscreen mode

Now that we have created the templates we need to precompile them with this command.

$ npx handlebars emails/ -f src/templates.js -c handlebars/runtime
Enter fullscreen mode Exit fullscreen mode

It compiles the templates into templates.js module and also includes the reference to handlesbars/runtime module that is needed for some reason.

Now you can import templates.js in your code and both text and html templates will be available.

// email.ts

import * as functions from 'firebase-functions';
import Mailgun from 'mailgun-js';

import * as Handlebars from 'handlebars/runtime';
import './templates';

const apiKey = functions.config().mg?.key;
const domain = 'mg.example.com';

const mg = new Mailgun({ apiKey, domain });

// import our email and text precompiles templates
const html = Handlebars.templates['html'];
const text = Handlebars.templates['text'];

export const send = (subscribers: string[]) => {
  const data = { title: 'Mailgun Test', body: 'How are you?' };

  return mg.messages().send({
    from: 'Mailgun Test <noreply@mg.example.com>',
    to: subscribers,
    subject: data.title,
    text: text(data),
    html: html(data),
  });
};
Enter fullscreen mode Exit fullscreen mode

Our email send function is now complete, but let's streamline our development environment a bit, so that Handlebars templates are precompiled automatically before we build our functions.

Change the scripts section in functions' package.json to this.

"scripts": {
    "build": "run-s build:templates build:tsc",
    "build:tsc": "tsc",
    "build:templates": "handlebars emails/ -f src/templates.js -c handlebars/runtime",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
Enter fullscreen mode Exit fullscreen mode

When we now execute npm run build Handlebars will compile the templates first before we compile function. This is done with run-s (s is for serial) which is a part of the npm-run-all utility package.

Wiring everything together

Now that we have all the different parts done, let's wire them all together.

// functions/src/index.ts

import * as functions from 'firebase-functions';
import { send } from './email';

export const sendEmail = functions.https.onCall(async data => {
  const { subscribers } = data;

  try {
    await send(subscribers);
    return { ok: true };
  } catch (err) {
    return { ok: false, error: err.message };
  }
});
Enter fullscreen mode Exit fullscreen mode

That's it! Now you can send transactional emails with Mailgun through Firebase Functions. I kept the example minimal in order to keep things simple, but you can fetch data from Firestore or send the email in the Firebase trigger functions. Imagination is the limit!

BONUS: Firebase Functions Emulator Startup

If you what to start both Firebase emulator and function compilation watch, when you start the project, change the scripts section of your main project's package.json to this.

"scripts": {
  "build": "run-p build:*",
  "build:functions": "cd functions && npm run build",
  "firebase:deploy:functions": "firebase deploy --only functions",
  "firebase:start": "firebase emulators:start --only functions",
  "start": "run-p watch:* firebase:start",
  "watch:functions": "cd functions && npm run watch"
},
Enter fullscreen mode Exit fullscreen mode

Libraries Mentioned

Conclusion

This is just one way to send transactional emails from Firebase functions. To keep templates separate from code we had to precompile our email templates as Firebase functions can't deal with file system very well. I am sure there are other templating languages that support precompilation, but Handlebars is good enough for the task.

You can find complete example code on Github.

https://github.com/codechips/firebase-functions-mailgun-handlebars-example

Thanks for reading!

Discussion

pic
Editor guide