IMAP/SMTP as a capability — the agent can search and read your mail, send and reply, and stand up subscriptions that wake it when matching mail arrives. Works with any provider.
What the agent can do
- Triage → fetch → act — search returns envelope summaries; fetch downloads a chosen message to staging as
.mdand.eml; send composes or replies in-thread. - Browse folders — list the mailbox's folders, filtered by the configured allowlist.
- Scope is enforced server-side — the sender allow/deny lists are pushed into the IMAP query, so out-of-scope mail never comes back.
Getting mailbox credentials
Give the agent a dedicated mail account — credentials are stored on disk, so keep it separate from your personal account. You'll need the IMAP and SMTP host and port, and a password; providers with 2FA require an app-specific password.
- IMAP
imap.gmail.com:993, SMTPsmtp.gmail.com:465. - App password at myaccount.google.com/apppasswords.
- IMAP
imap.mail.me.com:993, SMTPsmtp.mail.me.com:587. - App password at account.apple.com → Sign-In and Security.
- IMAP
outlook.office365.com:993, SMTPsmtp.office365.com:587.
- IMAP
imap.fastmail.com:993, SMTPsmtp.fastmail.com:465. - App password under Settings → Privacy & Security.
Secrets
secrets.json
{
"EMAIL_ADDRESS": "agent@example.com",
"EMAIL_PASSWORD": "app-specific-password",
"IMAP_HOST": "imap.gmail.com",
"IMAP_PORT": 993,
"SMTP_HOST": "smtp.gmail.com",
"SMTP_PORT": 465
}Configuration
Config splits into inbound (search and subscribe) and outbound (send), each with an approval mode and scope lists. The mode is a ladder: disabled doesn't register the tool, approval prompts per call, enabled runs within scope without prompting.
capabilities.json
{
"extensions": {
"email": {
"enabled": true,
"channel": "email",
"inbound": { "default": "approval", "senders": ["@acme.com"] },
"outbound": { "default": "approval", "recipients": [] }
}
}
}| Field | Type | Default | Effect |
|---|---|---|---|
| inbound.default | disabled | approval | enabled | approval | Approval policy for search and subscribe. |
| inbound.senders | string[] | [] | Sender allowlist — exact (alice@acme.com) or domain (@acme.com). Empty means any. Enforced in the IMAP query. |
| inbound.blocked | string[] | [] | Sender denylist; same syntax. |
| outbound.default | disabled | approval | enabled | approval | Approval policy for send. |
| outbound.recipients | string[] | [] | Recipient allowlist. Empty means any. |
| outbound.blocked | string[] | [] | Recipients the agent may never send to. |
Finer knobs tune scope and approval bypass: inbound.folders, inbound.window_days, inbound.max_results, the always_allow lists on each side (senders/recipients that skip the prompt), and inbound.require_auth (require DKIM/DMARC alignment on subscriptions).
Tools
email__search(from?, to?, subject?, body?, folder?)
Search emails via IMAP, returning envelope summaries (id, from, to, subject, date, snippet).
Parameters
- optional
from[string]Filter by sender address. - optional
to[string]Filter by recipient address. - optional
subject[string]Filter by subject (substring match). - optional
body[string]Filter by body content. - optional
folder[string, default INBOX]IMAP folder name.
Returns
ID: EMAIL_ID From: FROM To: TO Date: DATE Subject: SUBJECT Snippet: SNIPPET
(Showing N of TOTAL matches)
No emails found.
Search failed: ERROR
email__fetch(ids?, emailId?, folder?)
Download emails to staging as .md (parsed text with headers, attachment summary, image stats) and .eml (raw MIME).
Parameters
- optional
ids[string[]]Email IDs to fetch (batch). Use this or emailId. - optional
emailId[string]Single email ID (alternative to ids). - optional
folder[string, default INBOX]IMAP folder name.
Returns
Fetched N email(s) to /staging/in/
ID: ID | From: FROM | Subject: SUBJECT
FILENAME.md, FILENAME.eml
Use Read to access .md files. Use .eml for raw MIME / attachments.Rejected N email(s): ID: ID — REASON
Missing required field: ids or emailId
Fetch failed: ERROR
email__send(to, subject, body, replyToMessageId?)
Compose and send an email. Reply in-thread by passing replyToMessageId.
Parameters
to[string]Recipient email address.subject[string]Email subject.body[string]Plain text email body.- optional
replyToMessageId[string]RFC Message-ID to reply to (from an email__fetch result).
Returns
Email sent. Message-ID: MESSAGE_ID
Invalid arguments: ERROR
Email sending is disabled for this agent.
Send failed: ERROR
email__list_folders()
List all available IMAP mailbox folders.
Returns
FOLDER_PATH FOLDER_PATH — DISPLAY_NAME …
No folders found.
Failed to list folders: ERROR
email__subscribe(schedule, instructions, from?, subject?, folder?, id?, timezone?)
Watch for new emails matching criteria. Matches arrive on the configured channel as new turns with the agent's standing instructions.
Parameters
schedule[string]"realtime" for IMAP IDLE push, or a cron expression like "*/15 * * * *".instructions[string]Instructions delivered with each matching email.- optional
from[string]Filter by sender address. - optional
subject[string]Filter by subject. - optional
folder[string, default INBOX]IMAP folder to watch. - optional
id[string]Custom subscription ID (auto-generated if omitted). - optional
timezone[IANA timezone]Timezone for cron schedule. Defaults to agent timezone.
Returns
Subscription created: ID: ID Folder: FOLDER Schedule: SCHEDULE Target: PARTICIPANT Watermark: WATERMARK
Missing required fields: schedule, instructions
Subscriptions require a participant context.
Invalid cron expression: ERROR
email__unsubscribe(id)
Remove an email subscription.
Parameters
id[string]Subscription ID to remove.
Returns
Subscription "ID" removed.
Missing required field: id
Subscription not found: ID
email__list_subscriptions()
List all email subscriptions and their status.
Returns
ID: ID Folder: FOLDER Schedule: SCHEDULE Target: TARGET Status: active|paused Watermark: WATERMARK Created: ISO_TIMESTAMP Criteria: CRITERIA_JSON
No email subscriptions.
Subscriptions
With a dedicated channel, the agent can subscribe to matching mail — either real-time (IMAP IDLE) or on a schedule. New mail is delivered to that channel with each subscription's standing instructions, and the agent acts on it without being asked. Without a channel, the subscription tools are hidden and the on-demand tools still work.
Notes & gotchas
outbound.default at disabled and widen with a narrow outbound.recipients only when the agent needs to send; keep inbound.default at approval unless you're deliberately opening reads. Broad subscriptions widen the agent's input surface.secrets.json. Use a dedicated mailbox and an app-specific password.