Postal is a full self-hosted mail server: it sends, it receives inbound over incoming routes, and its HTTP endpoint POSTs your agent a parsed payload with plain_body, html_body, and decoded attachments. The catch is operational, not technical — you run the box (Docker, MariaDB, Caddy, DNS, TLS, IP warmup, blocklist monitoring, patching). MailKite is that same loop delivered as one signed JSON webhook, with nothing to host.
Postal is genuinely good software — MIT-licensed, mature, and it truly does parsed inbound, not just raw MIME. Self-hosting it buys you total control, data residency, and no per-message fee: at real scale you pay for a server and an IP, not for each email. Since v3 (2024) it even dropped RabbitMQ, so the stack is smaller than most guides remember.
Postal means standing up and keeping alive a full send+receive mail server: VPS with port 25, Docker, MariaDB, Caddy, DNS, TLS, PTR/rDNS, IP warmup, blocklist watch, and Postal + MariaDB upgrades. MailKite collapses every one of those stages into a signed webhook — the only box you run is your agent.
Postal's processed payload gives you spam_status, but the aligned SPF/DKIM/DMARC result an agent should weight before trusting a sender lives in the raw Authentication-Results headers — you parse them yourself. MailKite ships a structured auth block (spf/dkim/dmarc/spam) already resolved, so injection-trust is a field read, not a header parse.
On Postal the sending IP's reputation is yours to build and defend. If a fooled agent blasts a bad reply and lands the IP on a blocklist, that's your delisting to chase. MailKite sends over a monitored stream, so reputation and blocklist recovery aren't your on-call.
Point a webhook and get parsed JSON, or set a route to action:'agent' with an agentPrompt and MailKite runs the receive→reply loop for you on a durable queue — capped and recorded as a transcript. Plus a hosted MCP server and Claude Code plugin, none of which you host.
| MailKite | Postal | |
|---|---|---|
| Parsed inbound to your endpoint | One signed parsed JSON webhook | Yes — parsed fields to your HTTP endpoint |
| Who runs the mail server | Managed — nothing to host | You (Docker, MariaDB, Caddy) |
| Start receiving | DNS-verify a domain, add a webhook | Provision box, DNS, TLS, then routes |
| Auth for injection safety | auth block: spf/dkim/dmarc/spam inline | spam_status; parse Authentication-Results yourself |
| Attachments | Short-lived signed URLs | Base64 inline in the payload |
| IP reputation & warmup | Handled on a monitored stream | Yours to build and defend |
| Upgrades & patching | Handled | You run Postal + MariaDB upgrades |
| Per-message cost | Metered; free 3,000/mo in+out | None — server + IP + your time |
Competitor capabilities change — we re-audit these tables regularly. Spot something out of date? Tell us and we'll fix it.
Same job, but the auth verdict is a field (not a header parse), attachments are signed URLs (not base64 to decode), and the reply goes out over a monitored stream you don't host.
// 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 }] }
// No SPF/DKIM/DMARC verdict block — the aligned result lives in the raw
// Authentication-Results headers, so you parse it yourself.
const trusted = m.spam_status === "NotSpam"; // weaker than a real auth verdict
// 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 YOUR Postal box — you host the auth, the IP, 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
}); POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com" },
"to": [{ "address": "support@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": [
{ "filename": "po.pdf", "contentType": "application/pdf",
"size": 18213, "url": "https://api.mailkite.dev/att/2Hk9…/0?sig=…" }
]
} Postal is free-as-in-software, not free-as-in-no-work. MailKite meters volume instead of billing per box or per domain, and there's no server, IP, or patching in the price.
Point MX at MailKite to receive (SPF + DKIM to send). No VPS, no port-25 fight, no Docker/MariaDB/Caddy bootstrap, no PTR/rDNS or TLS to stand up — the mail server is already running.
Keep the same shape: read event.text, event.from.address, event.subject. Drop the base64 attachment decode (use the signed URLs) and stop parsing Authentication-Results — read event.auth.spf/dkim/dmarc instead.
Swap the fetch to your Postal /api/v1/send/message for mk.send({ from: event.to[0].address, inReplyTo: event.id, … }) — same account, threaded automatically, and no IP reputation or MariaDB backups left to keep alive.
Yes — this isn't a capability gap. Postal's incoming route can POST your agent a processed payload with plain_body, html_body, and decoded attachments. The difference is operational: with Postal you run the mail server, IP reputation, DNS, TLS, and upgrades doing it; with MailKite you receive a signed webhook and own none of that.
At real volume, self-hosting can win on unit economics: you pay for a server and an IP, not per email. Weigh that against the operational cost MailKite absorbs — IP warmup, blocklist delisting, patching, MariaDB maintenance, and being on-call when deliverability drifts. Free-as-in-software, not free-as-in-no-work.
You give up total control and data residency — on Postal every message lives on hardware and in a database you chose, which matters for regulated mail. If control and residency outweigh the ops and you have a team to run a mail server, self-host. If you'd rather receive a parsed, authenticated webhook, hand off the box.
The Postal alternative for AI agents →
The long-form take on our blog — receipts, runnable code, and where we won't overclaim.
Point a domain, drop in a webhook URL, receive your first email. Unlimited domains, no credit card.