Published on

My experience with Postmark

Authors

About Postmark

Postmark is an email delivery service that focuses on reliable and lightning-fast transactional email. Think password resets, user signups, order confirmations—not your marketing newsletters, but the kind of emails users expect to receive instantly.

Postmark-logo

Why I used Postmark

For a project, I needed something which can receive and process mails from clients. The mails would be automatically parsed and the results would be stored in a database somewhere. And then, I can integrate that data into my main application.

Inbound mail parsing is a core feature of Postmark. They let me set up an inbound email address. When Postmark receives an email sent to my address, they process it and deliver it to my application as a JSON object via a webhook. This structured data makes it easy to integrate inbound mail functionality into my main application.

Postmark's generous free tier is a good place to start. And it has a lot of cool features.

Inbound email parsing

Inbound email processing lets me receive and parse emails sent to a specific address. For example, when a user replies to a support email like reply@yourdomain.com, Postmark can capture that message, parse it (subject, body, sender, etc.), and send it to a webhook of your choice.

Integration with Supabase

I integrated Postmark with Supabase to process inbound emails and store the data in a Postgres table. Supabase is usually my go-to open source backend because it provides a Postgres tables + REST API. And I can configure my Supabase edge function for the custom webhook.

The best thing is that I can configure the whole thing once and forget about it. Supabase and Postmark will keep doing their thing behind the scenes.

Workflow

Integration experience

#1 Setting up Postmark

Postmark has 3 message streams - Broadcast, Transactional and Inbound. Transactional message streams are for messages sent to a single recipient and triggered by a user action like a welcome email, password reset, or receipts. Transactional streams do not support bulk messages. Broadcast message streams are for bulk messages sent to multiple recipients like announcements, newsletters, or other application email.

I needed something to process my inbound mails so I will be using the Inbound Stream.

Creating a Postmark account was easy. After I set it up, the Postmark team verified my account within a day. A server named "My First Server" was created by default. I can set up additional servers and every server has its own API token.

Each Postmark Server comes with an associated inbound message stream. The inbound email address will be in the format of a GUID@inbound.postmarkapp.com. Ideally, I need to set up some kind of inbound domain forwarding and not use this email directly. Postmark also keeps track of the MailboxHash to keep track of the mail thread in case I get multiple mails from the same sender.

For example, I can set up my DNS so that all mails to my app are send to Postmark's inbound stream. Postmark will then process these mails and forward them to a webhook of my choice. In other words, the email never “lands” in a traditional inbox; it’s captured and routed via Postmark and delivered to my app.

Ideally, this should be done as it's not safe to publicly share my Inbound Postmark address.

In order for my application to receive the parsed email, I had to specify a webhook URL where the HTTP POST request will be made. That URL will come from Supabase.

Before setting up the Supabase URL, I tested out the email parsing using a RequestBin from Pipedream (https://pipedream.com). Pipedream provides a nice ready-to-use interface for testing.

#2 Setting up Supabase

I set up a new project in Supabase and created a simple table to store the email content. After that, I wrote an edge function using the Supabase editor (web version) and deployed it in my project.

Now, I needed to think about how I was going to receive the mail (the format, attachments etc.). Based on that, I needed to figure out how to process the mail content.

The first thing that comes into mind would surely be to do it through an LLM call. But, I don't really feel that confident leaving something important to an AI. So, I decided to hardcode the logic (at least, at first). Right now, the Javascript looks something like this.

script.js
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
serve(async (req)=>{
  if (req.method !== "POST") {
    return new Response("Method Not Allowed", {
      status: 405
    });
  }
  let data;
  try {
    data = await req.json();
  } catch (e) {
    return new Response("Invalid JSON", {
      status: 400
    });
  }
  // Extract the text and HTML body
  const textBody = data.TextBody || "";
  const htmlBody = data.HtmlBody || "";
  // You can now process/store these as needed.
  // For demonstration, we'll just return them in the response.
  return new Response(JSON.stringify({
    textBody,
    htmlBody
  }), {
    headers: {
      "Content-Type": "application/json"
    },
    status: 200
  });
});

Might not look like it does much (yeah, it doesn't). Looks like I might have to use AI based parsing after all! Since API inference is available in Supabase functions, I am planning to use that. I haven't yet though of how that will work.

When I deploy this function, I get a URL which serves as my webhook endpoint.

Will all that completed, I will have a nice little pipeline set up for getting the content from inbound mails into a neatly formatted table.

#3 Setting up basic HTTP authentication

To make sure no one can access my webhook without authorization, I'll have to set up some basic HTTP authentication. I'll be able to do this by adding the credential in the inbound webhook URL.

For example, https://username:password@domain.com/hook

HTTP basic authentication is insecure over unencrypted HTTP. Inbound webhooks should only be used over HTTPS.

Final thoughts

If you’re still using your default SMTP server for transactional emails, I urge you to consider Postmark. It’s one of those tools that quietly does its job exceptionally well.