Give Your AI Agent a Real Email Inbox in Google Colab
AI agents can't complete real-world flows without email. Learn how to give your agent a live inbox in Google Colab using a temporary email API — OTP retrieval, signup automation, and MCP tools included.
The Gap Nobody Talks About
You can run a Python agent in Google Colab. It can call APIs, write code, scrape pages, parse documents, and call LLMs. But the moment that agent needs to complete a real-world account registration — the kind that sends a verification email — it stops cold.
No inbox. No way to receive an OTP. The flow is blocked.
This is not a minor inconvenience. A large fraction of real-world automation depends on email as a control point:
- Account signups with email confirmation
- Two-factor authentication via OTP
- Subscription confirmations and approval flows
- Password resets and verification links
The agent can get to the "Check your email" page. It just can't do anything after that.
This article shows how to fix that. You'll have a working, programmable email inbox running in Google Colab by the end — one the agent controls entirely, no human in the loop.
Why Traditional Solutions Fail
The obvious workarounds don't hold up in practice.
Mocking the email step is the most common approach. You bypass the verification entirely in your test environment. The problem: the agent doesn't learn to handle the real flow, and when you need production-level automation (provisioning accounts on external services, for example), the mock is useless.
Using a shared inbox like qa-team@yourdomain.com introduces race conditions the moment anything runs in parallel. Two agent runs trigger two verification emails into the same inbox. You can't reliably match which code belongs to which session.
Gmail IMAP is fragile at a level that makes it unsuitable for automated use. OAuth2 tokens expire, app passwords get blocked by workspace policies, IMAP polling adds unpredictable latency, and you're parsing raw RFC 2822 MIME messages — base64-encoded, quoted-printable — to get to the text body. It breaks every few months.
Manual verification defeats the purpose. Agents that pause for human input are not agents; they're wizards.
What Agents Actually Need
An agent that can handle email needs four things:
- An email address it controls, created on demand
- Real SMTP delivery — not simulated, not mocked
- A programmatic way to read incoming messages
- Automatic cleanup when the session ends
This maps exactly to what a programmable temporary email infrastructure provides.
UnCorreoTemporal is that infrastructure. It accepts real SMTP traffic, stores messages in a PostgreSQL database, exposes a REST API for reading them, and pushes real-time events via WebSockets. Every inbox is ephemeral by design — it expires on a configurable schedule without any teardown code required.
It is also MCP-native, which means Claude Desktop and any other MCP-compatible agent runtime can call it as a tool directly. More on that at the end.
Setting Up in Google Colab
Open a new Colab notebook. No special setup required — requests is available in every Colab runtime.
# Cell 1 — install nothing, requests is pre-installed
import requests
import re
import time
BASE_URL = "https://api.uncorreotemporal.com/api/v1"
Create an Inbox
A single POST call creates an ephemeral inbox. No authentication required for anonymous use. The response includes the address and a session_token that authenticates all subsequent operations.
# Cell 2 — create inbox
def create_inbox(ttl_minutes: int = 10) -> tuple[str, str]:
resp = requests.post(
f"{BASE_URL}/mailboxes",
params={"ttl_minutes": ttl_minutes},
)
resp.raise_for_status()
data = resp.json()
return data["address"], data["session_token"]
address, token = create_inbox(ttl_minutes=10)
print(f"Inbox: {address}")
# Output: mango-panda-42@uncorreotemporal.com
The address is human-readable (adjective-noun-number@uncorreotemporal.com) and unique per creation. The inbox is live immediately — it starts accepting SMTP delivery the moment it's created.
Read Incoming Messages
The list endpoint returns message metadata without loading full bodies, keeping responses fast. Fetch the full body only when a message has arrived.
# Cell 3 — wait for incoming email
def wait_for_email(address: str, token: str, timeout: int = 30) -> dict:
headers = {"X-Session-Token": token}
deadline = time.time() + timeout
while time.time() < deadline:
resp = requests.get(
f"{BASE_URL}/mailboxes/{address}/messages",
headers=headers,
)
resp.raise_for_status()
messages = resp.json()
if messages:
msg_id = messages[0]["id"]
full = requests.get(
f"{BASE_URL}/mailboxes/{address}/messages/{msg_id}",
headers=headers,
)
full.raise_for_status()
return full.json()
time.sleep(1.5)
raise TimeoutError(f"No email received within {timeout}s")
The full message response includes body_text and body_html as pre-decoded Python strings. No MIME parsing on your end — the server handles all of that internally via core/parser.py.
End-to-End Example: OTP Automation in Colab
Here's a complete flow you can run cell by cell. It simulates a signup that triggers a verification email, waits for it, and extracts the OTP.
# Cell 4 — simulate a service that sends a verification email
# Replace this with your actual service's signup endpoint
def simulate_signup(email: str) -> None:
"""
In a real scenario this would be:
requests.post("https://yourservice.com/register", json={"email": email})
Here we use a test endpoint that echoes a verification code back to the address.
"""
print(f"[→] Registering with email: {email}")
# Your actual call goes here
# Cell 5 — extract OTP from body text
def extract_otp(body_text: str) -> str:
# Match 6-digit codes not adjacent to other digits
match = re.search(r"(?<!\d)(\d{6})(?!\d)", body_text)
if not match:
raise ValueError("No 6-digit OTP found in email body")
return match.group(1)
# Cell 6 — full end-to-end flow
import requests, re, time
BASE_URL = "https://api.uncorreotemporal.com/api/v1"
# Step 1: create inbox
resp = requests.post(f"{BASE_URL}/mailboxes", params={"ttl_minutes": 10})
resp.raise_for_status()
data = resp.json()
address, token = data["address"], data["session_token"]
print(f"[✓] Inbox created: {address}")
print(f"[✓] Expires at: {data['expires_at']}")
# Step 2: use the address to sign up (replace with your service)
print(f"[→] Submitting signup with {address}...")
# requests.post("https://yourservice.com/register", json={"email": address})
# Step 3: wait for the verification email
print("[…] Waiting for verification email...")
headers = {"X-Session-Token": token}
deadline = time.time() + 30
message = None
while time.time() < deadline:
resp = requests.get(
f"{BASE_URL}/mailboxes/{address}/messages",
headers=headers,
)
resp.raise_for_status()
msgs = resp.json()
if msgs:
msg_id = msgs[0]["id"]
full = requests.get(
f"{BASE_URL}/mailboxes/{address}/messages/{msg_id}",
headers=headers,
)
full.raise_for_status()
message = full.json()
break
time.sleep(1.5)
if not message:
raise TimeoutError("Email not received in 30 seconds")
print(f"[✓] Email received from: {message['from_address']}")
print(f"[✓] Subject: {message['subject']}")
# Step 4: extract OTP
body = message["body_text"] or ""
match = re.search(r"(?<!\d)(\d{6})(?!\d)", body)
if match:
otp = match.group(1)
print(f"[✓] OTP extracted: {otp}")
else:
print("[!] No OTP found — check body_html if service sends HTML-only email")
print(message["body_html"][:500])
# Step 5: use the OTP to complete verification
# requests.post("https://yourservice.com/verify", json={"email": address, "code": otp})
print("[✓] Flow complete")
This is a real, runnable Colab notebook. Swap in your service's registration endpoint and you have a fully autonomous email verification flow — no human, no shared inbox, no IMAP credentials.
Real-Time Option: WebSocket Instead of Polling
If you're building a long-running agent and want push delivery instead of polling, the WebSocket endpoint fires immediately when an email arrives:
import asyncio
import json
import websockets
async def wait_for_email_ws(address: str, token: str) -> str:
url = f"wss://api.uncorreotemporal.com/ws/inbox/{address}?token={token}"
async with websockets.connect(url) as ws:
async for raw in ws:
event = json.loads(raw)
if event.get("event") == "new_message":
return event["message_id"]
# In Colab, asyncio.run() works from Python 3.7+
# message_id = asyncio.run(wait_for_email_ws(address, token))
The event is {"event": "new_message", "message_id": "<uuid>"}. The server sends a keepalive ping every 30 seconds to prevent connection drops in long-running workflows.
How This Works Internally
Understanding the architecture matters if you're integrating this into a production pipeline.
External SMTP sender
│
▼
AWS SES (spam + virus filter)
│ SNS webhook
▼
POST /api/v1/ses/inbound
│
▼
core/delivery.deliver_raw_email()
│
┌────┴────┐
▼ ▼
PostgreSQL Redis pub/sub
(messages) channel: mailbox:{address}
│
▼
WebSocket /ws/inbox/{address}
{"event": "new_message", "message_id": "..."}
Key components:
- FastAPI async backend: All API endpoints use async SQLAlchemy with PostgreSQL. The ORM models define
body_text,body_html,from_address,subject, andattachmentsas top-level fields — no raw MIME on your side. - SMTP ingestion: A local
aiosmtpd-based server handles direct SMTP delivery in development. In production, AWS SES receives inbound mail and forwards it via SNS webhook to/api/v1/ses/inbound. Both paths funnel through the samedeliver_raw_email()function. - Redis pub/sub: When a message is stored, it publishes to
mailbox:{address}. The WebSocket handler inws/inbox.pysubscribes to that channel and forwards the event to connected clients. Polling and push are both real — they're driven by the same underlying event. - Background expiration: Inboxes carry an
expires_attimestamp. A background worker soft-deletes expired mailboxes automatically. No cleanup code in your automation.
The architecture handles concurrency well. Each inbox is isolated. Parallel agent runs creating separate inboxes never share state.
Why This Matters for AI Agents
Most discussions of AI agent capabilities focus on tool-use: agents that can search the web, run code, call APIs. Email is treated as a solved problem — but it's not. The existing solutions require either human intervention or fragile integrations that break under real-world conditions.
Email is a coordination primitive. It's how external services signal back to your agent. Without it:
- Agents can't complete onboarding flows on third-party platforms
- QA pipelines that test registration flows require human testers
- Multi-step automation that depends on email confirmations stalls indefinitely
Programmable ephemeral inboxes change the constraint. Each agent gets its own inbox, lives for the duration of the task, and is gone when the job is done. The flow that was blocked is now automated.
Concrete use cases this enables:
- Autonomous signup testing: Create an account, confirm the email, assert the welcome message arrived — fully automated, repeatable in CI
- Agent-driven account provisioning: An agent registers on a third-party service, receives and processes the confirmation, continues the workflow
- Parallel testing without collision: 20 test runs, 20 inboxes, zero shared state
- Ephemeral research agents: An agent registers for access to a dataset or service, uses it, and the inbox expires when the job is done
MCP Integration for Claude and Compatible Agents
If you're building with Claude Desktop or any MCP-compatible agent runtime, UnCorreoTemporal ships a native MCP server. Configure it once, and the agent calls inbox tools directly — no REST calls in your prompt, no session token management in agent context:
{
"mcpServers": {
"uncorreotemporal": {
"command": "python",
"args": ["-m", "uncorreotemporal.mcp.server"],
"env": { "UCT_API_KEY": "uct_your_key_here" }
}
}
}
The agent can then call create_mailbox(), get_messages(address), and read_message(address, message_id) as first-class tools — the same way it would call a search engine or code executor. Email becomes a native agent capability, not a bolted-on integration.
What You Can Build
The pattern shown here — create inbox, trigger external flow, wait for email, extract code or link — composes into larger systems:
- A pytest fixture that creates a fresh inbox per test case and tears down automatically
- A CI/CD stage that provisions test accounts on every build with zero shared credentials
- A Colab-based agent that navigates real-world signup flows end to end
- A research tool that registers for services, collects confirmation emails, and logs structured data
The API surface is small. The core loop is three HTTP calls. The inbox handles the rest.
Try it at uncorreotemporal.com — your first inbox is one requests.post() away.
Written by
Software Engineer · Sr. Python Developer · AWS Certified Solutions Architect
Software engineer with 20 years of experience building Python backends, cloud infrastructure, and AI agent tooling. Builder of UnCorreoTemporal.
LinkedInReady to give your AI agents a real inbox?
Create your first temporary mailbox in 30 seconds. Free plan available.
Create your free mailbox