AgentMail is a local mail runtime for agents. It sends mail via SMTP, polls IMAP inboxes, stores raw mail on disk, indexes conversations in SQLite, and can bridge inbound mail into OpenClaw for automated handling.
- Profile-scoped mailbox config under
~/.agentmail/profiles/<profile>/ - Shared SQLite index at
~/.agentmail/agentmail.db - Local persistence for inbound mail, outbound mail, attachments, and metadata
- IMAP polling with one-shot and long-running watch modes
- Receive hooks that run after each newly saved inbound message
- Conversation lookup by sender or session
- A shared OpenClaw dispatch bridge for handing inbound mail to agents
- macOS
launchdhelpers for watchers and the dispatcher - A local inbox web UI for conversations, queue state, bridge logs, and config editing
Each mailbox lives in an AgentMail profile.
- Default profile:
~/.agentmail/* - Named profile:
~/.agentmail/profiles/<profile>/* - Shared database:
~/.agentmail/agentmail.db
Per profile, AgentMail keeps:
.envwith SMTP and IMAP credentialspolling.jsonwith mailbox and intervalmessages/for inbound mailsent/for outbound mailhooks/on_recieve.shreceive-watch.lock
agentmail receive once and agentmail receive watch do the same core work:
- Load profile credentials from
.env - Poll the configured IMAP mailbox for unseen messages
- Save each message locally as:
raw.emlbody.txtbody.htmlattachments/*metadata.json
- Insert or update the shared SQLite index
- Mark the IMAP message as seen
- Run
hooks/on_recieve.shif present
Inbound mail is indexed into a stable local session id:
mail:<profileId>:<sha1(normalized-sender)>
agentmail send sends mail through SMTP and also writes a local sent-message copy into the profile's sent/ directory. Outbound mail is indexed into the same SQLite session graph so local conversation history stays complete.
Reply threading is preserved through:
In-Reply-ToReferences- sender/session matching in the local index
The bridge is the shared dispatcher that turns newly indexed inbound mail into OpenClaw turns.
Flow:
receiveOnce()saves the inbound message and records it in SQLite with dispatch statuspendingagentmail dispatch runoragentmail dispatch onceloads the shared bridge config from~/.openclaw/mail-dispatch/config.json- The dispatcher claims pending or failed rows from SQLite
- It builds an OpenClaw payload from:
- sender/recipient metadata
- subject and threading headers
- attachment list and saved file paths
- local message directory
- body text or stripped HTML fallback
- It sends the turn into OpenClaw with:
openclaw gateway call chat.send --json --timeout 20000 --params '{...}'OpenClaw session isolation is explicit and per sender:
agent:<agentId>:mail:<normalized-sender-email>
That means one mailbox profile can be bound to one agent, while each external sender still gets their own OpenClaw session stream.
If the bound OpenClaw agent workspace contains all of the following files, AgentMail switches from a generic "reply with agentmail" prompt to the structured sales automation flow:
prose/inbound_intake.proselobster/reply_guarded_send.lobster.yamllobster/no_reply.lobster.yamlscripts/agentmail_reply_guard.py
When that automation stack exists, AgentMail writes:
openclaw-inbound-context.jsonsales_work_item.json
into the saved message directory and instructs the agent to use the guarded Lobster flow instead of calling agentmail send directly.
If those workspace files do not exist, the bridge falls back to direct reply instructions using agentmail --profile <profile> send ....
- Bridge log:
~/.agentmail/logs/bridge.log - Receive logs:
~/.agentmail/logs/receive-<profile>.log - Dispatcher log:
~/.agentmail/logs/dispatch.log - Dispatcher health check:
agentmail dispatch doctor - Queue status:
agentmail dispatch status --verbose - Deadletter or failed retry:
agentmail dispatch retry --include-deadletter
Retry schedule for failed dispatches:
- 1 minute
- 5 minutes
- 15 minutes
- 60 minutes
- then
deadletter
agentmail inbox starts a local HTTP server that exposes:
- profiles and sessions
- message bodies and attachments
- dispatch queue state
- bridge logs
- OpenClaw mail sessions
- dispatch bindings
- service status
- profile env editing
- polling config editing
- hook editing
The helper script ./start-ui.sh can run that UI in the foreground or background.
gitbun- an IMAP/SMTP mailbox
openclawCLI if you want the bridge- macOS if you want
agentmail service install(launchdonly)
Recommended check:
git --version
bun --version
openclaw --helpRecommended install:
curl -fsSL https://raw.githubusercontent.com/a1cnore/agent-mail/master/setup.sh | bashWhat setup.sh does:
- clones or updates the repo into
~/.local/src/agent-mail - runs
bun install - builds
dist/agentmail - installs
~/.local/bin/agentmail - adds
~/.local/bintoPATHwhen needed - creates
~/.agentmail/.envfrom.env.exampleif missing
If you already have a local checkout:
cd /path/to/agent-mail
./setup-local.shManual build:
bun install
bun run build:standalone
./dist/agentmail --helpCreate a profile and write its .env and polling.json in one step:
agentmail account create \
--name work \
--email you@example.com \
--smtp-host smtp.example.com \
--smtp-port 465 \
--smtp-secure true \
--smtp-user you@example.com \
--smtp-pass 'smtp-password' \
--imap-host imap.example.com \
--imap-port 993 \
--imap-secure true \
--imap-user you@example.com \
--imap-pass 'imap-password' \
--mailbox INBOX \
--interval 60If you want the profile bound to an OpenClaw agent immediately:
agentmail account create \
--name work \
--email you@example.com \
--smtp-host smtp.example.com \
--smtp-user you@example.com \
--smtp-pass 'smtp-password' \
--imap-host imap.example.com \
--imap-user you@example.com \
--imap-pass 'imap-password' \
--agent sales \
--dispatch-config ~/.openclaw/mail-dispatch/config.jsonValidate the result:
agentmail --profile work config validateThe bridge needs two things:
- a profile-to-agent binding in
~/.openclaw/mail-dispatch/config.json - a working OpenClaw installation that can accept
chat.send
Create or update the binding manually:
agentmail dispatch bind --account work --agent salesThe dispatcher config looks like this:
{
"accounts": {
"work": {
"agentId": "sales",
"enabled": true
}
},
"worker": {
"pollIntervalMs": 1000,
"maxConcurrentSessions": 4
}
}AgentMail reads OpenClaw runtime details from ~/.openclaw/openclaw.json. The important fields are:
gateway.portgateway.auth.tokenagents.list[].idagents.list[].workspace
The workspace path is what enables the optional Lobster/prose automation stack described above.
Use this sequence on a fresh machine:
agentmail --profile work receive once
agentmail dispatch once
agentmail dispatch status --verbose
agentmail dispatch doctorWhat you should see:
- inbound mail saved into
~/.agentmail/profiles/work/messages/... - a row in SQLite with dispatch state updates
- bridge log entries in
~/.agentmail/logs/bridge.log dispatch doctorreporting a valid env, polling config, binding, and watcher state
Manual mode:
agentmail --profile work receive watch
agentmail dispatch runmacOS launchd mode:
agentmail service install
agentmail service statusservice install creates:
- one watcher LaunchAgent per configured profile
- one shared dispatcher LaunchAgent
Everything is written under ~/Library/LaunchAgents/.
Foreground:
agentmail inbox --hostname 127.0.0.1 --port 8025Background helper:
./start-ui.sh --backgroundThen open http://127.0.0.1:8025.
agentmail [--profile <name>] <command>Commands:
sendaccountreceiveconfigindexconversationdispatchserviceinbox
agentmail send --to <list> --subject <text> [options]Key options:
--cc <list>--bcc <list>--text <text>--html <html>--in-reply-to <messageId>--references <list>--attach <path>
agentmail receive setup [--mailbox <name>] [--interval <seconds>]
agentmail receive once [--mailbox <name>]
agentmail receive watch
agentmail receive watch-allBehavior notes:
watchuses the active profilewatchwithout--profileauto-discovers configured profiles and watches them allwatch-allis the explicit "all configured profiles" form
agentmail conversation [--sender <email> | --session <id>] [--include-sent] [--json]agentmail index rebuildRebuilds the shared SQLite index from saved messages/ and sent/ folders.
agentmail dispatch bind --account <name> --agent <id> [--config <path>]
agentmail dispatch run [--config <path>]
agentmail dispatch once [--config <path>]
agentmail dispatch status [--verbose] [--json]
agentmail dispatch inspect [--account <name>] [--sender <email>] [--status <list>]
agentmail dispatch retry [--account <name>] [--sender <email>] [--include-deadletter]
agentmail dispatch doctor [--json]Main operator commands:
agentmail dispatch status --verboseagentmail dispatch doctor
agentmail service install
agentmail service status
agentmail service uninstallThis layer is macOS-specific because it shells out to launchctl.
agentmail inbox [--hostname <host>] [--port <port>]If this file exists, it runs after each newly saved inbound message:
- default profile:
~/.agentmail/hooks/on_recieve.sh - named profile:
~/.agentmail/profiles/<profile>/hooks/on_recieve.sh
Helper script:
./setup-hook.sh
./setup-hook.sh --profile work
./setup-hook.sh --all-profilesHook environment variables include:
AGENTMAIL_HOOK_EVENTAGENTMAIL_PROFILEAGENTMAIL_ACCOUNT_EMAILAGENTMAIL_MAILBOXAGENTMAIL_MESSAGE_UIDAGENTMAIL_MESSAGE_IDAGENTMAIL_MESSAGE_SUBJECTAGENTMAIL_MESSAGE_FROMAGENTMAIL_MESSAGE_TOAGENTMAIL_MESSAGE_CCAGENTMAIL_MESSAGE_REPLY_TOAGENTMAIL_MESSAGE_METADATA_FILEAGENTMAIL_MESSAGE_DIR
Hook failures are logged as warnings and do not stop receive processing.
Default profile:
~/.agentmail/.env~/.agentmail/polling.json~/.agentmail/messages/<timestamp>_uid-<uid>/...~/.agentmail/sent/<timestamp>_msg-<id>/...~/.agentmail/hooks/on_recieve.sh~/.agentmail/receive-watch.lock
Named profile:
~/.agentmail/profiles/<profile>/.env~/.agentmail/profiles/<profile>/polling.json~/.agentmail/profiles/<profile>/messages/<timestamp>_uid-<uid>/...~/.agentmail/profiles/<profile>/sent/<timestamp>_msg-<id>/...~/.agentmail/profiles/<profile>/hooks/on_recieve.sh~/.agentmail/profiles/<profile>/receive-watch.lock
Shared assets:
~/.agentmail/agentmail.db~/.agentmail/logs/bridge.log~/.agentmail/logs/dispatch.log~/.openclaw/mail-dispatch/config.json~/.openclaw/openclaw.json
bun install
bun run typecheck
bun testDo not commit:
- mailbox credentials
~/.agentmail/~/.openclaw/- generated local caches
- local log files
- local SQLite files