Better JAMstack Email with Cloud Functions & Mailgun

Posted: February 18th, 2019

Topics: Gatsby, Netlify, Mailgun, Cloud Functions, JAMstack

Email is super annoying, everone knows this. That might be a bit of an exaggeration, but every time I find an easy way to tackle email I feel like I should tell someone, so here we are.

For the longest, I would add email transactions to small projects by creating a little dummy gmail account and use NodeMailer to tie it all together. It works fine in most cases, but sometimes the "Less Secure Apps" thing screws up, sometimes Google think you're a bot -- too many sometimeses for me. Netlify also has a "Forms" offering, but I'm not a fan.

I actually tried out some NodeMailer/Mailgun transporter extension package at first before realizing that mailgun-js is offered as a standalone wrapper.

Enter Mailgun

Mailgun is a super simple email provider with a generous free tier (10,000 emails/month), way more than we'll need for the average side project.

You'll need a few things to make the best of this particular solution:

  • A domain name
  • Some level of DNS knowhow
  • A Netlify account & ideally a Gatsby build

Ok, so let's chat a little bit about the architecture of this scenario. This would work similarly with any interpretation of the JAMstack, but this example actually refers to this website, which consists of:

  • Gatsby Frontend (Contact Form)
  • Netlify Static Hosting
  • Netlify Cloud Functions

We're going to grab some basic contact form data on the frontend of our Gatsby/React app, shoot it to one of our API endpoints generated by our Netlify intagration, then process it within our cloud function with Mailgun.

First, we'll need a Mailgun account. Sign up for a free one, then add your domain if you want. You'll neet to add a few MX, TXT & CNAME records for it to work properly. You can probably use a vanity URL generated by Mailgun though.

Then, we'll need a cloud function. In this case, Netlify calls for your functions to be build within your repo, so within a functions folder, I'll create contact.js. Worry not, this would work with any cloud function host.

Here's what that looks like:

// Gatsby uses weird config stuff
require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`,
})

// Connect to our Mailgun API wrapper and instantiate it 
const mailgun = require('mailgun-js')
const mg = mailgun({
  apiKey: process.env.MAILGUN_API_KEY,
  domain: process.env.MAILGUN_DOMAIN,
})

// Response stuff
const successCode = 200
const errorCode = 400
const headers = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'Content-Type',
}

// Our cloud function
exports.handler = function(event, context, callback) {
  let data = JSON.parse(event.body)
  let { name, email, subject, message } = data
  let mailOptions = {
    from: `${name} <${email}>`,
    to: process.env.MY_EMAIL_ADDRESS,
    replyTo: email,
    subject: `${subject}`,
    text: `${message}`,
  }
  
  // It's really as simple as this, 
  // directly from the Mailgun dashboard
  
  mg.messages().send(mailOptions, (error, body) => {
    if (error) {
      console.log(error)
      callback(null, {
        errorCode,
        headers,
        body: JSON.stringify(error),
      })
    } else {
      console.log(body)
      callback(null, {
        successCode,
        headers,
        body: JSON.stringify(body),
      })
    }
  })
}

Now that you've got your cloud function set up, and an endpoint exposed, all we need to do now is hit it with some data. I always test out my functions before hand using Postman, but I'll skip that step. Your basic frontend React contact form logic could look something like this:

handleSubmit = e => {
    this.setState({ loading: true })
    let { name, email, subject, message } = this.state
    let data = { name, email, subject, message }
    axios.post(endpoints.contact, JSON.stringify(data)).then(response => {
      if (response.status !== 200) {
        this.handleError()
      } else {
        this.handleSuccess()
      }
    })
    e.preventDefault()
  }

  handleSuccess = () => {
    this.setState({
      name: '',
      email: '',
      message: '',
      subject: '',
      loading: false,
      error: false,
    })
  }

  handleError = msg => {
    this.setState({
      loading: false,
      error: true,
      msg
    })
  }

I was pretty happy about how quick & easy this was. Up and running in about 10 minutes with one NPM package and a tiny cloud function. Enjoy!