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
The SDK is async-only. A few lines:
from e2a.v1 import E2AClient
async with E2AClient(api_key="e2a_...") as client:
await client.messages.send(
"research-assistant@agents.e2a.dev",
{
"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:
Webhook delivery — subscribe a webhook (POST /v1/webhooks), and when Alice replies, e2a POSTs the signed event to your server. Verify the signature with construct_event (it raises on a bad signature or a replay):
from e2a.v1 import E2AClient, construct_event
@app.post("/webhook")
async def inbox(request):
raw = await request.body()
event = construct_event(raw, request.headers["X-E2A-Signature"], WEBHOOK_SECRET)
msg = event.data # metadata only (ids, sender, subject) — fetch the body via messages.get if you need it
print(f"{msg['from']}: {msg['subject']}")
async with E2AClient(api_key="e2a_...") as client:
await client.messages.reply(
msg["recipient"], msg["message_id"], {"body": "Got it. Does Thursday 2pm work?"}
)
return {"ok": True}
WebSocket delivery — no public URL needed. Great for running agents on your laptop or behind a firewall:
from e2a.v1 import E2AClient
async with E2AClient(api_key="e2a_...") as client:
async for notif in client.listen("research-assistant@agents.e2a.dev"):
msg = await client.messages.get(notif.recipient, notif.message_id)
print(f"{notif.from_}: {notif.subject}")
await client.messages.reply(notif.recipient, notif.message_id, {"body": "Got it, on it."})
listen() holds a WebSocket and yields a lightweight WSNotification when mail arrives (message_id, from_, subject, conversation_id). Fetch the full body with client.messages.get(...) and respond with client.messages.reply(...).
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 notif in client.listen("research-assistant@agents.e2a.dev"):
convo_id = notif.conversation_id or new_id()
msg = await client.messages.get(notif.recipient, notif.message_id)
draft = await compose_reply(msg, history=load_history(convo_id))
await client.messages.reply(
notif.recipient, notif.message_id, {"body": 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
Inbound messages carry SPF/DKIM results so you can decide how much to trust them, and webhook deliveries are signed end-to-end. Always verify the signature on a webhook before acting on the payload — construct_event does it for you and raises on a bad signature or a replay older than five minutes:
from e2a.v1 import E2AClient, construct_event
from e2a.v1.errors import E2AWebhookSignatureError
try:
event = construct_event(raw_body, signature_header, WEBHOOK_SECRET)
except E2AWebhookSignatureError:
# Could be a spoof — reject it
return Response(status_code=401)
handle(event)
The signature is HMAC-SHA256 over {timestamp}.{raw_body}, keyed by your webhook's signing secret (whsec_...).
What you can build from here
The three primitives — messages.send, listen/webhook, and conversation_id threading — cover most agent-in-your-inbox use cases. A few ideas:
- A scheduling assistant that you CC on meeting threads; it coordinates with the other side and books calendar holds.
- A customer-support triage agent that handles first replies and escalates to humans when confidence drops.
- A voice agent that sends follow-up emails after calls, then keeps the thread going when the person replies.
- A procurement bot that chases vendors for quotes and aggregates responses.
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.