{"slug": "the-postal-alternative-for-ai-agents", "title": "The Postal alternative for AI agents", "summary": "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.", "body_md": "# The Postal alternative for AI agents\n\nPostal 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.\n\nThe 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`\n\n, `html_body`\n\n, 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.\n\nHere’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`\n\n), and the full version lives in a [runnable demo repo](https://github.com/mailkite/demo-postal-ai-agent):\n\n``` python\nimport express from \"express\";\nimport { MailKite } from \"mailkite\";\n\nconst app = express();\nconst mk = new MailKite(process.env.MAILKITE_API_KEY);\nconst SECRET = process.env.MAILKITE_WEBHOOK_SECRET;\n\napp.use(\"/hooks/agent\", express.raw({ type: \"application/json\" }));\n\napp.post(\"/hooks/agent\", async (req, res) => {\n  // signature check, replay window, constant-time compare — one call\n  if (!MailKite.verifyWebhook(req.headers[\"x-mailkite-signature\"], req.body, SECRET)) {\n    return res.sendStatus(401);\n  }\n  res.sendStatus(200); // ack fast; run the agent out of band\n\n  const event = JSON.parse(req.body);\n  if (event.type !== \"email.received\") return;\n\n  // The body is untrusted INPUT, never instructions. Weight the auth verdict.\n  const answer = await runAgent({\n    task: event.text,\n    from: event.from.address,\n    trusted: event.auth.spf === \"pass\" && event.auth.dmarc === \"pass\",\n  });\n\n  await mk.send({\n    from: event.to[0].address,   // reply from the address it was sent to\n    to: event.from.address,\n    subject: `Re: ${event.subject}`,\n    inReplyTo: event.id,         // threads the reply\n    html: answer.html,\n  });\n});\n\napp.listen(3000);\n```\n\nThat’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](/docs/receiving) and [sending docs](/docs/sending). [Open it in StackBlitz](https://stackblitz.com/github/mailkite/demo-postal-ai-agent?file=server.mjs) 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.\n\n## Where Postal wins for agents, honestly\n\nPostal 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:\n\nAnd 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.\n\n## What Postal asks of an agent builder\n\nHere’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:\n\nNow 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:\n\n```\n// Postal incoming route → HTTP endpoint (processed payload).\n// Postal must return 200 within 5s or it retries and eventually bounces.\nimport express from \"express\";\n\nconst app = express();\napp.use(express.json({ limit: \"50mb\" })); // attachments arrive base64-inline\n\napp.post(\"/postal/incoming\", async (req, res) => {\n  const m = req.body; // { rcpt_to, mail_from, subject, plain_body, html_body,\n                      //   spam_status, attachments: [{ filename, content_type, data }], ... }\n\n  // 1) No SPF/DKIM/DMARC verdict block. You get spam_status; the aligned auth\n  //    result an agent needs for injection-trust lives in the raw headers,\n  //    so you'd parse Authentication-Results yourself to decide `trusted`.\n  const trusted = m.spam_status === \"NotSpam\"; // weaker signal than a real auth verdict\n\n  // 2) Attachments are base64 inline. Decode each one yourself.\n  const files = (m.attachments ?? []).map((a) => ({\n    filename: a.filename,\n    buffer: Buffer.from(a.data, \"base64\"),\n  }));\n\n  const answer = await runAgent({ task: m.plain_body, from: m.mail_from, trusted });\n\n  // Reply via Postal's send API — you host the auth and the server behind it.\n  await fetch(\"https://postal.yourco.dev/api/v1/send/message\", {\n    method: \"POST\",\n    headers: { \"X-Server-API-Key\": process.env.POSTAL_API_KEY, \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({\n      to: [m.mail_from],\n      from: m.rcpt_to,\n      subject: `Re: ${m.subject}`,\n      html_body: answer.html,\n    }),\n  });\n\n  res.sendStatus(200); // must be fast, or Postal retries\n});\n\napp.listen(3000);\n```\n\nNone of that is exotic if you run mail infrastructure. But two things are worth naming. First, the payload has `spam_status`\n\n, not a structured `{ spf, dkim, dmarc }`\n\nverdict, so the trust signal your agent uses to resist prompt injection is weaker unless you parse `Authentication-Results`\n\nout of the raw message yourself. Second, and bigger: the `fetch`\n\nto 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.\n\n## The comparison, no adjective inflation\n\n| Postal (self-hosted) | MailKite | |\n|---|---|---|\n| Who runs the mail server | You (Docker, MariaDB, Caddy) | Managed, nothing to host |\n| Start receiving | Provision box, DNS, TLS, then routes | DNS-verify a domain, add a webhook |\n| Inbound delivery | Parsed fields to your HTTP endpoint | One signed parsed JSON webhook |\n| Auth for injection safety | `spam_status` ; parse `Authentication-Results` yourself | `auth` block: spf/dkim/dmarc/spam inline |\n| Attachments | Base64 inline in the payload | Short-lived signed URLs |\n| IP reputation & warmup | Yours to build and defend | Handled on a monitored stream |\n| Deliverability drift | Your incident, your blocklist delisting | Handled |\n| Per-message cost | None (server + IP + your time) | Metered; free tier 3,000/mo in+out |\n| Upgrades & patching | You run Postal + MariaDB upgrades | Handled |\n\nThe 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.\n\n## What actually hits your agent’s webhook\n\nOn MailKite the same inbound email arrives already parsed, and the `auth`\n\nblock is the part that matters for an agent: it’s the verdict you weight before you let the body influence anything.\n\n```\n{\n  \"id\": \"msg_2Hk9…\",\n  \"type\": \"email.received\",\n  \"from\": { \"address\": \"ada@example.com\" },\n  \"to\": [{ \"address\": \"agent@myapp.ai\" }],\n  \"subject\": \"Re: invoice #1042\",\n  \"text\": \"Looks good — approved!\",\n  \"html\": \"<p>Looks good — approved!</p>\",\n  \"threadId\": \"<a1b2c3@mail.example.com>\",\n  \"auth\": { \"spf\": \"pass\", \"dkim\": \"pass\", \"dmarc\": \"pass\", \"spam\": \"ham\" },\n  \"attachments\": [\n    { \"id\": \"msg_2Hk9…:0\", \"filename\": \"po.pdf\", \"contentType\": \"application/pdf\",\n      \"size\": 18213, \"url\": \"https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…\" }\n  ]\n}\n```\n\nThe email body is untrusted input, never instructions. `From:`\n\nis plain text and trivially forged, so an agent that acts on what a message *says* is a prompt-injection target. The `auth`\n\nblock 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](/blog/agent-inbox-security-by-design/).\n\n## The managed version of the same loop\n\nMailKite, 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`\n\nevent with the body, a resolved `threadId`\n\n, attachments as signed URLs, and the `auth`\n\nverdict 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\"`\n\ncarries an `agentPrompt`\n\nand 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.\n\n## FAQ\n\n**Does Postal receive inbound email?**\nYes. 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`\n\n, `html_body`\n\n, and decoded attachments. The catch is that you run the server doing it.\n\n**Is Postal free?**\nThe 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.\n\n**Does Postal still need RabbitMQ?**\nNo. 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.\n\n**Does Postal’s inbound payload include an SPF/DKIM/DMARC verdict?**\nNot as a structured block. The processed payload includes `spam_status`\n\n, but the aligned SPF/DKIM/DMARC result an agent should weight before trusting a sender lives in the raw `Authentication-Results`\n\nheaders, so you’d parse it yourself. MailKite includes an `auth`\n\nobject with the spf, dkim, dmarc, and spam verdicts already resolved.\n\n**Should my AI agent use self-hosted Postal or a managed inbox?**\nSelf-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.\n\n**How does an agent reply from the address it was written to?**\nOn Postal, POST to `/api/v1/send/message`\n\nwith `X-Server-API-Key`\n\n, setting `from`\n\nto the route address. On MailKite, `mk.send({ from: event.to[0].address, inReplyTo: event.id, … })`\n\nthreads the reply automatically. Either way the reply goes out from the address the mail arrived at.\n\nPostal 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](https://github.com/mailkite/demo-postal-ai-agent) (or [run it in your browser](https://stackblitz.com/github/mailkite/demo-postal-ai-agent?file=server.mjs)), then [point a domain at MailKite](/docs/quickstart) and your agent’s next inbound email arrives as parsed JSON.\n\n*Related: give your AI agent its own email inbox and agent inbox security by design.*", "url": "https://wpnews.pro/news/the-postal-alternative-for-ai-agents", "canonical_source": "https://mailkite.dev/blog/postal-for-ai-agents/", "published_at": "2026-07-04 00:00:00+00:00", "updated_at": "2026-07-04 13:30:56.243879+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Postal", "MailKite", "MariaDB", "Caddy", "Docker", "RabbitMQ", "Ruby on Rails", "StackBlitz"], "alternates": {"html": "https://wpnews.pro/news/the-postal-alternative-for-ai-agents", "markdown": "https://wpnews.pro/news/the-postal-alternative-for-ai-agents.md", "text": "https://wpnews.pro/news/the-postal-alternative-for-ai-agents.txt", "jsonld": "https://wpnews.pro/news/the-postal-alternative-for-ai-agents.jsonld"}}