Email on inbox.sails.app. Bodies live under .sails/threads/. Inbox and sent stubs point at the thread. Two mailbox kinds:
| Kind | Create | Inbound | Folder |
|---|---|---|---|
| Agent | agent create plus optional agent claim | Starts a background goal (events/email.received.md) | agents/{id}/mail/ |
| Folder inbox | inbox create USERNAME | Stores stubs only. No goal | inboxes/{username}/mail/ |
MCP clients that should read and send themselves use a folder inbox. Agents use mail when inbound should wake a goal with a soul.
Done looks like
Outbound: mail send succeeds. The recipient gets one HTML email from markdown --text (plain-text fallback kept). With --thread, it is the same conversation in Sails and in Gmail.
Inbound reply from Gmail: it lands in the same Sails thread as your send, because Sails matches the reply's In-Reply-To header to the Message-ID it stamped on the way out.
Inbound to an agent: stub under agents/{id}/mail/inbox/, then a background goal as that agent. First tool call is thread show for that thread.
Inbound to a folder inbox: stub under inboxes/{username}/mail/inbox/ only. No goal.
Worked example: folder inbox for an MCP client
Job: claim helpdesk, send one note, find the reply, hydrate, answer in the same thread.
inbox create helpdesk
# {org}-helpdesk@inbox.sails.app
# or helpdesk@inbox.sails.app when your org owns the mail domain
# folder: inboxes/helpdesk/mail/inbox/
mail send --as helpdesk --to user@example.com --subject "Hi" --text "Hello **world**"
thread list --as helpdesk # find the THREAD_ID
thread show THREAD_ID --limit 5 --as helpdesk # last 5 messages, full bodies
mail send --as helpdesk --to user@example.com --thread THREAD_ID --subject "Hi" --text "Following up."
inbox create, inbox claim-bare, and inbox rm are repository owner only. Folder inboxes require --as (su only works for agents). Owners may pass --as AGENT|INBOX without su.
Agent send (owner, no su):
mail send --as site-bot --to you@example.com --subject "Site update" --text "Shipped the **subscribe form**."
In a background goal running as the agent, prefer the mail_send tool (to, subject, text, thread_id). Bash mail send works when workspace commands are wired.
What the output looks like
inbox show reed on an org that owns the domain (real output, ids from a live repo):
{
"address": "01a1500b-6815-7615-a3fe-1a9d2fd1bc6e@inbox.sails.app",
"alias_address": "ryan-reed@inbox.sails.app",
"claimed_address": "reed@inbox.sails.app",
"kind": "mailbox",
"mail_bare": true,
"mail_inbox": "inboxes/reed/mail/inbox",
"mail_username": "reed",
"name": "reed",
"note": "Inbound mail is stored here; no agent goal is started. Hydrate with thread show."
}
mail_bare: true means From is the bare address. alias_address is the old prefixed form, still accepted inbound. A prefixed inbox shows mail_bare: false and alias_address: null.
mail inbox --as reed lists stub file names, not bodies:
{
"inbox": "/ryan/home/inboxes/reed/mail/inbox",
"kind": "mailbox",
"messages": ["698711d9-5b19-4894-82ab-2d9f02e5a9ab.md"],
"note": "Use thread show THREAD_ID to hydrate full history"
}
thread show returns thread_id, subject, and messages[], each with from_address, to_address, subject, body, created_at, and in_reply_to.
Addresses
| Form | Meaning |
|---|---|
{uuid}@inbox.sails.app | Agent or folder inbox UUID. Always works |
{org}-{username}@inbox.sails.app | After agent claim USERNAME or inbox create USERNAME |
{username}@inbox.sails.app | Same commands, when your org owns the mail domain |
Outbound From prefers the claim when set. Claims are unique per org across agents and folder inboxes. Bare names are unique per domain, cannot collide with an existing {org}-{username} address, and cannot be a reserved local-part (postmaster, abuse, support, and similar). Do not publish inboxes/ as a site root.
Bare addresses for domain owners
An org owns the mail domain when it is the instance operator, or when it has a verified published hostname that is the mail domain or a DNS parent of it (sails.app owns inbox.sails.app). That org gets the exact local-part:
inbox create reed
# reed@inbox.sails.app
An inbox claimed before the org owned the domain stays prefixed until you promote it:
inbox claim-bare reed
# or by id: inbox claim-bare 01a1500b-6815-7615-a3fe-1a9d2fd1bc6e
After that, mail send --as reed sends From: reed@inbox.sails.app, and {org}-reed@inbox.sails.app keeps delivering as an inbound alias. Other orgs cannot reserve a bare name on a domain they do not own; their claims stay {org}-USERNAME@.
Threading
Always pass --thread when replying. Without it, the send is a new conversation. Subject is prefixed with Re: when threading and the subject does not already start with Re:.
Every outbound send sets Message-ID: <{message_uuid}@{domain}>. When the recipient replies, Gmail sends In-Reply-To and References, and Sails files the reply into the thread whose stored Message-ID matches.
Sends made before Message-IDs were stored have none, so a reply to one of them opens a new Sails thread (you will see two threads with the same subject, one starting Re:). Sails repairs these on its own when an inbound lookup misses (this repository, up to 25 rows). To repair a mailbox in one pass:
mail backfill-rfc --as helpdesk
The inbox stub is not the full conversation. Call thread show THREAD_ID before you decide or reply. Inbound agent goals prepend: first tool call should be thread_show for that id.
Commands
Live usage (mail --help, inbox --help, thread --help):
mail send --to ADDR [--subject S] [--thread ID] [--text BODY | --html-file PATH] [--as AGENT|INBOX]
mail inbox [--as AGENT|INBOX]
mail show PATH|ID [--as AGENT|INBOX]
mail backfill-rfc [--as AGENT|INBOX]
thread show THREAD_ID [--limit N] [--as AGENT|INBOX]
thread list [--as AGENT|INBOX]
inbox create USERNAME
inbox list
inbox show USERNAME|ID
inbox claim-bare USERNAME|ID
inbox rm USERNAME|ID
| Flag / command | What it does |
|---|---|
--to | Recipient. Required for mail send |
--subject | Subject line |
--text | Markdown body (default). Prefer this over HTML |
--html-file PATH | Use only when you already have HTML |
--thread ID | Continue a conversation; required for replies |
--as AGENT or --as INBOX | Owner acts as that mailbox without su. Required for folder inboxes |
mail inbox | List inbox stub file names |
mail show PATH|ID | One stub by path, or by id under mail/inbox/ |
mail backfill-rfc | Fill missing Message-IDs on older sends so replies thread |
thread list | Every thread the mailbox is in, with id and subject |
thread show ID --limit N | Full history (or the last N messages) before you reply |
inbox create | Claim a folder inbox (owner only) |
inbox claim-bare | Promote a prefixed claim to the bare address (domain owners) |
Delivery
Same-domain addresses on inbox.sails.app deliver locally (agent and folder inboxes). External addresses go through the platform (Resend).
Agent and folder inboxes are platform-managed. Do not webhook add for {uuid}@inbox.sails.app. Use Start a goal from a webhook only when connecting your own email provider.
Failure modes
CLI errors are copied from live runs.
| What you see | What it means | What to do |
|---|---|---|
mail: run as an agent (su AGENT) or pass --as ... | No mailbox chosen | Add --as NAME, or su AGENT first |
su: inboxes are not agents; use mail --as USERNAME | You tried su on a folder inbox | mail ... --as USERNAME |
mail send: --to is required | Missing recipient | Add --to ADDR |
inbox not found: NAME | No folder inbox by that name or id | inbox list for the real names |
thread show: invalid THREAD_ID | Not a thread UUID | Copy id from thread list |
mail show: ENOENT ... mail/inbox/ID.md | That stub id is not in this mailbox | Use a name from mail inbox, or pass the full path |
Reply opens a second thread starting Re: | Your send had no stored Message-ID, or you omitted --thread | mail backfill-rfc --as NAME; always reply with --thread |
| Goal acts on a stub and gets it wrong | Stub is not the body | First call: thread show THREAD_ID |
Mail to name@inbox.sails.app never arrives | No claim, wrong local-part, or bare form on an org that does not own the domain | Use {uuid}@inbox.sails.app or the claimed_address from inbox show |
inbox create helpdesk fails | Username already claimed by an agent or another inbox, or reserved | Pick another username |
You used webhook add for agent mail | Wrong ingress | Platform mail is not a custom webhook |
| Same-repo agent never got the letter | You used an external address | Send to {uuid or claim}@inbox.sails.app for local delivery |
Agent-to-agent inside the repo without email is message send, not mail send.
How to: Mail from an agent.