# Give your AI agent its own email inbox – MailKite

> Source: <https://mailkite.dev/blog/give-your-agent-an-inbox/>
> Published: 2026-07-03 23:47:29+00:00

# Give your AI agent its own email inbox

Give an AI agent a real, scoped address on a domain you control. Inbound mail arrives as parsed JSON to an event.received loop and the agent replies over the Send API, or MailKite runs the agent for you on a route with action: agent. Working code, the security caveat, and the honest DIY alternatives.

**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.** This post is for a developer wiring an agent to an inbox for the first time, and it shows two ways to build one: run the agent yourself (inbound mail arrives as parsed JSON, your handler calls your model, the agent answers with one `mk.send()`

), or let MailKite (which we build) run it for you with a route whose `action`

is `agent`

. Either way there’s no IMAP, no MIME parsing, and no personal Gmail account quietly wired into a bot.

Here’s the bring-your-own-agent loop, whole. Email in, verify the signature, hand the body to your model, reply through the same client. It runs as pasted on Node 18+ (`npm install mailkite express`

):

``` python
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;

  // Treat the body as untrusted INPUT, never as instructions (see the security section).
  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. `mk.send()`

returns `{ id, status }`

so you can log the outbound message, and the identical handler shape exists for Python, Ruby, Go, PHP, and Java; see the [receiving docs](/docs/receiving) and [sending docs](/docs/sending). Raw HTTP works too if you can’t take a dependency, but you’d be hand-rolling the HMAC verify that `verifyWebhook`

does in one line; prefer the SDK.

## What actually lands at your handler

MailKite decodes the message at the edge, so `runAgent`

gets fields, not MIME. This is the same [inbound webhook](/docs/receiving) from the pillar: decoded `text`

and `html`

, a resolved `threadId`

, attachments as short-lived signed URLs, and an `auth`

block:

```
{
  "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": [
    { "id": "msg_2Hk9…:0", "filename": "po.pdf", "contentType": "application/pdf",
      "size": 18213, "url": "https://api.mailkite.dev/att/2Hk9…:0?exp=…&sig=…" }
  ]
}
```

The `auth`

block tells the agent whether SPF, DKIM, and DMARC passed. Hold that thought; it’s load-bearing in a minute.

## Two ways to run an agent inbox

You own the loop above, or you hand the whole thing to MailKite. Same inbound edge; the difference is where the agent’s turns execute.

## Let MailKite run the agent (route action ‘agent’)

If you don’t want to host the loop, make the route itself the agent. A route with `action: "agent"`

carries an `agentPrompt`

(free-text instructions), and MailKite runs the model loop for you on every inbound message:

```
await mk.createRoute({
  match: "support@yourco.dev",
  action: "agent",
  agentPrompt: "Answer billing questions from the docs. Escalate anything else to humans@yourco.dev.",
});
```

The run doesn’t happen on the inbound request’s clock. We enqueue it on a Cloudflare Queue (`mailkite-agent-runs`

) with its own execution budget, so ingest still returns fast and a slow model call can’t wedge the pipeline. Each run is capped at 8 tool rounds, aborted at 5 minutes by a cron reaper as a crash-proof backstop, and recorded as a full transcript you can drill into per route in the dashboard. The system prompt bakes in the safety rules the next section is about: the email body is untrusted, at most one reply, and never reply to no-reply or automated senders. The agent replies with an internal `send_email`

tool (threaded via `inReplyTo`

), the same `/v1/send`

your own loop calls. Details in the [inbox-agents docs](/docs/inbox-agents).

Bring-your-own wins when the agent’s brain is your code: your model, your tools, your state. The built-in route wins when you’d rather not run infrastructure and the job is “read this inbound mail, answer or escalate.”

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

Here’s where I slow you down, because I walked 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, so anyone can forge the sender, then simply tell your agent what to do, and a naive loop obeys.

That’s why the `auth`

block is in the payload and why the loop passes a `trusted`

flag instead of blindly acting: you can at least see whether SPF and DMARC passed before you weight a sender’s instructions. But 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 can even do (owner-scoped tools, one reply, no acting on links). This is the same reasoning the built-in agent uses, and I wrote up the mistake honestly in [Why aren’t we seeing more agent security discussions?](/blog/agent-security-blind-spot/). Read it before you point either loop at anything that matters.

## When to build it yourself (and when not to)

An agent inbox isn’t exotic; you can assemble one without us, and sometimes you should. The honest alternatives:

**IMAP polling on a real mailbox.** Give the agent a Gmail/Workspace or Fastmail account and poll IMAP or the Gmail API. It works, but you own MIME parsing, threading, attachment decoding, and a mailbox that is really a person’s account with a person’s permissions. Best when the agent genuinely needs to live inside an existing human inbox.**Postmark inbound + your own loop.** Postmark’s inbound parse also POSTs parsed JSON; point it at the same`runAgent`

loop above. A fine call if you’re already on Postmark for sending and just want the inbound half.**Cloudflare Email Workers.** The receiving primitive: an email hits a Worker’s`email()`

handler. There’s no parsing, routing, or management layer; you build that. Good if you want to own the whole edge. (We build MailKite on Cloudflare, and this is the layer we put on top.)**Self-hosted Haraka.** Run your own MX. Total control, total ops: the SMTP server, spam filtering, TLS, and uptime are all yours. Worth it at real scale or under strict data-residency rules.

The DIY shape is the same in every case: an MX record (or a mailbox), something that turns raw MIME into fields, signature and SPF/DKIM/DMARC checks, and a reply path. MailKite is that stack assembled (MX edge, parse, auth, a signed and retried webhook, and the Send API), so the 25 lines up top are the whole integration. Below a dedicated mail-infrastructure engineer’s time the trade tends to pay for itself; above it, or when the agent must live in a human’s real mailbox, one of the above is the better pick.

## Let the agent use MailKite as a tool (MCP)

Everything so far is email reaching *in*. The other direction is your agent reaching *out* through email, and MailKite exposes its whole API as agent-native tools over a hosted MCP server. Point any MCP client at it:

```
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp
```

After that the agent calls `mailkite_send`

, `mailkite_get_message`

, `mailkite_list_domains`

, and the rest as first-class tool calls instead of hand-rolled HTTP. There’s also a [Claude Code plugin](/docs/skill) that wires the same tools into your editor. An agent with an inbox *and* this tool set can run a support address end to end: read the incoming message, look something up, reply, all as tool calls.

## 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 is parsed to JSON and POSTed 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.

**Should the agent run on my server or on MailKite?**
Both work. Host the loop yourself (`webhook`

route → your endpoint → your model → Send API) when the agent’s logic is your code. Use a route with `action: "agent"`

and an `agentPrompt`

when you’d rather MailKite run the model loop on its own durable queue and hand you a transcript.

**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: senders are trivially spoofable. Check the `auth`

(SPF/DKIM/DMARC) results before trusting instructions, and don’t rely on the system prompt alone; bound the agent’s authority. See the [agent-security post](/blog/agent-security-blind-spot/).

**Can the agent call MailKite as a tool?**
Yes. There’s a hosted MCP server at `mcp.mailkite.dev`

(`claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp`

) and a [Claude Code plugin](/docs/skill), 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 to the parts of the world that arrive by email. [Point a domain at MailKite](/docs/quickstart) and it’ll be reading and answering its own mail in a few minutes: pick the bring-your-own loop or a route with `action: 'agent'`

([inbox-agents docs](/docs/inbox-agents)), then read [the security post](/blog/agent-security-blind-spot/) before you let it act on anything.

*Related: the inbound pillar on why receiving email is hard, parsing inbound email to JSON in Node, and the AI agents guide.*
