lmmail — simple asynchronous mail for LLM agents

Use lmmail when an agent or human needs a durable, connection-free handoff. The address is a globally unique mailbox name. One mail contains:

Any authenticated lmctl appkey may send to an existing mailbox. Only the mailbox owner may list, read, or delete its mail. Listing never marks messages read; fetching one message does.

A mailbox named lmexec-replies-USER_ID-console may appear in your mailbox list. The web console's command runner creates it to receive command results; each result is deleted once it has been read, and anything left after an hour is removed. Do not use it for mail.

Fastest path: CLI

lmmail login
lmmail whoami
lmmail mailboxes
lmmail create build-owner
lmmail send build-owner --file ./handoff.md --attach ./logs.zip ./report.pdf
lmmail list build-owner --status unread --after 0
lmmail read build-owner 4 --out ./message-4
lmmail delete build-owner 4

read downloads the body and all attachments into the output directory and marks the message read. send prints the assigned sequence number.

Set LMMAIL_API_URL or pass --api URL. Authentication always uses Authorization: Bearer APP_KEY. Credential resolution is:

LMCTL_APPKEY -> LMCTL_APPKEY_FILE -> ~/.config/lmctl/appkey.json

Never pass a user id. The appkey is the identity, and the server stamps sender and owner fields from it.

Raw HTTP

Set LMMAIL_API_URL to https://lmctl.ai/lmmail before using the raw-HTTP examples (or pass --api URL to the CLI).

export LMMAIL_API_URL="https://lmctl.ai/lmmail"
export API="$LMMAIL_API_URL"
export KEY="APP_KEY"

Create and list your mailboxes:

curl -sS -X POST "$API/mailboxes" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"mailbox_name":"build-owner"}'

curl -sS "$API/mailboxes" -H "Authorization: Bearer $KEY"

Delete a mailbox and all of its messages when it is no longer needed:

curl -sS -X DELETE "$API/mailboxes/build-owner" \
  -H "Authorization: Bearer $KEY"
# -> {"deleted":true,"mailbox_name":"build-owner","messages_deleted":2}

Send is allocate, direct-upload, finalize:

RESP="$(
  curl -sS -X POST "$API/mailboxes/build-owner/messages" \
    -H "Authorization: Bearer $KEY" \
    -H "content-type: application/json" \
    -d '{
      "subject":"test report",
      "body":{"name":"report.md","content_type":"text/markdown","size":42},
      "attachments":[{"name":"logs.zip","content_type":"application/zip","size":2048}]
    }'
)"

For the body and every attachment, send every returned field to its own upload_url:

URL="$(jq -r '.body.upload_url' <<<"$RESP")"
mapfile -t FIELDS < <(
  jq -r '.body.fields | to_entries[] | "-F", "\(.key)=\(.value)"' <<<"$RESP"
)
curl -sS -X POST "$URL" "${FIELDS[@]}" -F "file=@report.md"

URL="$(jq -r '.attachments[0].upload_url' <<<"$RESP")"
mapfile -t FIELDS < <(
  jq -r '.attachments[0].fields | to_entries[] | "-F", "\(.key)=\(.value)"' <<<"$RESP"
)
curl -sS -X POST "$URL" "${FIELDS[@]}" -F "file=@logs.zip"

curl -sS -X POST "$API$(jq -r '.finalize_url' <<<"$RESP")" \
  -H "Authorization: Bearer $KEY"

List mail:

curl -sS "$API/mailboxes/build-owner/messages?status=unread&after=0" \
  -H "Authorization: Bearer $KEY"

Valid status values are all, unread, and read; default is all. after is exclusive and defaults to 0. Sequence gaps are normal.

Read metadata and obtain 120-second body/attachment download URLs:

MAIL="$(
  curl -sS "$API/mailboxes/build-owner/messages/4" \
    -H "Authorization: Bearer $KEY"
)"
curl -sS "$(jq -r '.body.download_url' <<<"$MAIL")" -o report.md
curl -sS "$(jq -r '.attachments[0].download_url' <<<"$MAIL")" -o logs.zip

The GET above marks message 4 read.

Delete:

curl -sS -X DELETE "$API/mailboxes/build-owner/messages/4" \
  -H "Authorization: Bearer $KEY"

API summary

POST   /mailboxes
GET    /mailboxes
POST   /mailboxes/{name}/messages
POST   /mailboxes/{name}/messages/{seq}/finalize
GET    /mailboxes/{name}/messages?status=...&after=...
GET    /mailboxes/{name}/messages/{seq}
DELETE /mailboxes/{name}/messages/{seq}

Bodies and attachments are each limited to 100 MiB. Mailbox names use letters, digits, ., _, and -, start with a letter or digit, and are at most 128 characters. There are no folders, threads, search, or replies in v1.

For deployed-stack verification, the repository includes cloud/test-integration/cli-aws.test.ts. It is disabled unless LMMAIL_INTEGRATION=1; it requires LMMAIL_API_URL, a fresh globally unique LMMAIL_TEST_MAILBOX, and a shared appkey from the normal resolution chain.

Verification scope

The API and CLI syntax above was checked against the tool source on 2026-10-19. The raw HTTP steps were also run end to end against https://lmctl.ai on 2026-10-20. Account permissions, limits and credentials differ per deployment; confirm them with your operator. Do not assume that access to one deployment grants access to another.