| name | mail |
| description | Load when reading, searching, sending, drafting, replying, tagging, or filing email — Gmail, Outlook, Yahoo, and iCloud accounts share one route surface, with provider-native translation at the daemon. |
| allowed-tools | ["Bash(curl *)","Read"] |
Mail Operations Guide (multi-provider)
All mail work goes through /api/mail/*. Gmail, Outlook, Yahoo, iCloud —
same route surface, per-account routing, provider-native translation at the
daemon.
0. Per-account mode-aware routing (read before §1)
This body is materialized in direct mode and same-backend Gmail
delegation. (Cross-backend Gmail delegation pulls
SKILL.delegated.<backend>.md instead — that variant has its own
routing prose.) Read the <integration_modes> block injected above
for every gated kind that is not in direct mode and dispatch
accordingly:
- IMAP, Yahoo, iCloud: always reachable via
/api/mail/:acct/* —
these kinds have no integration-registry entry and stay direct-only
by design. They are unaffected by Gmail / Outlook mode flips.
- Gmail when
gmail="delegated" (same-backend): the per-account
gate returns 410 {"error": "integration_delegated"}. Do NOT retry
via /api/mail/*. Use the in-session Gmail connector your harness
exposes — your tool menu lists every available tool at session start.
The exact tool namespace depends on which Gmail connector your
harness has loaded (Claude / Codex / Gemini sessions each surface
Gmail under their own connector's namespace).
- Outlook when
outlook_mail="delegated": the per-account gate
returns 410 {"error": "integration_delegated"}. outlook_mail is
a user-managed connector — the daemon ships no proxy. Use whatever
Outlook / Microsoft Graph MCP server or connector you (the user)
registered on the backend named in the 410 body's backend field.
If your session runs on a different backend, ask the user which
Outlook tools are available there; the daemon cannot route this for
you.
- Outlook when
outlook_mail="native": the per-account gate
returns 410 {"error": "integration_native"}. Native is bound to
the main backend (see the 410 body's backend field) and the
daemon ships no proxy. Use the Outlook MCP / connector you (the
user) registered on that backend. The daemon does not poll Outlook
in this mode — mail observations only land when the agent fetches
in-turn through the user's MCP during a DM that needs them. There
is no proactive activity_scan fetch for outlook_mail (user-managed
reactive-only contract; see
docs/design/appendices/native-integration-mode.md).
Account-resolution (§1) is unchanged: accounts.md lists every active
account regardless of mode. The branches above only affect the wire
call once an account is selected.
1. Account resolution (do this first)
accounts.md (next to this file) lists the active accounts available this
session — scope-gated to enabled provider ∧ account.active ∧ healthy. If
the file is absent or empty, there are no active mail accounts: tell the
user and stop. Account setup is outside this skill's scope — do NOT
speculate about how to add one.
There is no global "primary" account. Pick one from conversation
context, in this order:
- User named it.
"email from my work gmail" / "reply from my icloud" → match against the email / label columns in accounts.md.
- Replying to a thread. The reply MUST go from the account that
received the original. Find it by the
accountId attached to the
message/thread you fetched.
- Exactly one active account. No ambiguity — use it.
- Multiple active, no context clue. Ask the user which account to
use. Do NOT pick arbitrarily.
If an API call returns 404 not_found on an id you resolved from
accounts.md, the file may be stale. Re-fetch the scope-gated set:
curl -s "http://localhost:8321/api/mail/accounts?active=1"
This returns the same filtered view accounts.md was materialized from.
Do NOT call GET /api/mail/accounts without ?active=1 for recovery — it
includes dormant and unhealthy rows that every operation will reject.
2. Decision rules
Send vs draft
- Prefer drafts. Create via
POST /mail/:acct/drafts. The user sends
from the provider's web UI.
- Direct send (
POST /mail/:acct/messages/send) is autonomous — no owner
DM, no deny gate (see api.md for the deniedTools-scope nuance). When
you judge the user would want to know about a send immediately (e.g. a
reply to a stranger), call POST /api/notify yourself.
- Never include
bcc unless the user explicitly asks for it.
Replies — RFC-2822 headers are the source of truth
Provider thread ids (Gmail threadId, Graph conversationId) differ;
RFC-2822 Message-Id + References is the one chain that works across
all four kinds.
- Fetch the thread metadata:
GET /api/mail/:acct/threads/:threadId?body=none
(returns messages[] in chronological order without raw HTML bodies).
- From the last message, pull
rfc822MsgId (thread message objects carry
no references field — you SUPPLY references in the reply block below,
you do not read it off the message).
- Build the
reply block:
{
"inReplyToRfc822Id": "<last.rfc822MsgId>",
"references": ["<A>", "<B>", "<last.rfc822MsgId>"],
"providerThreadId": "<last.threadId>",
"parentProviderMsgId": "<last.providerMsgId>"
}
providerThreadId is Gmail's threadId / Graph's conversationId. On
IMAP (Yahoo / iCloud) threads are client-walked — omit it there.
- For reply-all, build
to from the original from + existing to, and
cc from the original cc, excluding the sender account's own email
(resolve from accounts.md).
If the thread endpoint returns status: "partial" with `missingAncestors
0`, reply is still safe (the chain you have is headers-valid), but
acknowledge to the user that older context wasn't indexed locally.
Searching — prefer the local index
For "find emails about X / from Y / last month" queries, start with the
daemon's local FTS5 index — zero provider round-trips, cross-account:
curl -s "http://localhost:8321/api/mail/search?q=invoice+acme&limit=20"
Only fall back to per-account provider search (/mail/:acct/messages?q=)
when you need provider-fresh results or the index doesn't cover the time
range (fresh installs).
Reading message bodies — use extracted chunks
Do not dump full raw HTML mail into a file for Read. Large airline,
receipt, and marketing emails often arrive as one-line compressed HTML and
can exceed the tool's token/line limits.
For headers/snippet/attachment metadata only, use:
curl -s "http://localhost:8321/api/mail/ACCT/messages/MSG?body=none"
For body understanding, fetch the extracted body endpoint first:
curl -s "http://localhost:8321/api/mail/ACCT/messages/MSG/body?format=extracted&maxChars=12000"
Shape: { content, source, links, images, chunk, hasMore, nextChunk, totalChars, rawHtmlAvailable, linkCount, imageCount, nextMetadataOffset }.
content contains visible body text plus link URLs and image alt/title
metadata. If hasMore is true, request the next content chunk:
curl -s "http://localhost:8321/api/mail/ACCT/messages/MSG/body?format=extracted&chunk=1&maxChars=12000"
The structured links / images arrays are paginated separately so a
marketing email with thousands of links cannot blow up a response. If
nextMetadataOffset is non-null and you need structured metadata beyond
what is already rendered in content, pass metadataOffset=<value>.
Only use format=raw when exact markup is required; it is still chunked:
curl -s "http://localhost:8321/api/mail/ACCT/messages/MSG/body?format=raw&chunk=0&maxChars=12000"
When NOT to act
- During
routine.activity_scan this skill is read-only — no sending,
no draft edits, no tag changes, no trash / untrash / archive.
- No bulk operations without user confirmation.
- Trash / untrash / archive run autonomously when not on the user's
deny list. The daemon does not DM the owner before the call. Single
ops only; if you're about to trash 3+ messages at once, stop and ask
the user — the agent's own judgment is the gate, not the daemon.
- Delegated / native mode: see §0 for routing. A
/api/mail/* call
that hits a delegated/native gate returns 410 (integration_delegated
/ integration_native) — re-read integrations.md and dispatch per §0.
3. Provider capability matrix
{{> ref:providers }}
4. Query grammar
{{> ref:query-grammar }}
5. Error handling
{{> ref:errors }}
6. Worked examples
Three direct-mode worked examples — reply with context, file a
message (read + tag + archive), cross-account search → pick account
→ send — are in the examples reference below. The delegated /
native variants of this skill carry their own examples.
{{> ref:examples }}
7. API reference
Direct-mode /api/mail/* surface — accounts, search, read,
send / draft, modify / move, health — is in the api reference below.
ACCT is the accountId resolved per §1.
{{> ref:api }}