PluginBench
MCP Server
Active
Apache-2.0

e2a — email for AI agents MCP Server

io.github.Mnexa-AI/mcp-server

Authenticated email gateway for AI agents — per-agent inboxes, SPF/DKIM verified, HITL approval.

What is the e2a — email for AI agents MCP server?

The e2a MCP server is an authenticated email gateway that gives AI agents real email addresses with inbound/outbound capabilities. It verifies senders via SPF/DKIM, delivers mail as structured data with HMAC-signed headers, and supports human-in-the-loop approval gates. Agents can receive, send, reply, and manage conversations through MCP tools or REST APIs.

e2a enables AI agents to send and receive authenticated emails with verified sender identity. It provides per-agent inboxes accessible via MCP tools, webhooks, WebSocket, or REST polling—no public URL required. Features include SPF/DKIM verification on inbound, optional human approval gates for outbound mail, conversation threading, and delivery over multiple channels (webhook, WebSocket, REST, MCP).

How to install e2a — email for AI agents

Copy-paste configuration for popular MCP clients.

transport: stdio
Config generated by PluginBench — verify against the source before use.
Environment / auth
  • E2A_API_KEY
    required
    secret

    e2a API key from https://e2a.dev

  • E2A_AGENT_EMAIL

    Default agent inbox (e.g. bot@your-domain.com). Optional — single-agent accounts auto-resolve.

  • E2A_BASE_URL

    Self-hosted e2a backend URL. Defaults to https://e2a.dev.

  • Authorization
    required

    Bearer token: accepts an e2a API key (e2a_…) or OAuth 2.1 access token (ate2a_…)

Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "@e2a/mcp-server"
      ],
      "env": {
        "E2A_API_KEY": "<YOUR_E2A_API_KEY>",
        "E2A_AGENT_EMAIL": "<YOUR_E2A_AGENT_EMAIL>",
        "E2A_BASE_URL": "<YOUR_E2A_BASE_URL>",
        "Authorization": "<YOUR_AUTHORIZATION>"
      }
    }
  }
}
Cursor
~/.cursor/mcp.json
{
  "mcpServers": {
    "mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "@e2a/mcp-server"
      ],
      "env": {
        "E2A_API_KEY": "<YOUR_E2A_API_KEY>",
        "E2A_AGENT_EMAIL": "<YOUR_E2A_AGENT_EMAIL>",
        "E2A_BASE_URL": "<YOUR_E2A_BASE_URL>",
        "Authorization": "<YOUR_AUTHORIZATION>"
      }
    }
  }
}
Windsurf
~/.codeium/windsurf/mcp_config.json
{
  "mcpServers": {
    "mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "@e2a/mcp-server"
      ],
      "env": {
        "E2A_API_KEY": "<YOUR_E2A_API_KEY>",
        "E2A_AGENT_EMAIL": "<YOUR_E2A_AGENT_EMAIL>",
        "E2A_BASE_URL": "<YOUR_E2A_BASE_URL>",
        "Authorization": "<YOUR_AUTHORIZATION>"
      }
    }
  }
}
VS Code
.vscode/mcp.json
{
  "servers": {
    "mcp-server": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "-y",
        "@e2a/mcp-server"
      ],
      "env": {
        "E2A_API_KEY": "<YOUR_E2A_API_KEY>",
        "E2A_AGENT_EMAIL": "<YOUR_E2A_AGENT_EMAIL>",
        "E2A_BASE_URL": "<YOUR_E2A_BASE_URL>",
        "Authorization": "<YOUR_AUTHORIZATION>"
      }
    }
  }
}
Claude Code
claude mcp add mcp-server --env E2A_API_KEY=<YOUR_E2A_API_KEY> --env E2A_AGENT_EMAIL=<YOUR_E2A_AGENT_EMAIL> --env E2A_BASE_URL=<YOUR_E2A_BASE_URL> --env Authorization=<YOUR_AUTHORIZATION> -- npx -y @e2a/mcp-server

Tools & capabilities

Tools this server exposes to the agent.

  • list_messagesList inbound messages for an agent's inbox
  • get_messageFetch a specific message with full body and attachments
  • get_attachmentRetrieve an attachment from a message
  • list_conversationsList conversation threads for an agent
  • get_conversationFetch a specific conversation thread
  • update_message_labelsAdd or remove labels on messages
  • send_messageSend an outbound email to any recipient
  • reply_to_messageReply to an inbound message, maintaining conversation threading
  • forward_messageForward a message to another recipient
  • list_pending_messagesList messages held for human-in-the-loop review
  • approve_messageApprove a held outbound message for delivery
  • reject_messageReject a held outbound message
  • create_webhookSubscribe to inbound email events via webhook

Use cases

  • Receive and process inbound emails in an AI agent workflow with verified sender authentication
  • Send authenticated emails from an agent to humans or other agents with optional approval gates
  • Build multi-agent communication systems with conversation threading and message history
  • Implement human-in-the-loop review for sensitive outbound agent emails before delivery
  • Integrate email-based workflows into agent frameworks (Claude, Cursor, Cline, LangChain) without managing SMTP infrastructure

e2a — email for AI agents MCP server FAQ

What is the e2a MCP server?

e2a is an authenticated email gateway that lets AI agents send and receive emails with SPF/DKIM verification. It exposes inbox and outbound tools via MCP, making email a first-class capability for agent frameworks.

Is e2a free?

e2a offers a hosted instance at e2a.dev (pricing not specified in README) and is open-source for self-hosting. The hosted MCP server at https://api.e2a.dev/mcp is available for use.

How do I install e2a in Cursor or Claude?

For Cursor, run `/add-plugin e2a` or search the marketplace. For Claude Code, use `claude plugin marketplace add Mnexa-AI/e2a` then `claude plugin install e2a@e2a`. For other MCP clients, point directly at https://api.e2a.dev/mcp.

What authentication does e2a require?

e2a supports OAuth 2.1 (browser-based authorization) or Bearer API keys. Agent-scoped credentials resolve automatically; account-scoped keys require passing the agent email per tool call.

Can I hold outbound emails for human approval?

Yes. Enable human-in-the-loop review on an agent via the protection config. Held messages can be approved/rejected via dashboard, API, MCP tools, or magic-link email before delivery.

Does e2a verify sender identity?

Yes. All inbound mail is verified via SPF/DKIM checks and signed with HMAC-authenticated headers (X-E2A-Auth-*). Outbound mail is relayed through e2a's SMTP, ensuring authenticated delivery.

README (reference)

Source of truth, from the repository.

<div align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="assets/e2a-wordmark-dark.svg"> <img src="assets/e2a-wordmark-light.svg" width="320" alt="e2a"> </picture>

Give your AI agents a real, authenticated email address.

Receive inbound over webhook · WebSocket · REST · MCP. Send through an HTTP API. Every sender — human or agent — identity-verified.

Tests Build image License npm @e2a/sdk PyPI e2a

Hosted (e2a.dev) · Quickstart · Concepts · API · SDKs · MCP · Deploy · FAQ

<a href="https://www.producthunt.com/products/e2a-open-source-email-api-for-agents?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-e2a-open-source-email-api-for-agents" target="_blank" rel="noopener noreferrer"><img alt="e2a – open-source email API for agents - Give your AI agents a real, authenticated email address. | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1145559&theme=light&t=1778615217650"></a>

</div>

[!IMPORTANT] e2a v1.0 reaches GA in mid-July 2026. The /v1 API and SDKs are in release-candidate shape today — any breaking changes land before GA, after which the interface is stable. Pin your SDK versions and watch Releases.

e2a is an authenticated email gateway for AI agents. It receives inbound mail, verifies the sender (SPF/DKIM), and delivers it to your agent as structured data with HMAC-signed X-E2A-Auth-* headers — over whichever channel fits your runtime. Outbound goes back out through an HTTP API, with an optional human-in-the-loop approval gate.

Four ways to plug an agent in:

  • MCP — point any MCP-aware runtime at the hosted server (https://api.e2a.dev/mcp) and your agent gets an inbox toolset (list_messages, send_message, reply_to_message, …). The fastest path for agent frameworks. → MCP server
  • SDKs — TypeScript (@e2a/sdk) and Python (e2a) clients with one-call webhook verification and a WebSocket listen() stream. → SDKs
  • Raw delivery — subscribe a webhook, open a WebSocket, or poll the REST API directly. → Delivery channels
  • CLIe2a listen bridges inbound mail to a local HTTP handler (including an OpenAI Responses auto-reply mode). → CLI

What you get on top of bare SMTP:

  • Authenticated transport — SPF/DKIM verified on inbound; HMAC-signed X-E2A-Auth-* headers on every delivery
  • No public URL required — WebSocket, REST polling, and MCP all work from a laptop or behind a firewall
  • Outbound API — agents send to other agents (SMTP relay) or humans (upstream SMTP, e.g. SES, Resend)
  • Human in the loop — opt-in approval gate that holds outbound mail until a reviewer approves via dashboard, magic-link email, the MCP tools, or the API
  • Conversation threading — a stable conversation_id that survives the email ↔ structured-data boundary

Quickstart

The fastest path is to give your AI agent an inbox directly. Install the e2a plugin — it registers the hosted MCP server and an operate-well skill, so your agent can send, receive, reply in-thread, and hold mail for review out of the box. On first tool use it runs an OAuth flow in your browser — no API key to paste.

Claude Code

claude plugin marketplace add Mnexa-AI/e2a
claude plugin install e2a@e2a

Codex

codex plugin marketplace add Mnexa-AI/e2a

Then launch codex, run /plugins, and install e2a.

Cursor — run /add-plugin e2a, or paste https://github.com/Mnexa-AI/e2a into the marketplace search in Cursor Settings.

Other MCP clients (Zed, Goose, Windsurf, Claude Desktop, raw mcp.json) — point straight at https://api.e2a.dev/mcp; ready-to-paste configs are in plugins/e2a/clients/. See plugins/e2a/README.md for the full per-client guide.

Use it

You can either use the hosted instance or self-host.

  • Hosted — sign up at e2a.dev. Includes the shared agents.e2a.dev domain for instant slug-based onboarding (no DNS setup), a dashboard, the hosted MCP server, and managed deliverability.
  • Self-host — see Self-host (Docker) and Deployment. Every feature works the same; the shared-domain slug shortcut just needs you to point a mail domain at your relay and set shared_domain in config.yaml.

How it works

   Human (Gmail/Outlook)  ·  another e2a agent
          │   ▲
  inbound │   │ outbound
    SMTP  ▼   │ upstream SMTP (to humans) / relay (to agents)
   ┌───────────────┐
   │   e2a relay   │  ← MX for your agent domain points here
   │               │
   │   inbound  ↓  │  ← verify (SPF/DKIM) · sign (X-E2A-Auth-*) · deliver
   │   outbound ↑  │  ← optional HITL hold · send
   └───────────────┘
          │   ▲
  deliver │   │ send · reply · forward (HTTP API)
          ▼   │
   ┌───────────────┐
   │   your agent  │  ← webhook / WebSocket / REST poll / MCP · SDK · CLI
   └───────────────┘

Inbound flow: SMTP → SPF/DKIM check → agent lookup → HMAC-sign auth headers → webhook / WebSocket / REST / MCP delivery.

Outbound flow: API call → optional HITL hold → SMTP relay (agent-to-agent) or upstream SMTP (agent-to-human).

Concepts

Delivery channels

Inbound mail reaches you several complementary ways — chosen per integration, not set on the agent. There is no delivery "mode" on the agent record; any agent the caller owns can be consumed over any of these:

ChannelHowPublic URL needed?
WebhooksAccount-level subscriptions (POST /v1/webhooks) — HTTPS POST per event, filterable by agent / conversation / event typeYes
WebSocketPer-agent real-time notification stream (/v1/agents/{email}/ws) + REST fetchNo
REST pollingPull messages via GET /v1/agents/{email}/messages — the default path for MCP-based agentsNo
MCP toolsThe e2a MCP server's inbox tools (list_messages, get_message, get_attachment, list_conversations, …) layered over the REST APINo

Notifications carry lightweight metadata (message id, sender, subject); you fetch the full body + attachments over REST when you want them. A disconnected WebSocket client accumulates "unread" messages; on reconnect, the server drains them as notifications.

Webhooks are an account-level resource (/v1/webhooks), chosen per integration rather than configured on the agent.

Auth headers

Every email delivered through e2a (webhook or WebSocket-fetched) carries signed headers:

HeaderDescription
X-E2A-Auth-Verifiedtrue if domain-level auth (SPF or DKIM) passed
X-E2A-Auth-SenderVerified sender email or agent domain
X-E2A-Auth-Entity-Typehuman or agent
X-E2A-Auth-Domain-CheckSPF/DKIM result string (e.g. spf=pass; dkim=none)
X-E2A-Auth-Delegationagent={id};human={id} if an active delegation binding exists
X-E2A-Auth-TimestampRFC3339 timestamp
X-E2A-Auth-Message-IdInternal e2a message ID this delivery is for
X-E2A-Auth-Body-HashHex SHA-256 of the raw message bytes
X-E2A-Auth-SignatureHMAC-SHA256 over a canonical string of the above

The signature covers:

verified \n sender \n entity_type \n domain_check \n delegation \n timestamp \n message_id \n body_hash

The MAC binds to both message_id and a SHA-256 of the raw message body. Substituting either invalidates the signature, so an attacker who captures one delivery cannot replay the auth claim on a different message or under a modified body.

Verifying the signature

Any field in the payload — including X-E2A-Auth-Verified — is just the server's claim until you authenticate the delivery: anyone who can reach your webhook URL can POST a forged body. To make a security decision, verify the delivery's envelope signature — the X-E2A-Signature header — with your webhook's signing secret, a whsec_… value returned once when you create the subscription (POST /v1/webhooks); store it then. Rotate via POST /v1/webhooks/{id}/rotate-secret (24h grace window where the old secret still verifies). The envelope signature covers the whole payload, so once it verifies, the X-E2A-Auth-* claims inside are trustworthy too.

The inner X-E2A-Auth-Signature (in the table above) is a separate mechanism, signed with the deployment's HMAC secret — not your whsec_ — so a webhook subscriber neither needs nor can verify it. It exists for same-trust-domain consumers that receive these as relayed message headers (e.g. a self-hosted deployment holding the HMAC secret). Your verification path as a subscriber is the envelope signature.

The one-call shortcut parses and verifies a delivery, returning a typed event — use it instead of trusting any field on an unverified payload:

from e2a.v1 import construct_event, E2AWebhookSignatureError

# raw request body + the X-E2A-Signature header + your whsec_… secret
try:
    event = construct_event(request_body, signature_header, webhook_secret)
except E2AWebhookSignatureError:
    abort(400)  # bad signature — reject the delivery
if event.type == "email.received":
    # metadata-only notification — fetch the full message (body + attachments)
    msg = await client.webhooks.fetch_message(event)
import { constructEvent, E2AWebhookSignatureError } from "@e2a/sdk/v1";

let event;
try {
  event = constructEvent(req.body, req.header("X-E2A-Signature")!, webhookSecret);
} catch (err) {
  if (err instanceof E2AWebhookSignatureError) return res.status(400).end(); // bad signature
  throw err;
}
if (event.type === "email.received") {
  // metadata-only notification — fetch the full message (body + attachments)
  const msg = await client.webhooks.fetchMessage(event);
}

construct_event / constructEvent checks that the HMAC matches the canonical signing string and the timestamp is within a 5-minute replay window. Pass an array of secrets to accept either during a rotation: constructEvent(body, header, [oldSecret, newSecret]).

Messages fetched over an authenticated channel — client.messages.get(address, id) or the client.listen(...) stream — are already trusted (the bearer token authenticated the call), so no verify step is needed there.

Conversation threading

Both send and reply accept an optional opaque conversation_id (server-minted when omitted). e2a propagates it to the recipient on delivery via payload.conversation_id, surfaced in this priority order:

  1. X-E2A-Conversation-Id header — authoritative for e2a-to-e2a traffic. Only honored when the SMTP envelope MAIL FROM originates from this relay, so external senders cannot forge it.
  2. In-Reply-To / References lookup — standard RFC 5322 threading, scoped to the recipient agent's own messages. Covers humans replying from Gmail/Outlook.

First contact from a human arrives with conversation_id: null — the inbound relay assigns no thread id by design. You don't have to mint one yourself: when the agent replies with conversation_id omitted, e2a auto-generates a stable conv_… anchor that later replies thread onto, and replies within an existing thread inherit the referenced message's id. An explicit conversation_id you pass always takes precedence; a forward starts a new thread.

Human in the loop (HITL)

When an agent's protection config holds an outbound message for review, send and reply calls do not dispatch immediately. The message is stored with status pending_review and the API returns HTTP 202 Accepted. A reviewer must approve it before delivery; otherwise, after a configurable TTL, the protection config's holds.on_expiry decides the terminal: approve (the message just goes out, terminal status sent — for outbound, approving is sending) or reject (discard, review_expired_rejected). (Inbound messages can be held for review too — there, the auto-approve terminal is review_expired_approved, releasing the message to the inbox.)

Reviewers can approve or reject via:

  • Dashboard / API — the account-scoped review queue POST /v1/reviews/{id}/approve or /reject (id-addressed, no inbox email needed; lists held items across all the account's inboxes via GET /v1/reviews). This is the primary path. The agent-path POST /v1/agents/{email}/messages/{id}/approve|reject is deprecated but still works identically for back-compat.
  • MCP toolsapprove_message / reject_message (with list_pending_messages / get_pending_message to find them).
  • Magic-link email — sent automatically when a hold fires; one-click GET /v1/approve?t=… and /v1/reject?t=… URLs (requires E2A_PUBLIC_URL and outbound SMTP configured).

Enable review holds on an agent via PUT /v1/agents/{email}/protection: set the outbound gate action to review (or turn on the content scan), plus the hold TTL (holds.ttl_seconds) and its expiry behavior (holds.on_expiry = approve or reject). Posture lives entirely on the protection sub-resource.

API

All endpoints are under /v1 unless noted. Auth is Authorization: Bearer <api_key> except for /api/health, /v1/info, /api/feedback, and the HITL magic-link routes. Path parameters containing @ (agent emails) must be URL-encoded.

The surface covers domain registration + verification, agent CRUD, inbound/outbound messages, webhook subscriptions, HITL approve/reject (API key or signed magic-link token), GDPR-style export and deletion, and a WebSocket channel for real-time inbound delivery.

See docs/api.md for the full endpoint reference, or api/openapi.yaml for the machine-readable spec.

MCP server

The fastest way to give an AI-agent runtime an inbox. e2a runs a hosted Model Context Protocol server — point any MCP-aware host (Claude Desktop, Cursor, Cline, Google ADK, LangChain, OpenAI Agents SDK, …) at the Streamable HTTP endpoint:

https://api.e2a.dev/mcp

Authenticate either with OAuth 2.1 (add e2a as a connector and authorize in the browser) or a Bearer API key (Authorization: Bearer <e2a API key>). An agent-scoped credential resolves its agent server-side; account-scoped callers pass the agent email per tool call.

The toolset covers the full agent loop — inbox (list_messages, get_message, get_attachment, list_conversations, get_conversation, update_message_labels), outbound (send_message, reply_to_message, forward_message), HITL review (list_pending_messages, approve_message, reject_message), plus agent/domain/webhook management. Inbound is consumed by polling (list_messages) or a create_webhook subscription.

The server publishes to npm as @e2a/mcp-server for self-hosting. See mcp/README.md for per-framework setup and the full tool reference.

CLI

npm install -g @e2a/cli
e2a login

The CLI is a thin developer convenience — it covers only what the other surfaces don't do ergonomically. Drive agents (read/send/reply/list/labels) over the MCP tools or the SDKs; manage domains/agents/webhooks/keys/HITL in the web dashboard.

CommandDescription
e2a loginOpen a browser login and save your API key + default agent to ~/.e2a/config.json
e2a listen --agent <email>Stream inbound email for an agent over WebSocket (real-time; --json for raw, --forward <url> to bridge to a local HTTP handler)
e2a config [list|get|set]View or update the local config

When the --forward <url> endpoint path ends in /v1/responses, listen switches to OpenAI Responses API forwarding: each inbound email is formatted as a Responses payload and the model's output is sent back as an auto-reply. Add --forward-token <token> to attach a bearer token to the forwarded request:

e2a listen --forward http://localhost:18789/v1/responses --forward-token <token>

See cli/README.md for full reference.

SDKs

Python

pip install e2a            # webhook mode
pip install 'e2a[ws]'      # adds WebSocket support
from e2a.v1 import E2AClient, construct_event

client = E2AClient()                                       # reads E2A_API_KEY
event = construct_event(request_body, signature_header, webhook_secret)  # parse + HMAC-verify
if event.type == "email.received":
    # event.data is metadata only — replying needs just the recipient + message_id
    # fetch the full body with client.webhooks.fetch_message(event) if needed
    meta = event.data
    await client.messages.reply(meta["recipient"], meta["message_id"],
                                {"body": "Got it!", "conversation_id": "conv_123"})

WebSocket (no public URL needed):

from e2a.v1 import E2AClient

async with E2AClient(api_key="e2a_…") as client:
    async for notif in client.listen("bot@your-domain.com"):  # falls back to E2A_AGENT_EMAIL
        # notif is lightweight metadata — fetch the body when you want it
        email = await client.messages.get(notif.recipient, notif.message_id)
        await client.messages.reply(notif.recipient, notif.message_id, {"body": "Got it!"})

See sdks/python/README.md.

TypeScript

npm install @e2a/sdk

See sdks/typescript/README.md.

Deployment

Three audiences each configure a different surface:

AudienceWhat they configureWhere
Server operator — runs the Go backendDB, signing key, SMTP, OAuth, optional shared domainconfig.yaml + E2A_* env
CLI / SDK user — calls the API from their machineJust the deployment URL (and login)E2A_URL + e2a login
Web dashboard deployer — hosts the Next.js dashboardPublic site URL + brandingNEXT_PUBLIC_* build-time env

The Go binary runs on any container host; storage is plain Postgres 14+; outbound mail goes through standard SMTP. Most workers coordinate via SELECT … FOR UPDATE SKIP LOCKED, so multi-replica is safe — the two real horizontal-scaling caveats are in-memory WebSocket fan-out and per-process rate limits.

See docs/deployment.md for the full env-var reference, shared-domain DNS setup, and scaling/limitation notes.

Security

  • Identity — agent registration requires DNS TXT verification of domain ownership (custom domains)
  • Domain auth — SPF and DKIM checked on every inbound message
  • Header signatures — HMAC-SHA256 over canonical auth-header string; reject if timestamp older than 5 minutes
  • SSRF protection — webhook URLs must be HTTPS (in production), resolve to public IPs, use domain names (no raw IPs, no private/loopback ranges)
  • OAuth CSRF — single-use, time-limited nonce in the state parameter
  • Production mode (env: production in config.yaml) enforces the above where development mode is more permissive

Report security issues privately — see SECURITY.md for the disclosure process and what's in scope. Do not file public GitHub issues for vulnerabilities.

Data handling

Message envelopes and inbound bodies live in Postgres for 10 days by default; outbound bodies are scrubbed at terminal HITL transition; API keys are stored as hashes; attachments go in JSONB rows (no S3/GCS). Application logs include sender/recipient addresses (standard MTA practice) but never bodies, attachments, raw keys, or HMAC secrets. Users can self-export (GET /v1/account/export) and self-delete (DELETE /v1/account?confirm=DELETE) for GDPR Art. 15 / Art. 17 / CCPA.

See docs/data-handling.md for the full retention table, log fields, user-rights endpoints, and the operator-side responsibilities (backups, TLS, at-rest encryption, log redaction, compliance).

FAQ

Why not just use SendGrid / Resend / Postmark for sending and their inbound parsing for receiving?

Four things that aren't possible to bolt on without significant rework:

  1. Inbound with no public URL. Agents authenticate with their API key and consume inbound mail over a WebSocket to /v1/agents/{email}/ws, by polling the REST API, or through the MCP tools — no webhook URL, no ngrok, no port forward. Useful for agents on developer laptops, edge devices, or behind corporate firewalls. SendGrid/Resend are webhook-only by design.

  2. Conversation threading on every reply. Whether a human replies from Gmail or another e2a agent replies via the API, the inbound message arrives at the agent with a stable conversation_id already mapped to the original thread. For human senders, the relay does standard In-Reply-To / References lookup scoped to the recipient agent's own messages. For agent-to-agent where both sides are on e2a, it also trusts an X-E2A-Conversation-Id header it controls (envelope-from is its own domain), which survives clients that rewrite threading headers. SendGrid/Resend never see inbound mail — they aren't receivers — so neither path is available without you building both yourself.

  3. Slug provisioning on a shared domain. Operators set shared_domain: agents.e2a.dev and users POST {"email": "my-agent@agents.e2a.dev"} to immediately register an agent on the shared domain with no DNS configuration. Possible because e2a is the SMTP relay claiming the domain — Resend / SendGrid are providers, not platforms, and can't multi-tenant a shared address space without you running the relay yourself.

  4. Built-in review hold + auto-expiration. A per-agent protection policy (outbound gate action review, or the content scan) holds mail in pending_review state. Reviewers approve via dashboard, magic-link email, the MCP tools, or the API; a background worker auto-acts on expired holds based on the holds.on_expiry config. Magic-link tokens are HMAC-encoded — stateless, no session backend. With Resend / SendGrid you'd hold the message in your own DB, build the timer, the approval UI, and the stateless review tokens.

You can absolutely use SES / Resend / SendGrid as e2a's outbound SMTP for delivery to humans — that's what outbound_smtp in config.yaml is for. They complement e2a; they don't replace the inbound receiver, agent abstraction, or any of the layers above transport.

Why email at all? Why not webhooks, gRPC, or MCP between agents?

Email is the only protocol where every human already has an address and a working client. Webhooks / gRPC / MCP are great inside systems you control, but they don't reach Gmail or Outlook. If you want an agent that talks to humans (or to other organizations' agents) without forcing everyone to install a new client, email is the universal substrate.

e2a doesn't replace webhooks or MCP — your agent receives email through them. It bridges email's universal addressability to the structured-data world the agent code already lives in.

What stops an attacker from spoofing the X-E2A-Auth-* headers?

The relay never trusts inbound X-E2A-Auth-* headers — it derives the auth claim from scratch and signs it with HMAC-SHA256 against signing.hmac_secret, so any values a sender injects are ignored (read the signed auth_headers field, not raw message headers). The signed canonical binds Sender + Verified + Body-Hash + Message-Id together — replay attempts, body swaps, and sender-only forgery all fail validation. Each delivery is bound to that specific message body, not just the sender claim, so a captured (headers, signature) tuple can't be lifted onto a different message.

For a webhook subscriber, though, the protection you actually rely on is the delivery's envelope signature (X-E2A-Signature, verified with your whsec_): a forged POST to your URL fails envelope verification regardless of what X-E2A-Auth-* values it carries. The inner re-signing above is for the relayed-header trust model — consumers that hold the deployment HMAC secret and receive X-E2A-Auth-* as message headers — which a /v1/webhooks subscriber is not.

Receivers verify with the SDK — construct_event(body, header, secret) / constructEvent(body, header, secret) does parse + HMAC verify in one call (or verify_webhook_signature(...) / verifyWebhookSignature(...) if you only need the boolean check). No API call back to e2a needed. If a signing secret leaks, rotate it via the dashboard; the previous secret keeps verifying through a 24h grace window, then stops. If it's stolen from the relay, the attacker has bigger access than headers anyway.

Isn't this just SMTP with extra steps?

Yes — and the extra steps are the point. Concretely:

  • SPF/DKIM verdict normalization so receivers don't reimplement domain auth
  • HMAC-signed delivery contract binding sender, body hash, message ID, and verification status
  • WebSocket / REST / MCP transport for agents without public URLs
  • HITL approval flow with auto-expiration and stateless magic-link review
  • Conversation-Id threading that survives the email ↔ structured-data boundary
  • Slug-based agent provisioning on a shared domain
  • Per-agent webhook routing, rate limits, and HITL config

Building those on top of bare Postfix is a real project. e2a is that project, open source.

How does this compare to running Postfix or Postal myself?

If you want a full MTA, run an MTA — Postfix and Postal are great. e2a isn't trying to replace them at the SMTP transport level (it uses go-smtp for receiving and dial-out for sending). The value is the layer above transport: the auth model, agent abstraction, signed delivery contract, retry policy for webhook failures, HITL approval flow, SDKs and CLI. If you're comfortable operating an MTA and only need email plumbing, e2a may be more than you want. If you want the agent abstraction and signed identity layer prebuilt, that's what this is.

Why open source if there's a hosted version?

Two reasons:

  1. Auditability. Identity infrastructure for your agents should be readable code, not a vendor black box. You can verify the cosign signature on ghcr.io/mnexa-ai/e2a, reproduce the build, and confirm what's actually running.
  2. Self-host as a real option. The hosted instance at e2a.dev runs the same ghcr.io/mnexa-ai/e2a image you can pull right now. Convenience features on the hosted side (the shared agents.e2a.dev domain, managed deliverability) are config + DNS, not closed-source extras.

The hosted version at e2a.dev has paid tiers (a free tier plus paid plans); billing is opt-in on the hosted side — config (settable via env) points the OSS server at an external limits/billing sidecar, and the OSS code path stays unchanged. Self-hosting runs on generous default limits with no billing.

Development

make build               # go build -o bin/e2a ./cmd/e2a
make run                 # build + run (cp config.example.yaml config.yaml first)
make test                # all Go tests (needs Postgres on :5433)
make test-unit           # Go unit tests only (no DB)
make test-integration    # integration tests (needs Postgres)
make test-e2e            # e2e tests (needs Postgres)
make docker-up           # start local Postgres + Mailpit via docker compose
make migrate             # apply SQL migrations to local DB

See CLAUDE.md for the full developer guide (architecture, tests, code generation, conventions).

Self-host (Docker)

Requires Docker.

git clone https://github.com/Mnexa-AI/e2a.git
cd e2a
docker compose up -d

Postgres comes up first (migrations run automatically), then the API server, then the dashboard. Three host ports:

  • :8080 — HTTP API
  • :2525 — SMTP relay
  • :3000 — Dashboard (Caddy + Next.js, proxies /api/* to the API server)

Health check:

curl http://localhost:8080/api/health
# {"status":"ok"}

Open http://localhost:3000 in a browser to view the dashboard. Sign-in requires Google OAuth credentials configured in config.yaml; for an API-only smoke test you can skip the dashboard and use the bootstrap flow below.

Create your first user and API key (no OAuth required):

docker compose exec e2a e2a -config /etc/e2a/config.yaml -bootstrap-email you@example.com
# User:    you@example.com (id=...)
# API key: e2a_...

Save the key — it's only shown once. Register an agent and confirm it works:

KEY=e2a_...
curl -X POST http://localhost:8080/v1/agents \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"email":"my-bot@agents.e2a.dev"}'   # an email on the deployment shared domain (or a domain you've verified)

curl -H "Authorization: Bearer $KEY" http://localhost:8080/v1/agents

To receive real inbound mail, point a domain's MX record at your relay host:

  • A: your-domain.com → server IP
  • MX: your-domain.comyour-domain.com (priority 10)

Then register and verify the domain through the API (see Domains). Without DNS, the API still works for testing — but external email won't reach your relay.

Upgrades and migrations. The e2a binary embeds migrations/*.sql and auto-applies any pending ones at startup (tracked in a schema_migrations table). When you upgrade e2a, restarting the container applies new schema migrations automatically — no manual step. E2A_MIGRATION_MODE controls this: auto (default, applies pending), verify (refuse startup and report pending), or skip (emergency surgery). Migrations are idempotent and non-destructive, so re-applying is safe.

(The compose file also mounts migrations/ into Postgres' init directory, but that path only runs on first start with an empty data volume — the binary's startup auto-apply is what keeps an upgraded deployment current.)

Contributing

By submitting a pull request, you certify the Developer Certificate of Origin for your contribution. Sign your commits with git commit -s.

License

Apache 2.0 — see LICENSE and NOTICE.

Related MCP servers

Give your AI agent stealth web scraping with Cloudflare bypass and CSS selection, powered by Scrapling.

67k
Python
BSD-3-Clause
View repository →

Give your AI coding agent full control of a live Chrome browser for automation, debugging, and performance analysis.

45k
TypeScript
Apache-2.0
View repository →

Let AI agents manage your Puter files, websites, and serverless workers over MCP.

43k
TypeScript
AGPL-3.0
View repository →

Browser automation for AI agents via MCP, powering ByteDance's Agent TARS hybrid GUI/DOM browser control.

37k
TypeScript
Apache-2.0
View repository →

Run arbitrary shell commands from an MCP-connected AI agent.

37k
TypeScript
Apache-2.0
View repository →

Filesystem access MCP server from ByteDance's UI-TARS/Agent TARS ecosystem.

37k
TypeScript
Apache-2.0
View repository →