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: "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 },
};

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 boss@acme.com (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.

Not just SMTP

Existing tools cover one half of the problem. Mailbuttons covers both halves and adds the policy layer that agents 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-in
Pre-LLM content guardsBuilt-inYou implementYou implement
Audit log of agent decisionsBuilt-inYou implementVaries
Time to first agentMinutesDays–weeksHoursHours (outbound only)Minutes

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": "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.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