For developers
Full-duplex JMAP under the hood. Policy enforced server-side, before the LLM sees anything. ~40 lines to wire up.
Building a prototype? Engineers can spin up a free sandbox to exercise the policy gate and JMAP API end-to-end.
export const policy: MailPolicy = {
defaultAction: "bounce",
senders: [
{
match: { address: "boss@acme.com", requireDkim: true, requireSpf: true },
capabilities: ["read_calendar", "propose_meeting", "confirm_meeting"],
rateLimit: { perHour: 20, perDay: 100 },
tokenBudget: { perThread: 20_000 },
},
{
match: { address: "notifications@calendar-bot.internal", requireDkim: true },
capabilities: ["ingest_conflict_notice"],
rateLimit: { perHour: 200 },
},
],
contentGuards: [
{ reject: /ignore (previous|all) instructions/i, reason: "prompt_injection_suspected" },
],
auditLog: { retentionDays: 90, includeBodyHash: true },
};Every inbound message is evaluated against this document before your agent runs. There is no "just talk to the LLM" path.
Only messages from boss@acme.com (with DKIM and SPF passing) reach the agent. Everything else bounces.
The boss can read the calendar and book meetings. The conflict-bot can only ingest conflict notices. The agent cannot do anything outside the listed capabilities.
Bodies matching a known prompt-injection regex are rejected before the LLM sees them. Audit row records the reason.
Every decision — accepted, rate-limited, content-guarded — is logged for 90 days, with a SHA-256 of the inbound body for proof.
Full schema in the policy reference.
Existing tools cover one half of the problem. Mailbuttons covers both halves and adds the policy layer that agents specifically need.
| Mailbuttons | Self-hosted (Postfix/Stalwart) | Cloudflare Email Workers | Resend / Postmark / SendGrid | AgentMail (and similar) | |
|---|---|---|---|---|---|
| Inbound + outbound in one product | ✓ | ✓ (you build it) | Inbound only, raw | Outbound only | ✓ |
| DKIM/SPF verification on inbound | Built-in | You implement | You implement | N/A | Varies |
| Per-sender capability scoping | Built-in | — | — | — | — |
| Pre-LLM content guards | Built-in | You implement | You implement | — | — |
| Audit log of agent decisions | Built-in | You implement | — | — | Varies |
| Time to first agent | Minutes | Days–weeks | Hours | Hours (outbound only) | Minutes |
Stalwart receives over SMTP. JMAP normalises it. DKIM/SPF/DMARC verified.
Sender match → verification → content guards → rate limit → token budget. Audit row written.
Webhook fires with the verified message and granted capabilities. Your code replies via the SDK.
Three commands to a working agent. Same code as the reference integration READMEs.
# Run the reference integration
git clone https://github.com/mailbuttons/claude-scheduling-agent-ts
cd claude-scheduling-agent-ts && npm install
MAILBUTTONS_API_KEY=sk_... ANTHROPIC_API_KEY=sk-ant_... npm startSee also: 60-second walkthrough.
What the policy layer actually defends against.
| Threat | Control |
|---|---|
| Random sender on the internet talks to your LLM | Sender allowlist + DKIM/SPF requirement |
| Spoofed From: header | DKIM/SPF/DMARC verdicts surfaced and required |
| Prompt injection via message body | Server-side regex content guards, before tokenisation |
| LLM cost runaway from one prolific sender | Per-sender per-thread and per-day token budgets |
Honest list. Subscribe / star the GitHub org if any of these matter to you.
Mailbuttons runs a managed, multi-tenant email API with a JMAP-compatible control plane and agent-focused extensions.
Authenticate with your tenant key and forward JMAP method calls exactly as you would against any standards-compliant server. Mailbuttons preserves compatibility while layering optional AI memory features through `_mailbuttons` hints.
List messages in an agent inbox:
# Save the payload once for reuse
cat <<'EOF' >/tmp/email-query.json
{
"using": [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:mail"
],
"_mailbuttons": { "account": "agent@example.com" },
"methodCalls": [
[
"Email/query",
{
"accountId": "{{accountId}}",
"filter": { "inMailbox": "{{mailboxId}}" },
"sort": [{ "property": "receivedAt", "isAscending": false }],
"limit": 25
},
"c1"
]
]
}
EOF
# Fetch the tenant-scoped JMAP session
curl -s https://emailapi.mailbuttons.com/api/v1/jmap/session \
-H "Authorization: Bearer <TENANT_API_KEY>"
# Issue a JMAP request (email query in this example)
curl -s https://emailapi.mailbuttons.com/api/v1/jmap \
-H "Authorization: Bearer <TENANT_API_KEY>" \
-H "Content-Type: application/json" \
--data @/tmp/email-query.jsonTip: The Rust and Python SDKs wrap this payload alongside type-safe helpers for common operations like queueing outbound mail or ingesting threads.
POST /api/domains/addPOST /api/domains/{domain}/verify