For developers

Send and receive email for AI agents.

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.

policy.tsreference integration
export const policy: MailPolicy = {
  defaultAction: "bounce",
  senders: [
    {
      match: { address: "[email protected]", requireDkim: true, requireSpf: true },
      capabilities: ["read_calendar", "propose_meeting", "confirm_meeting"],
      rateLimit: { perHour: 20, perDay: 100 },
      tokenBudget: { perThread: 20_000 },
    },
    {
      match: { address: "[email protected]", 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 },
};

What the policy enforces

Every inbound message is evaluated against this document before your agent runs. There is no "just talk to the LLM" path.

Sender allowlist + verification

Only messages from [email protected] (with DKIM and SPF passing) reach the agent. Everything else bounces.

Capability scoping

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.

Content guard

Bodies matching a known prompt-injection regex are rejected before the LLM sees them. Audit row records the reason.

Audit log

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.

Autonomy and scale, or control and auditability

The question isn't who ships more features — it's whether you're optimising for agent autonomy and scale, or for human control and auditability. Most tools here cover one half of the email problem; Mailbuttons covers both halves and adds the policy layer that agents in regulated settings specifically need.

MailbuttonsSelf-hosted (Postfix/Stalwart)Cloudflare Email WorkersResend / Postmark / SendGridAgentMail (and similar)
Inbound + outbound in one product✓ (you build it)Inbound only, rawOutbound only
DKIM/SPF verification on inboundBuilt-inYou implementYou implementN/AVaries
Per-sender capability scopingBuilt-inNot a focus
Pre-LLM content guardsBuilt-inYou implementYou implementNot a focus
Tamper-evident audit log of agent decisionsBuilt-inYou implementNot a focus
Human-granted provisioning (no agent self-signup)Built-inYou buildNot a focus — agents self-provision
EU/UK data residencyBuilt-inYour choiceVariesNot a focus
Self-serve provisioning at scaleHuman-grantedYou buildLimitedOutbound onlyBuilt for it
Time to first agentMinutesDays–weeksHoursHours (outbound only)Minutes

To be fair to the agent-email APIs: if you want agents to spin up their own inboxes at scale and move fast, tools like AgentMail are built for exactly that, with strong deliverability and frictionless self-serve onboarding. If a human needs to authorise what each agent can do, and you need an auditable record for a regulator, that's us.

How it works

1

Inbound message arrives

Stalwart receives over SMTP. JMAP normalises it. DKIM/SPF/DMARC verified.

2

Policy evaluator runs

Sender match → verification → content guards → rate limit → token budget. Audit row written.

3

Agent invoked

Webhook fires with the verified message and granted capabilities. Your code replies via the SDK.

Quick start

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 start

See also: 60-second walkthrough.

Threat model

What the policy layer actually defends against.

ThreatControl
Random sender on the internet talks to your LLMSender allowlist + DKIM/SPF requirement
Spoofed From: headerDKIM/SPF/DMARC verdicts surfaced and required
Prompt injection via message bodyServer-side regex content guards, before tokenisation
LLM cost runaway from one prolific senderPer-sender per-thread and per-day token budgets

What this doesn't do yet

Honest list. Subscribe / star the GitHub org if any of these matter to you.

  • Beta. Expect occasional rough edges.
  • The sandbox is a single-mailbox evaluation slot. Production deployments live on the Business tier and above.
  • No on-prem deployment yet (planned post-1.0).
  • No built-in calendar, CRM, or ticketing integrations — those are your job.
  • Reference integrations exist for the Claude Agent SDK in TypeScript and Python; other languages and SDKs are not yet covered.

JMAP reference

Platform architecture

Mailbuttons runs a managed, multi-tenant email API with a JMAP-compatible control plane and agent-focused extensions.

  • Inbound: Dedicated subdomains, SPF/DKIM/DMARC enforcement, bounce handling
  • Proxy: `/api/v1/jmap` terminates on Mailbuttons edge infrastructure with tenant-aware routing hints
  • Outbound: Signed mail via your verified domains or managed tenant pools
  • Agents: Optional tool execution layer for inbox-driven automations
  • Memory: Vector embeddings + timeline indexes so AI agents recall entire threads

What the API exposes

  • First-class JMAP-compatible session + method pipeline (mail, threads, identities)
  • Domain provisioning with DNS manifests and verification polling
  • Pluggable auth hints to scope a request to an individual agent inbox
  • Unified logging for inbound agent messages and outbound mail

JMAP-compatible call example

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": "[email protected]" },
  "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.json

Tip: The Rust and Python SDKs wrap this payload alongside type-safe helpers for common operations like queueing outbound mail or ingesting threads.

Domain provisioning workflow

  1. Add a domain from the dashboard or via POST /api/domains/add
  2. Apply the generated TXT records for SPF, DKIM selector, and DMARC
  3. Poll verification or trigger it manually with POST /api/domains/{domain}/verify
  4. Create mailboxes through SDK helpers or raw JMAP-compatible calls