April 13, 2026 · 5 min read

Send real email from a Python AI agent in 20 lines

Most AI agents today live behind a chat UI. That's fine for demos, but the moment you want your agent to participate in real work — coordinating with customers, scheduling meetings, responding to vendor outreach — you need the inbox everyone else uses. Email.

This post walks through the shortest path to a Python agent that actually sends and receives email, with verified sender identity, conversation threading, and real-time delivery. No SMTP server, no public webhook URL (if you don't want one), no DNS.

Get an email address for your agent

Sign up at e2a.dev, pick a slug like research-assistant, and you'll get research-assistant@agents.e2a.dev. Auto-verified, ready to receive mail immediately. If you prefer your own domain, you can register it instead (MX + TXT record), but the shared domain is faster for experiments.

Install the Python SDK:

pip install e2a

Send an email

Three lines:

from e2a.v1 import E2AClient

client = E2AClient(api_key="e2a_...")

client.send(
    to=["alice@example.com"],
    subject="Quick question",
    body="Hi Alice — I'm your scheduling assistant. Got a second for a quick sync?",
)

That's it. The message goes out signed with your domain's DKIM, shows up in Alice's inbox looking like a normal email, and her reply will route back to your agent via e2a.

Receive replies

Two modes, depending on whether your agent has a public URL:

Cloud mode (webhook) — when Alice replies, e2a POSTs the email to your server:

@app.post("/webhook")
async def inbox(request):
    email = client.parse(await request.body())
    print(f"{email.sender}: {email.subject}")
    await email.reply(f"Got it. Does Thursday 2pm work?")
    return {"ok": True}

Local mode (WebSocket) — no public URL needed. Great for running agents on your laptop or behind a firewall:

from e2a.v1 import AsyncE2AClient

async with AsyncE2AClient(api_key="e2a_...") as client:
    async for email in client.listen("research-assistant@agents.e2a.dev"):
        print(f"{email.sender}: {email.subject}")
        await email.reply("Got it, on it.")

listen() holds a WebSocket, receives a lightweight notification when mail arrives, fetches the full message via REST, and yields an AsyncInboundEmail object you can reply to inline.

Thread conversations across turns

Multi-turn threading is the difference between an agent that sends isolated emails and one that maintains context. Pass conversation_id on each outbound and you'll see it back on subsequent inbounds in the same thread:

async for email in client.listen("research-assistant@agents.e2a.dev"):
    convo_id = email.conversation_id or new_id()

    draft = await compose_reply(email, history=load_history(convo_id))
    await email.reply(draft, conversation_id=convo_id)

Works across humans replying from Gmail and other e2a agents replying via the platform. First contact from a human arrives with conversation_id=None — assign one yourself, and every reply in that thread will carry it.

Verify the sender

Every inbound includes signed auth headers showing SPF/DKIM results. Use them to decide how much to trust the message:

if email.is_verified:
    # SPF/DKIM passed; sender domain is authenticated
    handle(email)
else:
    # Could be a spoof — route to quarantine or ask for confirmation
    flag_for_review(email)

The signature is HMAC-SHA256 over a canonical string; you can verify it yourself against your webhook secret if you want defense in depth.

What you can build from here

The three primitives — send, listen/webhook, and conversation_id threading — cover most agent-in-your-inbox use cases. A few ideas:

Anything that can receive email and take action can now have an agent attached to it. The Python SDK README and API reference cover attachments, CC/BCC, reply-all, and the auth header details if you want to go deeper.