A LangChain agent with its own inbox
LangChain's create_agent plus three tools over the Mailgi API. The agent gets a real address, not a seat in your Gmail.
To build a LangChain email agent, register a Mailgi address with one POST, wrap three plain functions (send_email, list_inbox, read_email) with LangChain's tool decorator, and pass them to create_agent. The agent then reads and answers mail at its own address with no OAuth flow or shared inbox.
Give the agent an address
One call, no signup form. Do this once per agent and keep the key in your secrets store.
Register
The response carries the address and an API key that is shown exactly once.
Wait a couple of seconds before the first send while the mailbox is provisioned.
# Once, outside the agent: give it an address and keep the key
curl -s -X POST https://api.mailgi.xyz/v1/agents/register \
-H "Content-Type: application/json" -d '{"label":"my-agent"}'
# => {"emailAddress":"buzzing-falcon@mailgi.xyz","apiKey":"amb_..."}
export MAILGI_API_KEY=amb_... # shown once: store it
Three tools, plain HTTP
Framework-free functions over the REST API, saved as mailgi_tools.py. Each was run against the live API before publishing.
The three tools
Docstrings become the tool descriptions the model sees, so keep them short and literal.
from is a list of objects; the address is m['from'][0]['email']. Treating it as a string is the most common mistake with this API.
A 402 comes back as text with a top-up link rather than an exception, so the model can explain it.
import os, time, requests
API = "https://api.mailgi.xyz"
KEY = os.environ["MAILGI_API_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
def send_email(to: str, subject: str, body: str) -> str:
"""Send a plain-text email from the agent's own address."""
r = requests.post(f"{API}/v1/mail/send", headers=H,
json={"to": [to], "subject": subject, "textBody": body})
if r.status_code == 402:
return "Not sent: balance too low. Top up: " + r.json()["error"]["details"]["topupPage"]
r.raise_for_status()
return f"Sent, message id {r.json()['messageId']}"
def list_inbox(limit: int = 10) -> str:
"""List the newest messages: id, sender, subject, preview."""
r = requests.get(f"{API}/v1/mail", headers=H, params={"limit": limit})
r.raise_for_status()
return "\n".join(
f"{m['id']} | {m['from'][0]['email']} | {m['subject']} | {m['preview'][:80]}"
for m in r.json()["messages"]) or "Inbox is empty."
def read_email(message_id: str) -> str:
"""Read one message's full text."""
r = requests.get(f"{API}/v1/mail/{message_id}", headers=H)
r.raise_for_status()
m = r.json()
body = m.get("textBody") or m.get("htmlBody") or ""
if isinstance(body, list): # JMAP parts
body = "".join(m["bodyValues"][p["partId"]]["value"] for p in body)
return f"From: {m['from'][0]['email']}\nSubject: {m['subject']}\n\n{body}"
Wire them into LangChain
The agent
tool() turns each function into a LangChain tool, using its docstring as the description.
Tested with langchain 1.4. Swap the model string for any provider LangChain supports.
# pip install langchain langchain-anthropic requests
from langchain.agents import create_agent
from langchain.tools import tool
import mailgi_tools as mail
agent = create_agent(
"anthropic:claude-sonnet-5-5", # any chat model LangChain supports
tools=[tool(mail.send_email), tool(mail.list_inbox), tool(mail.read_email)],
system_prompt=(
"You have your own email address. Use list_inbox and read_email "
"to check mail, and send_email to reply. Never invent a message id."
),
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Any new mail? Summarise it."}]}
)
print(result["messages"][-1].content)
What the tools can't do yet
Better to know before the model tries.
No search, no threads
There is no search parameter; list_inbox pages newest first and the model matches messages itself. Replies are new messages with “Re:” in the subject.
No attachments
Send links instead. Incoming attachments are not exposed.
Mail to strangers is metered
Sending to other @mailgi.xyz agents is free; to outside addresses it costs $0.0005 per recipient plus $0.02 the first time you write to a new address. With no balance, send_email returns the top-up link instead of raising, so the model can tell the user.
Questions, answered
01How do I give a LangChain agent email access?
Give it its own address rather than access to yours: register one with a single POST to Mailgi, then wrap send, list and read calls to the Mailgi REST API as LangChain tools and pass them to create_agent.
02Why not use the Gmail toolkit?
It works for your own inbox, but it needs OAuth, and the agent can read everything in that mailbox. For an agent shipped to other people, Gmail's restricted scopes also require verification and an annual security assessment.
03Can the agent reply in the same thread?
Not with threading headers. send_email starts a new message; put “Re:” and the original subject in the subject line and most mail clients group it correctly.
04Does it work with LangGraph?
Yes. create_agent returns a LangGraph graph, and the same tools work in any custom LangGraph node.
One POST to start.
No signup form, no OAuth, no card to start. Receiving is free.
Open the dashboard →