Alcarta Join the waiting list

MAIL GATEWAY · REST + MCP

Your agent may ask.
Only you may send.

Alcarta gives an AI agent real access to mailboxes that already live somewhere else — IMAP, SMTP, JMAP — without ever handing it the credentials. Every message that leaves, and every deletion, waits for a person.

One message when the hosted version opens. No other mail, and the address is not sold or shared.

Or self-host it today →

FLOW · AGENT TO HUMAN 01/05
How a message leavesA pixel schematic. An agent calls the gateway, which holds the mailbox keys and talks to the mail server. When the agent wants to send, the gateway only records an approval, chk_9Qm4vR7x, in the waiting state. A human approves it, and only then does the gateway hand the message to SMTP.
The agent never touches the mail server. The gateway does, once a person agrees.

The approval queue is the spine.

IMAP access is a commodity. The part worth building is that every destructive or outbound action is a two-party transaction. An agent may only declare what it wants. It receives an approval id and nothing happens.

agent    → wants_to_send_draft(draft_id: "drf_8fK2Lp")
gateway  → { "approval_id": "chk_9Qm4vR7x", "state": "waiting" }

// no message was sent.

A person opens the queue, reads one sentence that the gateway wrote and the agent cannot influence, sees the message as it stands on the mail server right now, and decides. Only then does the gateway perform the real SMTP or IMAP action — exactly once, whatever number of replicas are running — and write an audit row naming both identities.

An approval that nobody answers expires on its own. A message that moved or that somebody deleted between the request and the decision makes the action stop and say so. There is no code path that skips the check: the web pages are a client of the same REST API, with the same tokens and the same scope middleware as an agent.

SCENE 01 · THE READ 02/05
Reading a mailboxA pixel schematic. The agent calls read_inbox on the gateway. The gateway, which holds the mailbox credentials behind a lock marked never leaves, opens IMAP on port 993 to the mail server and returns message headers to the agent. The credentials travel on no path in the diagram.
read_inbox opens IMAP from the gateway, not from the agent. The agent receives headers and bodies. The credential is envelope-encrypted per row, bound to its mailbox and its organisation, and is decrypted only inside the process that dials the mail server.
SCENE 02 · THE APPROVAL 03/05
A message waiting for a decisionA pixel schematic. The agent sends a message towards SMTP, but it stops at a barrier holding approval chk_9Qm4vR7x in the waiting state. A person presses approve. The barrier retracts and turns green, the message continues to the mail server, and the approval reads executed.
wants_to_send_draft returns chk_9Qm4vR7x. The bytes stop at the barrier until a person presses approve — or until the approval expires, which also ends it.

Try the gesture.

This is the real card, with the real five-second undo. The first click is the decision; the window is the take-back. Nothing here reaches a mail server.

WAITING expires in 41 min

triage-bot wants to send a message to 3 recipients from finance@acme.com.

agt_7fK2wQ · mcp · token “nightly-triage” · 14:02:11

Reason supplied by the agent — untrusted text

Quarterly invoice reminder for the three accounts that passed their due date this morning.
MAILBOX
finance@acme.com
RECIPIENTS
3
FOLDER
Drafts
SIZE
12.4 KB
APPROVAL
chk_9Qm4vR7x
DRAFT
drf_8fK2Lp
PREVIEW [12 records]
From: finance@acme.com
To: ap@northwind.example, ap@contoso.example, billing@fabrikam.example
Subject: Invoice reminder — three accounts past due

Good morning,

Three invoices passed their due date this morning. The details are below.
SCENE 03 · THE LEDGER 04/05
The audit ledgerA pixel schematic. Four audit rows, newest last: approval.requested at 14:02:11 naming the agent alone, then approval.approved, approval.executed and mail.sent, each naming both the agent and the person who decided. A chain link joins each row to the one before it.
Every committed action writes one row naming both identities — the agent that asked and the person who decided — hash-chained to the row before it. A direct action writes the same row with an empty approval id, so an organisation can ask later which changes no human authorised.

Twenty-four tools, one authorization path.

The same twenty-four tools are reachable over REST at /api/v1 and over MCP at /mcp. Each is implemented once, in the crate that owns its domain, and each carries one required scope. A tool that behaves differently on the two surfaces is a bug, and a test fails on it.

It reads

  • get_identity READ any active token
  • read_inbox READ read_inbox@<mbx>
  • monitor_inbox READ monitor_inbox@<mbx>

It declares, and stops

  • wants_to_move NEEDS APPROVAL wants_to_move@<mbx>
  • wants_to_copy NEEDS APPROVAL wants_to_copy@<mbx>
  • wants_to_expunge NEEDS APPROVAL wants_to_expunge@<mbx>
  • wants_to_send_draft NEEDS APPROVAL wants_to_send_draft@<mbx>

It acts, because a person granted the scope

  • move DIRECT move@<mbx>
  • copy DIRECT copy@<mbx>
  • trash DIRECT trash@<mbx>
  • spam DIRECT spam@<mbx>
  • archive DIRECT archive@<mbx>
  • set_flags DIRECT set_flags@<mbx>
  • start_draft DIRECT start_draft@<mbx>
  • edit_draft DIRECT edit_draft@<mbx>
  • patch_draft DIRECT patch_draft@<mbx>
  • reply_to_thread DIRECT reply_to_thread@<mbx>
  • discard_draft DIRECT discard_draft@<mbx>

The decision itself

  • approval_status DECIDES approval_status@<mbx>
  • approval_list DECIDES approval_list@<mbx>
  • monitor_approval DECIDES monitor_approval@<mbx>
  • cancel_approval DECIDES cancel_approval@<mbx>, or be the requester
  • approval_commit DECIDES approval_commit@<mbx>
  • send_draft DECIDES send_draft@<mbx> + an approved chk_

A scope is granted per mailbox, never per organisation. A subject with no grant on a mailbox is told the mailbox does not exist, rather than that it may not see it.

Run it yourself.

One binary, PostgreSQL or SQLite, and Redis when you run more than one replica. Migrations run on boot. Long-lived key material is read from mode-0600 files, never from environment values.

services:
  gateway:
    image: alcarta-core:latest
    environment:
      GW_BASE_URL: https://mail.example.com
      GW_BIND_ADDR: 0.0.0.0:8080
      GW_DATABASE_URL: postgres://gw@db/gw
      GW_REDIS_URL: redis://redis:6379
    volumes:
      - ./keys:/keys:ro          # the KEK, mode 0600
    ports: ["8080:8080"]
  • REST/api/v1
  • MCP/mcp — streamable HTTP
  • MAILIMAP · SMTP · JMAP
  • STOREPostgreSQL · SQLite
  • AUTHEd25519 JWT · PAT · OAuth 2.1

Give your agent the inbox.
Keep the send button.

One message when the hosted version opens. No other mail, and the address is not sold or shared.

Prefer to run it yourself? Read the deployment notes →