MailKite
Get started
All posts
Gabe 5 min read

Give your AI agent its own email inbox

An AI agent that can't receive email is half-deaf. Here's how to give an agent a real, scoped address on your own domain — inbound parsed to JSON, an event.received loop, and autonomous replies over the same API you already use.

An AI agent with its own email inbox is an autonomous program that owns a real address on a domain you control — so it can receive verification codes, be handed work by email, and reply on its own without a human in the loop. With MailKite, mail sent to that address is parsed into clean JSON and POSTed to your agent as an email.received event, and the agent answers by calling mk.send(). No IMAP, no MIME parsing, no personal Gmail account quietly wired into a bot.

I keep watching people build genuinely capable agents that are, in one specific way, deaf. They can call APIs, run tools, reason for pages — and then they hit a wall the second the real world tries to reach them. A vendor emails a confirmation link. A customer replies to a thread the agent started. A teammate forwards something and says “deal with this.” Every one of those is email, and most agents have no way to hear it.

An inbox fixes that. Not a shared human inbox the agent screen-scrapes — its own address, on your domain, that you can reason about and revoke.

Why an agent needs a real address

Three jobs come up over and over, and all three need a genuine inbox:

  • Receiving verification codes. Half the useful services on the internet gate signup behind an emailed code or magic link. An agent that can sign itself up for things needs somewhere those codes can land — and reading them out of your personal inbox is exactly the coupling you don’t want.
  • Being tasked by email. Email is the universal work queue. agent@yourco.dev is a dead-simple interface: a human or another system emails the agent, the agent does the thing. No new UI, no bot framework, no Slack app review.
  • Replying autonomously. Threads have two directions. If the agent can only send cold and never answer a reply, it isn’t participating in a conversation — it’s spraying. A real inbox closes the loop.

Give it a scoped inbox on your domain

The setup is the same one from the inbound pillar: point a domain at MailKite (add the MX record, verify), pick an address for the agent, and set a webhook URL. From then on, anything sent to agent@yourco.dev arrives at your handler already parsed:

{
  "id": "msg_2Hk9…",
  "type": "email.received",
  "from": { "address": "ada@example.com" },
  "to": [{ "address": "agent@yourco.dev" }],
  "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…?exp=…&sig=…" }
  ]
}

The agent gets decoded text and html, a resolved threadId, attachments as short-lived signed URLs — and, crucially, an auth block telling it whether SPF, DKIM, and DMARC passed. Hold that thought; it’s load-bearing in a minute.

The loop: email in → agent → reply out

Here’s the whole thing in Node. Verify the signature, hand the message to your agent, send the reply through the same client. Nothing exotic:

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

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) => {
  const sig = req.headers["x-mailkite-signature"];
  if (!MailKite.verifyWebhook(sig, 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;

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

  await mk.send({
    from: "agent@yourco.dev",
    to: event.from.address,
    subject: `Re: ${event.subject}`,
    html: answer.html,
  });
});

That’s a fully autonomous email agent: it hears, it thinks, it answers. mk.send() returns { id, status } so you can log the outbound message, and the same handler shape exists for Python, Go, Ruby, PHP, and Java — see the receiving docs and sending docs.

The part I have to flag: inbound email is untrusted input

Here is where I have to slow you down, because I walked straight into this hole myself. The moment your agent follows what an email says, that email body is a prompt-injection vector. From: is plain text — anyone can forge it. So an attacker can email your agent and simply tell it what to do, and a naive loop will obey.

This is why the auth block is in the payload and why the code above passes a trusted flag rather than blindly acting: you can at least see whether SPF and DMARC passed before you weight a sender’s instructions. But — and I mean this — checking auth is necessary, not sufficient. You cannot prompt your way out of prompt injection; the real answer is architectural, bounding what a fooled agent is even able to do. I wrote up the mistake honestly in Why aren’t we seeing more agent security discussions?, and there’s more on the security model soon in the follow-up. Read it before you point this at anything that matters.

Your agent can also use MailKite as a tool

The loop above is email reaching in. The other direction is your agent reaching out through email itself — and MailKite exposes its whole API as agent-native tools. There’s a hosted MCP server (mcp.mailkite.dev) and a Claude Code plugin, so an agent can list domains, read a message, and send a reply as first-class tool calls instead of hand-rolled HTTP. An agent with an inbox and a set of email tools can run a support address end to end.

FAQ

Can I give an AI agent its own email address? Yes. Point a domain at MailKite, pick an address like agent@yourco.dev, and set a webhook. Inbound mail to that address is parsed to JSON and POSTed to your agent as an email.received event; the agent replies with mk.send(). It’s a real, scoped mailbox on your domain, not a personal account bolted onto a bot.

How does the agent read incoming email? It doesn’t parse MIME. MailKite decodes the message at the edge and delivers text, html, a resolved threadId, attachments as signed URLs, and an auth result. Your handler verifies the webhook signature and reads plain fields.

Isn’t letting an agent act on email dangerous? It’s a prompt-injection surface, yes — email senders are trivially spoofable. Check the auth (SPF/DKIM/DMARC) results before trusting instructions, and don’t rely on the system prompt alone. The durable fix is bounding the agent’s authority; see the agent-security post.

Can the agent call MailKite as a tool? Yes — there’s a hosted MCP server at mcp.mailkite.dev and a Claude Code plugin, so sending mail, reading messages, and managing domains are all tool calls the agent can make directly.


Give your agent an inbox and it stops being deaf. Point a domain at MailKite and it’ll be reading and answering its own mail in a few minutes — then read the security post before you let it act on anything.

Discuss this post: Hacker News Share on X

Related posts