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
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"
]
}
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>'
});
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
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" }
}
NOTE: to set this value in production later you can use the command below.
$ firebase functions:config:set mg.key="your-mailgun-api-key"
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;
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
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>
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
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
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),
});
};
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"
},
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 };
}
});
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"
},
Libraries Mentioned
- https://github.com/mailgun/mailgun-js
- https://www.npmjs.com/package/@types/mailgun-js
- https://github.com/handlebars-lang/handlebars.js
- https://github.com/mysticatea/npm-run-all
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!
Top comments (0)