All posts
Architecture·By Deepak··10 min read

Email for AI Agents: Real Address, Inbox & Send Capability

TL;DR

Giving an AI agent a real email address requires three distinct systems: a verified sending identity (SPF/DKIM/DMARC), an inbound parsing layer that delivers structured payloads to your agent, and thread state management. This post walks through each layer with concrete implementation detail.

TL;DR: Giving an AI agent a real email address requires three distinct systems: a verified sending identity (SPF/DKIM/DMARC), an inbound parsing layer that delivers structured payloads to your agent, and thread state management. This post walks through each layer with concrete implementation detail.

Email for AI Agents: Real Address, Inbox & Send Capability

Email for AI agents isn't just about calling an SMTP endpoint. It means giving your agent a persistent, authenticated identity that can send reliably, receive replies, parse content, and maintain conversation state across sessions. Most developers stitch this together from three or four separate services and discover the failure modes the hard way. This post covers the full stack so you don't have to.

Why agent email is different from transactional email

Transactional email is fire-and-forget: an OTP fires, a receipt fires, nobody replies. Agent email is conversational — your agent sends a message, a human responds, and the agent must correlate that reply to the original context, parse the intent, decide on an action, and potentially reply again. That changes the architecture at every layer.

The core differences:

Dimension Transactional Agent Email
Reply expected Rarely Always
Thread correlation Not needed Critical
Inbound parsing Optional Mandatory
Sending volume Burst Low, steady
Auth requirements DKIM/SPF DKIM/SPF/DMARC + reputation
State management Stateless Stateful per thread

Transactional senders optimize for throughput and deliverability at scale. Agent senders optimize for correctness — the right reply correlated to the right context, every time.

Giving your agent a real email address

A real address means a real domain you control, not a shared subdomain from a provider. Your agent's address — assistant@yourdomain.com or agent+{uuid}@yourdomain.com — needs three things to function credibly:

1. DNS authentication records

SPF declares which mail servers may send from your domain. A minimal record:

TXT @ "v=spf1 include:_spf.yoursendingprovider.com ~all"

Use ~all (softfail) during testing, -all (hardfail) in production once you've verified all sending paths. Forgetting a sending path — say, your agent sends via a secondary API key on a different IP pool — causes SPF failures that silently hurt deliverability.

DKIM signs message headers and body with a private key held by your sending provider. The public key sits in DNS:

TXT selector1._domainkey "v=DKIM1; k=rsa; p=MIGf..."

The selector prefix (selector1) lets you rotate keys without downtime. Always verify DKIM alignment — the d= value in the DKIM signature must match the From: domain. Misalignment breaks DMARC.

DMARC ties SPF and DKIM together and tells receiving servers what to do with failures:

TXT _dmarc "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@yourdomain.com; pct=100"

Start with p=none to collect reports, move to p=quarantine once reports show clean alignment, then p=reject for full enforcement. Google and Yahoo's 2024 bulk sender requirements made DMARC mandatory for any domain sending at scale — agents that skip it will find their mail increasingly filtered.

2. A stable From identity

Your agent's From: address should be stable per agent or per agent role, not per-message. Rotating addresses confuse recipients and destroy reputation signals. If you need per-conversation routing, use a stable From: with a unique Reply-To::

From: assistant@yourdomain.com
Reply-To: thread+a8f3c2@yourdomain.com
Message-ID: <a8f3c2.1750000000@yourdomain.com>

The Reply-To encodes thread context. Replies arrive at thread+a8f3c2@yourdomain.com, your inbound parser extracts a8f3c2, and you have your correlation key without touching the From: address at all.

3. Warm sending reputation

New domains and new IPs start with no reputation. Gmail, Outlook, and other providers apply heavier filtering to cold identities. For automated senders this is particularly sharp — low engagement rates (no human opens and replies) can tank deliverability even with perfect authentication.

Warm your domain by starting with low volume (under 50 messages/day), ensuring early recipients are engaged, and watching bounce rates. A bounce rate above 2% signals problems. Dedicated IP addresses isolate your agent's reputation from shared infrastructure — worth considering once volume exceeds a few hundred messages per day.

Building the inbound parsing layer

Inbound email for an AI agent means converting a raw MIME message into a structured payload your agent can act on. This has more moving parts than most developers expect.

MX records and routing

For your agent to receive email, your domain's MX records must point to an inbound mail server:

MX 10 inbound.yourmailprovider.com

That server receives SMTP connections, accepts the message, and delivers it to your webhook. The MX priority number (10) matters when you have multiple records — lower numbers are tried first.

Parsing raw MIME

A real email is a MIME multipart document. The structure matters:

Content-Type: multipart/alternative; boundary="boundary123"

--boundary123
Content-Type: text/plain; charset=utf-8

Plain text body here.

--boundary123
Content-Type: text/html; charset=utf-8

<html>HTML body here.</html>
--boundary123--

For LLM ingestion you want the text/plain part — it's cleaner, shorter, and cheaper to tokenize. If plain text is absent, you'll need to strip HTML. Watch for text/html parts that encode content in base64 (Content-Transfer-Encoding: base64) — decode before passing to your agent.

Headers carry metadata your agent needs:

  • References: and In-Reply-To: — the thread chain. Preserve these when replying.
  • Message-ID: — globally unique per message. Use this as your idempotency key.
  • Subject: — thread subject. Strip Re: prefixes carefully; some clients stack them.
  • From: — the sender. Parse the address out of display-name format: "Alice Smith" <alice@example.com>.

Webhook delivery

Your inbound provider POSTs parsed message data to your webhook endpoint. A typical payload looks like:

{
  "message_id": "<a8f3c2.1750000000@yourdomain.com>",
  "from": "alice@example.com",
  "to": ["thread+a8f3c2@yourdomain.com"],
  "subject": "Re: Project update",
  "text": "Sounds good. Can you schedule the call for Thursday?",
  "html": "<p>Sounds good...</p>",
  "headers": {
    "in-reply-to": "<a8f3c2.1750000000@yourdomain.com>",
    "references": "<a8f3c2.1750000000@yourdomain.com>"
  },
  "timestamp": 1750000000
}

Your webhook handler should return 200 within 5-10 seconds — do the heavy agent work asynchronously. If your handler takes 30 seconds parsing with an LLM, the provider will retry, and you'll process the same message twice. Deduplicate on message_id.

import hashlib
import requests

def handle_inbound(payload):
    message_id = payload['message_id']
    
    # Deduplicate using message_id as key
    if redis_client.get(f'processed:{message_id}'):
        return {'status': 'duplicate'}, 200
    
    # Mark as processing before any async work
    redis_client.setex(f'processed:{message_id}', 86400, '1')
    
    # Enqueue for agent processing
    queue.enqueue('process_email', payload)
    
    return {'status': 'accepted'}, 200

Mails.ai's inbound parsing layer handles MIME decomposition and delivers a clean structured payload — attachments are extracted separately, so your agent doesn't need to implement full MIME parsing to get started.

Thread state and conversation correlation

This is where most DIY implementations break. Your agent sends message A, the human replies, and your agent needs to know: what was message A, what context was it sent in, and what conversation state am I continuing?

The Reply-To threading pattern

Encode a correlation ID in your Reply-To address. When the reply arrives at your inbound endpoint, extract it:

import re

def extract_thread_id(to_address: str) -> str | None:
    # Matches thread+{id}@yourdomain.com
    match = re.search(r'thread\+([a-zA-Z0-9]+)@', to_address)
    return match.group(1) if match else None

def handle_inbound(payload):
    thread_id = None
    for addr in payload.get('to', []):
        thread_id = extract_thread_id(addr)
        if thread_id:
            break
    
    if thread_id:
        context = load_thread_context(thread_id)
        # Resume conversation with context
    else:
        # New thread — create fresh context
        context = create_new_thread(payload)

Storing thread state

Minimum thread state your agent needs:

{
  "thread_id": "a8f3c2",
  "original_message_id": "<a8f3c2.1750000000@yourdomain.com>",
  "messages": [
    {"role": "agent", "content": "...", "sent_at": 1750000000},
    {"role": "human", "content": "...", "received_at": 1750000100}
  ],
  "agent_context": {},
  "created_at": 1750000000,
  "last_activity": 1750000100
}

The messages array is what you pass as conversation history to your LLM. agent_context holds whatever your agent needs to resume — open tasks, user preferences, workflow state.

RFC-compliant reply headers

When your agent sends a reply, it must set In-Reply-To: and References: correctly or clients won't thread the conversation:

def build_reply_headers(original_message_id: str, references: list[str]) -> dict:
    new_references = references + [original_message_id]
    return {
        'In-Reply-To': original_message_id,
        'References': ' '.join(new_references[-10:])  # Keep last 10 to avoid header bloat
    }

Strip only one Re: from the subject — some agents strip all of them, which breaks email clients that track thread subjects.

Putting it together: the full flow

sequenceDiagram
    participant Agent as AI Agent
    participant SMTP as SMTP Provider
    participant Inbox as Human Inbox
    participant MX as MX Server
    participant WH as Webhook Handler
    participant Queue as Task Queue

    Agent->>SMTP: Send with Reply-To thread+abc123
    SMTP->>Inbox: Deliver signed message
    Inbox->>MX: Human replies
    MX->>WH: POST parsed payload
    WH->>Queue: Enqueue with thread_id abc123
    Queue->>Agent: Resume with thread context
    Agent->>SMTP: Reply with In-Reply-To set

Sending: authentication and API integration

Sending email from your agent code is straightforward once authentication is in place. Using the @mailsai/sdk in TypeScript:

import { MailsClient } from '@mailsai/sdk';

const client = new MailsClient({ apiKey: process.env.MAILS_API_KEY });

async function agentSend(threadId: string, to: string, subject: string, body: string) {
  const messageId = `${threadId}.${Date.now()}@yourdomain.com`;
  
  await client.send({
    from: 'assistant@yourdomain.com',
    replyTo: `thread+${threadId}@yourdomain.com`,
    to,
    subject,
    text: body,
    headers: {
      'Message-ID': `<${messageId}>`,
    },
  });
  
  return messageId;
}

For Python, until the SDK publishes, use requests directly:

import requests
import os

def agent_send(thread_id: str, to: str, subject: str, body: str) -> str:
    message_id = f"{thread_id}.{int(time.time())}@yourdomain.com"
    
    resp = requests.post(
        'https://api.mails.ai/v1/send',
        headers={'Authorization': f'Bearer {os.environ["MAILS_API_KEY"]}'},
        json={
            'from': 'assistant@yourdomain.com',
            'reply_to': f'thread+{thread_id}@yourdomain.com',
            'to': to,
            'subject': subject,
            'text': body,
            'headers': {'Message-ID': f'<{message_id}>'},
        }
    )
    resp.raise_for_status()
    return message_id

Classification: deciding what to do with inbound mail

Not every email your agent receives needs the same response. A human reply to an ongoing task is different from an auto-reply, an out-of-office notice, or a bounce notification. Classifying inbound mail before routing it to your agent prevents wasted LLM calls and wrong actions.

Basic heuristics:

  • Auto-Submitted: auto-replied header → discard or log, don't respond
  • X-Autoreply: header → same
  • Content-Type: multipart/report with message/delivery-status → bounce, update your sending state
  • Subject matches /out of office|on vacation|away from/i → log, schedule follow-up

For more nuanced classification — distinguishing task replies from questions, escalations from acknowledgments — email classification via LLM with a few labeled examples outperforms regex at the cost of ~$0.001/message. Worth it at any scale where wrong routing causes downstream agent errors.

Common failure modes

Replay attacks on webhooks: An attacker replays your inbound webhook payload. Verify the provider's HMAC signature on every request. Reject requests older than 5 minutes.

Reply loops: Your agent replies to an auto-reply, which triggers another auto-reply. Check Auto-Submitted headers and track sent message IDs — never send to an address your agent received a message from in the same second.

Thread context loss: Your thread state store TTLs out before the conversation ends. Set TTL based on your expected conversation length plus a buffer — 30 days is usually safe.

From address mismatch: Your agent sends from assistant@yourdomain.com but your SPF record doesn't include that sending IP. Monitor DMARC aggregate reports (the rua address) weekly during ramp-up.

For a full look at what agents can do once they have a working email identity, see the agent email use cases — scheduling, triage, data collection, and multi-step workflows all run on this same infrastructure stack.

Frequently Asked Questions

What DNS records do I actually need before my agent can send and receive?

For sending: SPF TXT record on your domain, DKIM TXT record at selector._domainkey.yourdomain.com, and a DMARC TXT record at _dmarc.yourdomain.com. For receiving: an MX record pointing to your inbound mail server. All four are required for a production setup — missing DMARC won't break sending immediately, but Google and Yahoo's bulk sender policies make it mandatory for reliable inbox placement.

How do I correlate a human's reply back to my agent's original context?

Encode a correlation ID in the Reply-To address using subaddressing: thread+{id}@yourdomain.com. When the reply arrives at your inbound endpoint, extract the ID from the to field, load the corresponding thread state from your store, and resume. This pattern works without touching the From: address, which keeps your sender reputation stable.

Should my agent use a shared IP or a dedicated IP for sending?

Shared IPs work at low volume (under a few hundred messages/day) because you inherit existing reputation. At higher volumes, or when your sending patterns are unusual (automated, low-engagement), dedicated IPs isolate your reputation. Warm a dedicated IP slowly — start under 50 messages/day and ramp over 4-6 weeks.

How do I prevent my agent from getting into reply loops with auto-responders?

Check Auto-Submitted, X-Autoreply, and Precedence: bulk or Precedence: auto-reply headers on every inbound message before triggering agent logic. Also maintain a set of message IDs your agent has sent — if an inbound In-Reply-To references one of your agent's messages and the Auto-Submitted header is set, discard it without replying.

What's the minimum thread state I need to store per conversation?

At minimum: the original Message-ID, the correlation ID used in Reply-To, the full message history (role + content + timestamp), and any agent-specific context needed to resume work. Store this in Redis or a database with a TTL of at least 30 days. The Message-ID is your deduplication key for inbound webhooks — always check it before processing.

Can my agent handle email if I'm not using a dedicated email provider?

You can route email through Gmail or Outlook with SMTP credentials, but you lose inbound webhook delivery, structured parsing, and programmatic control over authentication records. For anything beyond personal testing, a dedicated email API for agents that handles MX routing, MIME parsing, and webhook delivery is the right foundation — the operational overhead of DIY SMTP at scale isn't worth the cost savings.

Start sending with Mails.ai

Live now

Ship agent email in ~6 lines.

Free tier, no card. Mint a key and drop the SDK into your agent.

Get your API key
Live now

Built for agents.
Self-serve in minutes.

The API is live and self-serve. Drop ~6 lines into your agent and ship.

npmpnpmbunnpx
$ npm install @mailsai/sdk
Live on npm today · @mailsai/sdk + @mailsai/mcp-server