Notification of a delivery failure in Mailgun

When you use an email service provider like Gmail, you get delivery failure emails. These emails are great because it enables you to reach out to your intended recipient in another way, or just retry the email later on.

Because I use Mailgun with Gmail’s Send Mail As feature for my custom domains, I needed to implement a solution that handles delivery failures.


I’m cheap, so I host my domains with Cloudflare as they don’t charge a markup – I pay what they pay. Cloudflare recently introduced email forwarding which is great. I used to send and forward emails with Mailgun on their free plan, but they removed the forwarding feature right around the time Cloudflare introduced theirs, lucky me.

I ran into the situation of sending multiple chaser emails to someone because I thought they’d just not got back to me. It turned out my emails just weren’t getting through but I didn’t know. I then started down this path.

Mailgun offers webhooks for many different events. Now I just had to build something to handle the delivery failed event. I’d built some Lambda functions before and Netlify functions but Cloudflare Workers was new at the time, so I decided to build it on that instead.

How I built it

To start, I installed Wrangler, Cloudflare’s development CLI, and got a project set up. A Cloudflare Worker runs a short snippet of code on the edge. I wanted to use Node, because JavaScript is a language I’m most comfortable in, so had to enable the node_compat flag on the worker to allow various methods inside the crypto functions to work. I also set up CI using a GitHub action so each time I push changes to GitHub, the worker gets built and deployed to the global Cloudflare network. I used the cloudflare/wrangler-action package to do this.

A simple overview of how it works is the worker receives a request and parses its body. If there is no request body, then the worker returns a 405 bad request status code. With the body, we create a hmacDigest from the body timestamp, token, and a special signing key that we get from the Mailgun dashboard. I’m using the crypto-js library to do this. We compare this calculated digest with the signature in the request body to make sure they match. This authenticates the request to ensure only our Mailgun account can send webhooks to this Worker. We also cache this digest, along with the URL so that we can see whether it has been used before. Mailgun wouldn’t send a webhook event twice, so it must be someone trying to get information by replaying an old webhook event. By validating whether we’ve processed the digest before, we can prevent replay attacks.

// Make sure a body is included
let body
try {
    body = await request.json()
} catch(e) {
    return new Response("Bad request", { status: 405 })
// Verify that the Mailgun Signature matches the one that they sent us
const hmacDigest = hex.stringify(hmacSHA256(body.signature.timestamp + body.signature.token, env.MAILGUN_SIGNING_KEY))
// Load Cloudflare Cache
const cache = caches.default
// Set Cache Key for this signature = https://worker.domain/signature
const cacheKey = request.url + hmacDigest
// Ensure the signature has not been used already
const alreadyUsedSignature = await cache.match(cacheKey)
if (alreadyUsedSignature !== undefined) {
    return new Response("This is a replay attack. The signature has been used before", { status: 401 })
if (hmacDigest !== body.signature.signature) {
    return new Response("Could not verify signature", { status: 406 })

Now we have all of the security out of the way, we get to actually processing the error message received from Mailgun. First, we get the recipient, so we know who we need to email back seeing as they didn’t receive our message. Then we grab the sender, as it may be you manage more than 1 domain so you need to know who sent the email. The email subject helps too as it may be you sent more than 1 email to the same person. We also want to be informed of the error message, so we get that too.

To allow us to send mail, we use Mailgun’s HTTP API, configured using env variables. We have one for what email should send these notifications, and one for who the notifications should go to. We then just need a Mailgun API key to allow us to send the email. This API key should match the domain you’d like to send email from.

// Set up the email to send
const mailOptions = {
    from: `Galexia Mail Reporting <info@${env.DOMAIN}>`,
    subject: "New delivery failure in Mailgun",
    text: `
An email to:


With a subject of:

Has failed.

The error message was:
${body['event-data']['delivery-status'].description || body['event-data']['delivery-status'].message}
// Convert the email JSON to FormData
const form_data = new FormData()
for (var key in mailOptions) {
    form_data.append(key, mailOptions[key]);
// Send the email
const sendEmail = await fetch(`${env.DOMAIN}/messages`, {
    method: 'POST',
    body: form_data,
    headers: {
        'authorization': `Basic ${new Buffer('api' + ':' + env.MAILGUN_API_KEY).toString('base64')}`,
        'accept': 'application/json'

Setting this all up gets me delivery failure messages like this:

Of course, if you get an error sending that email, you could be stuck in a loop and still get silent failures, so I’ve set my recipient to a Gmail address which has pretty good availability.

In summary, having this worker set up allows me to catch any emails that don’t get delivered, and then be able to dive into the Mailgun logs to debug the issue. On the flex plan, logs are only stored for 5 days, so having this webhook set up gives me near instant error messages and allows me to action any issues quickly.

To build on this, I’d want to set up something to notify me of forwarding failures from Cloudflare’s free email forwarding.

Southampton Focus

Southampton Focus is the online place to go for finding businesses in and around the area. Businesses pay for a listing on the site and get free access to Southampton Focus’ vast social network presence.

Southampton Focus already had a site but decided to scrap it and start completely afresh. They chose a theme from Theme Forest and I configured WordPress and the theme to their specifications. I had to customize the theme quite a bit to get it to the functionality that was advertised on Theme Forest, and we used many different plugins together to give the site company listings functionality.

Overall, I’m really pleased with how it turned out considering the scope of the site.

Stakks Pancake House

This Pancake House in Southampton wanted a complete website redesign. They got Mirror Digital Media to design the site and I built a custom WordPress theme from the designs. I then migrated all of the old blog posts to the new website and made a couple of small design changes that weren’t finalized in the initial design before pushing the website live. I used Local by FlyWheel to develop the site but there was a lot of hassle with sending an ngrok link every day for the client to view the site. In the future, I’m just going to create sites under a subdomain on my Digital Ocean Droplet and work on them there so that my client will be able to access them at all times.

Train In Blocks

I started working with Mikey Lau, founder of Train In Blocks in April 2020. He wanted to create a web app for personal trainers to manage their clients. We had a lot of project meetings to work out how best to implement such an app and I was given complete control of the implementation. I decided Okta was a good fit for Authentication, and we were going to use Stripe to process payments. As I love Vue I decided to use that on the front end, and I have quite a bit of experience with PHP so used that, along with slim, to create an API for updating content in a MySQL database.

Initially, I did a lot of the front-end work, but I taught Mikey HTML, CSS, and Vue and started to move more into a project manager role and backend developer. I created a Stripe WebHook to handle sign-ups which then creates a user in Okta and sends the customer an email about their new account. We also created a site in Nuxt for sign-ups to take place and for potential customers to learn more about the application.

TV Shows App

I like to keep track of the TV Shows I watch. I used to do this in a Word Document and manually go to IMDb and check whether the show was running or had been cancelled and then add the TV Show to my calendar. I decided to build an app that would do this for me.

I decided to try and learn a new framework whilst creating this fun little side-project. I’ve heard good things about Svelte so decided to give it a shot. It’s a framework very much like Vue (which I love by the way). It splits it’s template files into CSS, HTML, and JavaScript which made it easy to get the hang of. It doesn’t come with something like Vue data, and there’s a weird thing where you need to tell the framework a variable has changed by doing variable = variable. Svelte devtools aren’t anywhere near as intuitive either. These small little things are the price you pay for infinitely better load times. All the processing is done in a compile step, whereas, Vue does all it’s processing in the browser. It’s a framework I’d go to for a simple static site that might have some components that are easier to create in a framework than with vanilla JavaScript. For most of my projects though, I’ll be sticking to Vue.

My TV Show app is behind somewhat of a firewall, Cloudflare Access. I didn’t want the hassle of rolling an authentication system with my simple app so I decided to use Cloudflare Access instead. Otherwise, anyone would be able to add or remove TV Shows from my list. If you got to you’ll see that you have to log in with Cloudflare first.

The app queries the episodate API when you perform a search and stores the added TV Show in a Fauna Database. It does this by using a Lambda function hosted on Netlify. There are also Lambdas for removing and viewing saved TV Shows.

The app then queries the Fauna Database for all currently watching TV Shows and fetches information on them from the episodate API. The TV show is green for currently airing. Red for cancelled/finished. And has no colour for shows that haven’t announced new episodes yet.

The app exports all of the episodes set in the future to an iCal file which you can download and import into any calendar software. This is still a bit of a manual task and I wanted to automate it further so I created another Lambda function that fetches the TV Show information, and serves the calendar. This was quite a tricky process as I had to learn a lot more about Lambdas than just copying the FaunaDB examples. I got it working but it takes about 16 seconds to query all of the TV Shows I watch, longer than the 10-second execution limit on Netlify. I shopped around for another solution and came across Serverless, which makes deploying on AWS simpler. It only needs about 100mb of memory so I adjusted that limit accordingly.

If I were to make this a production app I’d probably cache the API in Local Storage or in the Fauna Database and find a way of not having to query every TV Show when something is removed or added.

If you want to get set up you can fork my repository and add a .env file containing your FaunaDB secret. Or, if you want a Lambda function acting as a webCal you can fork that repo and query any API you like. It uses ics.js to create the webCal.

Classics Branding Exercise

For one of our electives in media at Sixth Form, we had to create an advertising campaign for a product of our choice. I designed a racing video game and came up with a poster, website, and adverts.