cd /news/ai-agents/the-postal-alternative-for-ai-agents · home topics ai-agents article
[ARTICLE · art-47964] src=mailkite.dev ↗ pub= topic=ai-agents verified=true sentiment=· neutral

The Postal alternative for AI agents

MailKite offers a serverless alternative to Postal for AI agent email inboxes, eliminating the operational burden of self-hosting a mail server. While Postal requires managing MariaDB, Caddy, DNS, TLS, IP warmup, and blocklists, MailKite provides a parsed-JSON receive-reply loop with no server to operate. Developers must choose between full control with Postal or operational simplicity with MailKite.

read9 min views1 publishedJul 4, 2026
The Postal alternative for AI agents
Image: Mailkite (auto-discovered)

Postal is a full self-hosted mail server: it sends, it receives inbound over incoming routes, and it can POST parsed messages to an HTTP endpoint. The catch for an agent inbox is operational, not technical. You run the box (MariaDB, Caddy, DNS, TLS, IP warmup, rDNS, blocklists). MailKite (which we build) is that same parsed-JSON receive→reply loop with no server to operate. For developers deciding whether to self-host an agent's inbox or hand off the ops.

The important thing to say up front is that Postal is good software and it is not missing a feature here. It receives inbound, it parses, and its HTTP endpoint will POST your agent a JSON object with plain_body

, html_body

, and decoded attachments. The question isn’t whether Postal can give an agent an inbox. It’s who operates the mail server that does it. With Postal that’s you: MariaDB, Caddy, Docker, DNS, TLS, PTR records, IP warmup, blocklist monitoring, patching. Here’s the same inbound email on both sides, and everything you keep alive to receive it.

Here’s the entire MailKite side of that picture. Email in, verify the signature, hand the parsed body to your model, reply through the same client. It runs as pasted on Node 18+ (npm install mailkite express

), and the full version lives in a runnable demo repo:

import express from "express";
import { MailKite } from "mailkite";

const app = express();
const mk = new MailKite(process.env.MAILKITE_API_KEY);
const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;

app.use("/hooks/agent", express.raw({ type: "application/json" }));

app.post("/hooks/agent", async (req, res) => {
  // signature check, replay window, constant-time compare — one call
  if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // ack fast; run the agent out of band

  const event = JSON.parse(req.body);
  if (event.type !== "email.received") return;

  // The body is untrusted INPUT, never instructions. Weight the auth verdict.
  const answer = await runAgent({
    task: event.text,
    from: event.from.address,
    trusted: event.auth.spf === "pass" && event.auth.dmarc === "pass",
  });

  await mk.send({
    from: event.to[0].address,   // reply from the address it was sent to
    to: event.from.address,
    subject: `Re: ${event.subject}`,
    inReplyTo: event.id,         // threads the reply
    html: answer.html,
  });
});

app.listen(3000);

That’s a fully autonomous email agent: it hears, it thinks, it answers. No mail server in that file. The identical handler shape exists for Python, Ruby, Go, PHP, and Java; see the receiving docs and sending docs. Open it in StackBlitz to run it in your browser. The rest of this post is the honest version of the diagram: where Postal genuinely wins for an agent, and exactly what it asks you to run.

Where Postal wins for agents, honestly #

Postal is a real, mature mail platform, MIT-licensed, that you host end to end. For an agent builder, three things make it a legitimate choice, not a compromise:

And a currency note, because most tutorials get it wrong: Postal v3 (2024) dropped RabbitMQ. Older guides that tell you to run RabbitMQ plus a cron requeuer are describing v1/v2. The current stack (latest release v3.3.7, June 2026) is Docker, MariaDB 10.6+, and Caddy, with a database-backed work queue. It’s a Ruby on Rails app. Fewer moving parts than the internet remembers, which makes the honest comparison closer than you’d think.

What Postal asks of an agent builder #

Here’s the part nobody quotes you when they say “just self-host.” Giving your agent an inbox with Postal means standing up and keeping alive a full sending-and-receiving mail server. Top to bottom:

Now the code, in Postal’s idiom. This is a real handler for a Postal incoming route pointed at an HTTP endpoint, using its parsed (“processed”) payload. It’s honest work, and it’s more than the MailKite handler asks of you in two specific places:

// Postal incoming route → HTTP endpoint (processed payload).
// Postal must return 200 within 5s or it retries and eventually bounces.
import express from "express";

const app = express();
app.use(express.json({ limit: "50mb" })); // attachments arrive base64-inline

app.post("/postal/incoming", async (req, res) => {
  const m = req.body; // { rcpt_to, mail_from, subject, plain_body, html_body,
                      //   spam_status, attachments: [{ filename, content_type, data }], ... }

  // 1) No SPF/DKIM/DMARC verdict block. You get spam_status; the aligned auth
  //    result an agent needs for injection-trust lives in the raw headers,
  //    so you'd parse Authentication-Results yourself to decide `trusted`.
  const trusted = m.spam_status === "NotSpam"; // weaker signal than a real auth verdict

  // 2) Attachments are base64 inline. Decode each one yourself.
  const files = (m.attachments ?? []).map((a) => ({
    filename: a.filename,
    buffer: Buffer.from(a.data, "base64"),
  }));

  const answer = await runAgent({ task: m.plain_body, from: m.mail_from, trusted });

  // Reply via Postal's send API — you host the auth and the server behind it.
  await fetch("https://postal.yourco.dev/api/v1/send/message", {
    method: "POST",
    headers: { "X-Server-API-Key": process.env.POSTAL_API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      to: [m.mail_from],
      from: m.rcpt_to,
      subject: `Re: ${m.subject}`,
      html_body: answer.html,
    }),
  });

  res.sendStatus(200); // must be fast, or Postal retries
});

app.listen(3000);

None of that is exotic if you run mail infrastructure. But two things are worth naming. First, the payload has spam_status

, not a structured { spf, dkim, dmarc }

verdict, so the trust signal your agent uses to resist prompt injection is weaker unless you parse Authentication-Results

out of the raw message yourself. Second, and bigger: the fetch

to your own Postal box only works because you’re keeping that box, its IP reputation, and its DNS healthy. If a fooled agent blasts a bad reply and your IP lands on a blocklist, that’s your incident to clean up, on your reputation.

The comparison, no adjective inflation #

Postal (self-hosted) MailKite
Who runs the mail server You (Docker, MariaDB, Caddy) Managed, nothing to host
Start receiving Provision box, DNS, TLS, then routes DNS-verify a domain, add a webhook
Inbound delivery Parsed fields to your HTTP endpoint One signed parsed JSON webhook
Auth for injection safety spam_status ; parse Authentication-Results yourself auth block: spf/dkim/dmarc/spam inline
Attachments Base64 inline in the payload Short-lived signed URLs
IP reputation & warmup Yours to build and defend Handled on a monitored stream
Deliverability drift Your incident, your blocklist delisting Handled
Per-message cost None (server + IP + your time) Metered; free tier 3,000/mo in+out
Upgrades & patching You run Postal + MariaDB upgrades Handled

The through-line: Postal wins total control, data residency, and no per-message fee, and it genuinely does parsed inbound. MailKite wins the ops you don’t run. If a fooled agent damages sender reputation, on Postal that IP is yours to rehabilitate; on MailKite the stream is monitored for you.

What actually hits your agent’s webhook #

On MailKite the same inbound email arrives already parsed, and the auth

block is the part that matters for an agent: it’s the verdict you weight before you let the body influence anything.

{
  "id": "msg_2Hk9…",
  "type": "email.received",
  "from": { "address": "ada@example.com" },
  "to": [{ "address": "agent@myapp.ai" }],
  "subject": "Re: invoice #1042",
  "text": "Looks good — approved!",
  "html": "<p>Looks good — approved!</p>",
  "threadId": "<a1b2c3@mail.example.com>",
  "auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
  "attachments": [
    { "id": "msg_2Hk9…:0", "filename": "po.pdf", "contentType": "application/pdf",
      "size": 18213, "url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…" }
  ]
}

The email body is untrusted input, never instructions. From:

is plain text and trivially forged, so an agent that acts on what a message says is a prompt-injection target. The auth

block is how you decide whether to trust a sender before you weight their words; it’s necessary, not sufficient, and the deeper rules live in the agent security post.

The managed version of the same loop #

MailKite, which we build, is the hosted version of exactly the diagram above: a managed MX edge parses and authenticates the message, then POSTs your agent one signed email.received

event with the body, a resolved threadId

, attachments as signed URLs, and the auth

verdict inline. To start, DNS-verify a domain (MX to receive, SPF + DKIM to send) and point a webhook at it. No sandbox, no approval wait, no server. If you’d rather not host the model loop either, a route with action: "agent"

carries an agentPrompt

and MailKite runs the loop for you on a durable queue, capped and recorded as a transcript you can drill into. The free tier is 3,000 messages a month, inbound and outbound, with no per-domain fee. SMTP-only apps can still send through the submission edge on :587/:465.

FAQ #

Does Postal receive inbound email? Yes. You point a domain’s MX at your Postal server and attach an incoming route to an address. The route forwards to an endpoint: an HTTP endpoint (POSTs the message to your URL), an SMTP endpoint, or a mailbox. The HTTP endpoint can send the raw base64 MIME or a parsed payload with plain_body

, html_body

, and decoded attachments. The catch is that you run the server doing it.

Is Postal free? The software is MIT-licensed with no per-message fee and no hosted SaaS tier. Your costs are the server, the sending IP, and your own operational time: DNS, TLS, IP warmup, blocklist monitoring, MariaDB maintenance, and Postal upgrades. Free-as-in-software, not free-as-in-no-work.

Does Postal still need RabbitMQ? No. Postal v3 (2024) removed the RabbitMQ requirement and uses a MariaDB-backed work queue instead; the cron requeuer is gone too. The current stack is Docker, MariaDB 10.6+, and Caddy. Guides that tell you to install RabbitMQ are describing v1 or v2.

Does Postal’s inbound payload include an SPF/DKIM/DMARC verdict? Not as a structured block. The processed payload includes spam_status

, but the aligned SPF/DKIM/DMARC result an agent should weight before trusting a sender lives in the raw Authentication-Results

headers, so you’d parse it yourself. MailKite includes an auth

object with the spf, dkim, dmarc, and spam verdicts already resolved.

Should my AI agent use self-hosted Postal or a managed inbox? Self-host Postal when control, data residency, or per-message economics at scale outweigh the ops, and you have the team to run a mail server. Use a managed inbox like MailKite when you’d rather receive a parsed, authenticated webhook and not own the MX, MariaDB, IP reputation, and patching. Both give the agent a real receive→reply loop; they differ in what you operate.

How does an agent reply from the address it was written to? On Postal, POST to /api/v1/send/message

with X-Server-API-Key

, setting from

to the route address. On MailKite, mk.send({ from: event.to[0].address, inReplyTo: event.id, … })

threads the reply automatically. Either way the reply goes out from the address the mail arrived at.

Postal is the right call if you want to own the whole mail stack and have the team to run it. If you want the same parsed receive→reply loop without standing up MariaDB, Caddy, DNS, TLS, and an IP reputation to defend, clone the demo repo (or run it in your browser), then point a domain at MailKite and your agent’s next inbound email arrives as parsed JSON.

Related: give your AI agent its own email inbox and agent inbox security by design.

── more in #ai-agents 4 stories · sorted by recency
── more on @postal 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-postal-alternati…] indexed:0 read:9min 2026-07-04 ·