--- title: AI Email Agent: Giving an Agent an Inbox That Actually Works description: An agent with an email address can read threads, open attachments and reply. Here is what that requires: domain setup, inbound routing, threading, and the deliverability nobody warns you about. date: 2026-08-11 author: Victor Villalobos locale: en source: https://www.zavu.dev/en/blog/ai-email-agent tags: AI Agents, Email, Tutorial --- # AI Email Agent: Giving an Agent an Inbox That Actually Works Email is the channel agents are best suited to and the one most of them never get. Best suited, because email is where the hard messages live. Nobody sends a purchase order over WhatsApp. Quotation requests, invoices, supplier confirmations and support threads with six replies and a PDF attached all arrive by email, and every one of them is a task with enough context in it for an agent to actually do the work. Never get, because giving an agent an inbox means solving deliverability, and deliverability is a discipline with its own decade of accumulated pain. This covers what it takes. ## What an agent with an inbox actually needs Five things, and only one of them is the model: | Requirement | Why it is not optional | |---|---| | A domain you send from, authenticated | Unauthenticated mail goes to spam or is rejected outright | | Inbound routing | Somewhere for replies to land as events, not in a mailbox nobody reads | | Threading | An agent that answers a five-message thread without reading it answers the wrong question | | Attachments both ways | The PDF is usually where the actual request is | | A suppression list | Sending to an address that hard bounced burns the reputation you spent months building | Miss the first and nothing arrives. Miss the last and everything stops arriving, three weeks later, all at once. ## Setting up the domain Email authentication is three records with three jobs. DKIM signs your mail so receivers can verify it came from you. SPF says which servers may send as your domain. DMARC tells receivers what to do when the first two disagree. ```bash npx zavudev email-domains add example.com ``` That returns the records to publish. DKIM is required to send at all. SPF, DMARC and a custom MAIL FROM are marked recommended, which understates it: they are what separates the inbox from the promotions tab. Publish them at your DNS provider, then: ```bash npx zavudev email-domains verify example.com ``` Two practical notes. Verification checks what the provider has cached, so correct records can still read as pending for a while; re-running verify does not force a re-scan. And use a subdomain you can afford to burn, like `agent.example.com`, rather than your main corporate domain. If the agent gets something badly wrong at volume, the damage is contained to a domain you can retire. ## Receiving mail Sending is the easy half. An agent needs replies, which means an MX record and receiving enabled on the sender. The setting that matters most for agents is catch-all. With it on, the sender receives mail addressed to **any** local part at the domain, not just its own address. That means `orders@`, `quotes@`, `support@` and `ticket-4471@` all reach the same agent, and the original recipient arrives in the webhook's `data.to` so the agent knows which one was used. This is what makes per-thread addressing possible. Give every conversation its own reply address, and threading becomes exact rather than a guess based on subject lines: ``` quote-8842@agent.example.com -> the quotation thread for deal 8842 ticket-4471@agent.example.com -> support ticket 4471 ``` No parsing of `Re: Re: Fwd:`, no matching on subject, no confusion when two customers open threads about the same product on the same day. ## The agent Inbound email arrives as a `message.inbound` event like every other channel, so an agent that already answers on WhatsApp answers on email with no new code. When you want email-specific behaviour, describe it rather than writing it. With the skills installed, the prompt is: > Add email handling to this agent. Reply addresses look like `quote-{dealId}@agent.example.com`, so pull the deal ID out of the recipient. Download any attachments and pass them to `answerQuotation`. Reply on the same thread with the generated PDF attached. What it writes: ```ts import { defineFunction } from "@zavudev/functions" import Zavudev from "@zavudev/sdk" const zavu = new Zavudev({ apiKey: process.env.ZAVU_API_KEY }) export default defineFunction({ on: ["message.inbound"], handler: async (event, ctx) => { if (event.data.channel !== "email") return const dealId = event.data.to.match(/quote-(\d+)@/)?.[1] // Attachment listing is REST only: Stainless has not generated it into the // SDK yet, so call the endpoint directly rather than a method that does // not exist. const res = await fetch( `https://api.zavu.dev/v1/messages/${event.data.messageId}/attachments`, { headers: { Authorization: `Bearer ${process.env.ZAVU_API_KEY}` } }, ) const { items } = await res.json() const reply = await answerQuotation({ dealId, body: event.data.text, files: items.map((a: { downloadUrl: string }) => a.downloadUrl), }) await zavu.messages.send({ to: event.data.from, channel: "email", subject: `Re: quotation ${dealId}`, text: reply.text, htmlBody: reply.html, attachments: [{ filename: "quote.pdf", content: reply.pdfBase64 }], }) }, }) ``` Two details in there are the whole reason email is different from every other channel. **Attachments arrive as stored files, not as bytes in the webhook.** `GET /v1/messages/{messageId}/attachments` returns a short-lived signed `downloadUrl` per file, generated fresh on each request. Fetch promptly, do not cache the URL. This is where the actual request usually lives: the spec, the PO, the photo of the damaged item. Note that this endpoint is REST only today. The TypeScript SDK covers sending attachments but not listing them, so call it with `fetch` as above. **Sending attachments takes base64 or a URL.** Either `content` with base64 bytes or `path` with a URL the server fetches, up to 40MB total. And `content_id` lets you embed an image inline, referenced from the HTML body as `cid:your_content_id`, which is how an agent sends a chart rather than describing one. ## The failures that only happen on email **The reply-all loop.** Your agent replies to an address that is itself an autoresponder. It replies. Your agent replies. By morning you have four thousand messages and a domain reputation problem. Never reply to an address that has already been answered in the last few minutes, and never to `noreply@` or `mailer-daemon@`. **Answering the wrong question.** Email threads carry history, and the newest message is often "sounds good, go ahead" with the actual request four replies up. Feed the agent the thread, not the last message. Per-thread addressing above is what makes retrieving the right thread reliable. **Slow burn deliverability.** Nothing fails visibly. Open rates just decline, then replies stop, and by the time anyone investigates the domain has been in the spam folder for a month. The controls that matter: send only to addresses that asked, honour unsubscribes immediately, and validate lists before large sends. ```bash curl -X POST https://api.zavu.dev/v1/introspect/email \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -d '{"emails": ["maria@example.com", "info@deaddomain.example"]}' ``` That returns `deliverable`, `risky` or `undeliverable` per address, with the reason: invalid syntax, a domain with no MX records, a disposable inbox, a role address, or an address already on your suppression list from a previous bounce. Drop the undeliverable ones before sending, and a bounce rate that would have taken your domain down stays flat. On individual sends, addresses that would be a guaranteed hard bounce are failed before dispatch rather than sent, with `errorCode` set to `EMAIL_INVALID_RECIPIENT`, `EMAIL_DOMAIN_NOT_FOUND` or `EMAIL_RECIPIENT_SUPPRESSED`. Advisory signals like role addresses do not block the send, which is why the batch check above is worth running yourself before anything large. **The agent that answers everything.** Email arrives from cold outreach, newsletters, invoices and actual customers. An agent that treats all of it as a task to complete will happily negotiate with a spam bot. Classify first, act second. ## What it costs Email on Zavu is billed from the prepaid balance in blocks of 1,000: **$0.40 per 1,000 transactional emails** and **$0.80 per 1,000 marketing or broadcast emails**. A block is charged when your monthly count crosses each 1,000 boundary. Free teams start with $2 of credit and are capped at 3,000 emails per month and 100 per day, which is enough to build and test an agent and not enough to run a campaign. For an agent handling a few hundred conversations a month, the email line is a rounding error next to the model tokens. The cost that matters on this channel is a burned domain, which is why the validation step is worth more than it looks. ## When email is the wrong channel If the answer is one line and needed now, email is the wrong place. Nobody watches their inbox the way they watch WhatsApp, and a two-hour reply that is technically fast reads as slow. If you need a decision in a conversation, use a channel with turn-taking. Email threads are asynchronous by design, and an agent that needs three clarifications will take three days to get them. If the recipient is a consumer in Brazil, LATAM, India or Southeast Asia, they are on [WhatsApp](/en/whatsapp-api) and your email is going to a tab they open on Tuesdays. Email wins on long context, attachments, B2B, and anything that needs a record. Which is a lot, and it is the part agents are best at. ## Keep reading - [How to build an AI agent](/en/blog/how-to-build-an-ai-agent): the loop, then deployed on a channel. - [AI agent frameworks compared](/en/blog/ai-agent-frameworks): where email sits in the stack. - [Newsletter examples](/en/blog/newsletter-examples): the marketing side of the same domain setup. - [AI agents for WhatsApp, SMS and email](/en/ai-agents): one agent, every channel.