Give your ADK agents a supervised email channel — send, read, and approve messages through MultiMail's REST API with graduated oversight that fits ADK's tool-calling model.
Google ADK structures agents as tool-calling systems: each capability is a typed Python function the agent can invoke during a session. Email is a natural fit — your agent can check an inbox, read a thread, draft a reply, or send a new message as first-class tool calls, not raw SMTP calls embedded in the prompt.
MultiMail is designed for exactly this pattern. Its REST API exposes each email operation as a discrete endpoint — send_email, check_inbox, read_email, reply_email, decide_email — that maps cleanly to an ADK tool definition. Each call carries your API key, and each mailbox has an oversight mode that governs how much the agent can do without human approval.
The combination is particularly useful for long-running ADK workflows that end in outbound communication. Rather than giving the agent direct SMTP access, you route email through MultiMail and choose an oversight mode: gated_send holds every outbound message for human approval before delivery, while monitored lets the agent act freely and notifies a human on every send.
Every MultiMail operation maps to one ADK tool definition. send_email, check_inbox, read_email, and decide_email are individual REST endpoints, not a general-purpose SMTP wrapper. ADK's type-annotated function tools translate directly — no adapter layer needed.
ADK sessions can span many steps before producing outbound communication. MultiMail's gated_send mode holds the final send for approval without blocking the session — the agent keeps working while a human reviews the draft in a separate flow.
Each ADK agent can be assigned its own MultiMail mailbox ([email protected], [email protected]). Recipients see a distinct sender address per agent, and you can apply different oversight modes to each mailbox independently.
When a message is held for approval in gated_send mode, MultiMail fires a webhook. Wire this to an ADK evaluation step or a human callback without polling list_pending on a timer.
ADK evaluation workflows use MultiMail test keys (mm_test_...) that record sends without delivering them. Switch to mm_live_... for production. Tool function signatures are identical in both modes — just swap the environment variable.
import os
import requests
from google.adk.agents import Agent
MULTIMAIL_API = "https://api.multimail.dev"
HEADERS = {
"Authorization": f"Bearer {os.environ[&"cm">#039;MULTIMAIL_API_KEY']}",
"Content-Type": "application/json",
}
def send_email(to: str, subject: str, body: str) -> dict:
"""Send an email from the agent&"cm">#039;s mailbox. Returns message_id and delivery status."""
resp = requests.post(
f"{MULTIMAIL_API}/send_email",
headers=HEADERS,
json={"to": to, "subject": subject, "body": body},
)
resp.raise_for_status()
return resp.json()
def check_inbox(mailbox: str, unread_only: bool = True) -> dict:
"""List recent emails in a mailbox. Returns a list of message summaries with IDs."""
resp = requests.get(
f"{MULTIMAIL_API}/check_inbox",
headers=HEADERS,
params={"mailbox": mailbox, "unread_only": unread_only},
)
resp.raise_for_status()
return resp.json()
def read_email(message_id: str) -> dict:
"""Fetch the full body and metadata of a single email by message_id."""
resp = requests.get(
f"{MULTIMAIL_API}/read_email",
headers=HEADERS,
params={"message_id": message_id},
)
resp.raise_for_status()
return resp.json()
def reply_email(message_id: str, body: str) -> dict:
"""Reply to an existing email thread. Preserves thread context automatically."""
resp = requests.post(
f"{MULTIMAIL_API}/reply_email",
headers=HEADERS,
json={"message_id": message_id, "body": body},
)
resp.raise_for_status()
return resp.json()
agent = Agent(
model="gemini-2.0-flash",
name="email_agent",
instruction="You are an email assistant. Use the provided tools to read and respond to emails.",
tools=[send_email, check_inbox, read_email, reply_email],
)Wrap MultiMail REST endpoints as typed Python functions. ADK reads type annotations and docstrings to build the tool schema automatically — no decorator or registration call required.
import os
import requests
from google.adk.agents import Agent
MULTIMAIL_API = "https://api.multimail.dev"
HEADERS = {
"Authorization": f"Bearer {os.environ[&"cm">#039;MULTIMAIL_API_KEY']}",
"Content-Type": "application/json",
}
def send_email(to: str, subject: str, body: str) -> dict:
"""Queue an email for human approval before delivery. Returns pending message_id."""
resp = requests.post(
f"{MULTIMAIL_API}/send_email",
headers=HEADERS,
json={"to": to, "subject": subject, "body": body},
)
resp.raise_for_status()
"cm"># With gated_send oversight, result["status"] == "pending_approval"
return resp.json()
def list_pending() -> dict:
"""List all outbound messages currently waiting for human approval."""
resp = requests.get(f"{MULTIMAIL_API}/list_pending", headers=HEADERS)
resp.raise_for_status()
return resp.json()
agent = Agent(
model="gemini-2.0-flash",
name="gated_email_agent",
instruction=(
"Draft and queue outbound emails using send_email. "
"Always confirm the recipient and subject before sending. "
"Inform the user that messages require human approval before delivery."
),
tools=[send_email, list_pending],
)
"cm"># Human-side approval — called from your webhook handler or approval UI
def decide_email(message_id: str, decision: str, reason: str = "") -> dict:
"""Approve or reject a pending message. decision must be &"cm">#039;approve' or 'reject'."""
resp = requests.post(
f"{MULTIMAIL_API}/decide_email",
headers=HEADERS,
json={"message_id": message_id, "decision": decision, "reason": reason},
)
resp.raise_for_status()
return resp.json()With gated_send oversight on the mailbox, send_email returns pending_approval immediately. The ADK session continues without blocking. A human approves or rejects via decide_email before delivery occurs.
import asyncio
import os
import requests
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
MULTIMAIL_API = "https://api.multimail.dev"
HEADERS = {
"Authorization": f"Bearer {os.environ[&"cm">#039;MULTIMAIL_API_KEY']}",
"Content-Type": "application/json",
}
MAILBOX = "[email protected]"
def check_inbox(unread_only: bool = True) -> dict:
"""List recent emails in the support mailbox. Returns message summaries with IDs."""
resp = requests.get(
f"{MULTIMAIL_API}/check_inbox",
headers=HEADERS,
params={"mailbox": MAILBOX, "unread_only": unread_only},
)
resp.raise_for_status()
return resp.json()
def read_email(message_id: str) -> dict:
"""Fetch full email content by message_id."""
resp = requests.get(
f"{MULTIMAIL_API}/read_email",
headers=HEADERS,
params={"message_id": message_id},
)
resp.raise_for_status()
return resp.json()
def tag_email(message_id: str, tags: list[str]) -> dict:
"""Apply classification tags to an email (e.g. billing, technical, general)."""
resp = requests.post(
f"{MULTIMAIL_API}/tag_email",
headers=HEADERS,
json={"message_id": message_id, "tags": tags},
)
resp.raise_for_status()
return resp.json()
def reply_email(message_id: str, body: str) -> dict:
"""Reply to a message thread. Preserves thread context."""
resp = requests.post(
f"{MULTIMAIL_API}/reply_email",
headers=HEADERS,
json={"message_id": message_id, "body": body},
)
resp.raise_for_status()
return resp.json()
agent = Agent(
model="gemini-2.0-flash",
name="support_triage_agent",
instruction=(
"Check the support inbox, read each unread email, tag it by category "
"(billing, technical, or general), then draft and send a helpful reply "
"using reply_email."
),
tools=[check_inbox, read_email, tag_email, reply_email],
)
async def run_triage():
session_service = InMemorySessionService()
session = await session_service.create_session(
app_name="support_triage", user_id="system"
)
runner = Runner(
agent=agent,
app_name="support_triage",
session_service=session_service,
)
async for event in runner.run_async(
user_id="system",
session_id=session.id,
new_message=types.Content(
role="user",
parts=[types.Part(text="Process all unread support emails.")],
),
):
if event.is_final_response():
print(event.content.parts[0].text)
asyncio.run(run_triage())An ADK session that checks a support inbox, reads unread messages, tags each one by category, and drafts replies — using MultiMail tools across the full lifecycle.
import os
import requests
from google.adk.agents import Agent
from google.adk.evaluation import AgentEvaluator
"cm"># mm_test_... keys record sends without delivering — safe for eval
API_KEY = os.environ.get("MULTIMAIL_API_KEY", "mm_test_your_test_key_here")
MULTIMAIL_API = "https://api.multimail.dev"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def send_email(to: str, subject: str, body: str) -> dict:
"""Send an email. In test mode, records the send without delivering it."""
resp = requests.post(
f"{MULTIMAIL_API}/send_email",
headers=HEADERS,
json={"to": to, "subject": subject, "body": body},
)
resp.raise_for_status()
return resp.json()
def check_inbox(mailbox: str, unread_only: bool = True) -> dict:
"""List recent emails in a mailbox."""
resp = requests.get(
f"{MULTIMAIL_API}/check_inbox",
headers=HEADERS,
params={"mailbox": mailbox, "unread_only": unread_only},
)
resp.raise_for_status()
return resp.json()
agent = Agent(
model="gemini-2.0-flash",
name="email_eval_agent",
instruction="You are an email assistant. Use send_email and check_inbox to manage communications.",
tools=[send_email, check_inbox],
)
"cm"># No real emails sent during evaluation
evaluator = AgentEvaluator()
evaluator.evaluate(
agent=agent,
eval_dataset_file_path="eval/email_agent_test_cases.json",
)Use a test API key during ADK evaluation runs. Test-mode calls to send_email record the send without delivering the message, so full agent sessions can be evaluated without side effects.
Install the ADK package with pip and sign up at multimail.dev to get your API key and provision a mailbox. Use a test key (mm_test_...) during development.
pip install google-adk requests
"cm"># Set your MultiMail API key as an environment variable
export MULTIMAIL_API_KEY="mm_test_your_test_key_here"Define Python functions with type annotations and docstrings for each MultiMail operation your agent needs. ADK builds the tool schema from the signatures automatically — no additional registration is required.
import os
import requests
MULTIMAIL_API = "https://api.multimail.dev"
HEADERS = {
"Authorization": f"Bearer {os.environ[&"cm">#039;MULTIMAIL_API_KEY']}",
"Content-Type": "application/json",
}
def send_email(to: str, subject: str, body: str) -> dict:
"""Send an email from the agent&"cm">#039;s mailbox. Returns message_id and delivery status."""
resp = requests.post(
f"{MULTIMAIL_API}/send_email",
headers=HEADERS,
json={"to": to, "subject": subject, "body": body},
)
resp.raise_for_status()
return resp.json()
def check_inbox(mailbox: str, unread_only: bool = True) -> dict:
"""List recent emails. Returns a list of message summaries with stable IDs."""
resp = requests.get(
f"{MULTIMAIL_API}/check_inbox",
headers=HEADERS,
params={"mailbox": mailbox, "unread_only": unread_only},
)
resp.raise_for_status()
return resp.json()Pass the tool functions to the Agent constructor. Choose an oversight mode in the MultiMail dashboard for your mailbox — gated_send holds every outbound message for human review before delivery.
from google.adk.agents import Agent
agent = Agent(
model="gemini-2.0-flash",
name="email_agent",
instruction=(
"You are an email assistant. Use check_inbox to read incoming messages "
"and send_email to respond. Always confirm the recipient before sending."
),
tools=[send_email, check_inbox],
)Use ADK's Runner and InMemorySessionService to execute the agent. Test keys ensure no real emails are sent during development. Swap to mm_live_... when ready for production.
import asyncio
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
async def main():
session_service = InMemorySessionService()
session = await session_service.create_session(
app_name="email_agent", user_id="dev"
)
runner = Runner(
agent=agent,
app_name="email_agent",
session_service=session_service,
)
async for event in runner.run_async(
user_id="dev",
session_id=session.id,
new_message=types.Content(
role="user",
parts=[types.Part(text="Check my inbox and summarize any unread messages.")],
),
):
if event.is_final_response():
print(event.content.parts[0].text)
asyncio.run(main())Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.