{
  "schema": "leumas.docs.page/1",
  "id": "skill:leumas-studio#domains/mail",
  "slug": "skills/leumas-studio/domains/mail",
  "kind": "tools",
  "bucket": "skill",
  "title": "Mail — what you can do",
  "name": "Mail",
  "eyebrow": "what you can do",
  "chip": null,
  "summary": "<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run pnpm skills:sync. -->",
  "keywords": [
    "leumas-studio",
    "fetch remember",
    "tabs focus",
    "skill-sync",
    "skills",
    "pnpm",
    "edit",
    "hand"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run `pnpm skills:sync`. -->\n\n# Mail — what you can do\n\n`/admin/d/mail` · pillar `protocols`\n\nAn install email address that runs callables: subject names one, body supplies its props, reply returns the result.\n\n## Fetch this, never remember it\n\nThese change per install and per release. Call for them - a list written here would be a future lie.\n\n| What | Call | Why |\n|---|---|---|\n| The address, the allow list and whether secrets are encrypted at rest | `GET /api/mail/config` | `GET /api/mail/config` | Tells you the inbound domain and whether this install seals secrets. `sealing: false` means the address token is stored in the clear. |\n| The mailbox itself, with its token masked | `GET /api/mail/mailboxes` | `GET /api/mail/mailboxes` | One per install. The address is shown with the token masked to its last four; POST /api/mail/mailboxes/:id/reveal returns it whole. |\n| What a subject line is allowed to run | `GET /api/mail/bindings` | `GET /api/mail/bindings` | Each binding is one subject to one callable, plus the argMap that decides which message keys reach it. |\n| Every message handled, and every one refused | `GET /api/mail/deliveries` | `GET /api/mail/deliveries` | The door answers 200 to everything so it cannot be probed, so this log is the ONLY place a refusal is visible. `phase` names the gate it stopped at. |\n\n## How to actually do things here\n\nWritten by hand, because the order and the traps are the part no file can derive.\n\n### Let a colleague email a workflow and get the result back\n\n1. Create the address on the Address tab, or POST /api/mail/mailboxes with a localPart.\n2. Add their address to the allow list — an EMPTY allow list accepts nobody, which is the safe default and the surprising one.\n3. Add a binding whose subject is the reference they will type, bound to the workflow, with an argMap from `args.<key>` or `text` onto its declared inputs.\n4. Send them the full address including the +token: it is the shared secret, and From: alone is forgeable.\n\n**Trap:** The argMap is a WHITELIST — an input you did not map never reaches the callable, whatever the sender puts in the body. A binding on a denied action (http-request, fs-*, unpinned crud-config) is refused when you save it, not when mail arrives.\n\n### React to mail without writing a binding\n\n1. Build a rule on the `email-received` trigger; its `mailbox` prop is the address to watch.\n2. The payload carries { from, to, subject, snippet, messageId, ts }, so `{{from}}` and `{{subject}}` resolve in any action's props.\n\n**Trap:** Two delivery paths feed that trigger and they are not equally trustworthy: the mail door has a real SPF/DKIM verdict, the IMAP listener has none, so a rule that acts on `{{from}}` from a polled mailbox is acting on a claim.\n\n### Work out why mail is not arriving\n\n1. Read GET /api/mail/deliveries and look at `phase`.\n2. `sig` means the provider signature failed — the MAIL_INGRESS_SECRET variable and the Worker secret disagree.\n3. `verdict` means SPF and DKIM both failed: the sender is not who the header claims.\n4. `sender` means they are not on the allow list. `secret` means the address they used had the wrong token.\n5. No rows at all means nothing reached the API: check the MX record and the Worker.\n\n**Trap:** An unknown recipient writes NO row on purpose — an anonymous sender must not be able to fill the log — so silence can mean the local part is wrong.\n\n## Tabs\n\nEvery one is a real URL. A tab with its own section below carries a real job.\n\n| Tab | Path | Group | What it is for |\n|---|---|---|---|\n| `address` | - | The address | Creates the address, sets the allow and deny lists, and rotates the token. Warns when this install stores secrets unencrypted. |\n| `bindings` | - | The address | Maps a subject line to a callable and its message body to that callable's declared inputs. |\n| `deliveries` | - | The address | Shows what the door handled and what it refused, with the gate each refusal stopped at. |\n\n## Tabs in focus\n\n### `address` - Address\n\nCreates the address, sets the allow and deny lists, and rotates the token. Warns when this install stores secrets unencrypted.\n\n**Read with:** `GET /api/mail/config` | `GET /api/mail/mailboxes`\n\n**Write with:** `POST /api/mail/mailboxes` | `PATCH /api/mail/mailboxes/:id` | `POST /api/mail/mailboxes/:id/rotate`\n\n### `bindings` - Bindings\n\nMaps a subject line to a callable and its message body to that callable's declared inputs.\n\n**Read with:** `GET /api/mail/bindings` | `GET /api/mail/bindable`\n\n**Write with:** `POST /api/mail/bindings` | `PATCH /api/mail/bindings/:id` | `DELETE /api/mail/bindings/:id`\n\n### `deliveries` - Deliveries\n\nShows what the door handled and what it refused, with the gate each refusal stopped at.\n\n**Read with:** `GET /api/mail/deliveries`\n\n## API\n\nMounted prefixes that serve this domain: `/api/mail` · `/api/mail/inbound`\n\nThe full route table is `leumas-capabilities/reference/api-routes.md`.\n\n",
  "source": {
    "path": ".claude/skills/leumas-studio/reference/domains/mail.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 5011,
    "hash": "362ad8c2e75be131be44ceef73eb978c057e0849"
  },
  "urls": {
    "html": "/p/skills/leumas-studio/domains/mail",
    "json": "/docs/skills/leumas-studio/domains/mail.json",
    "md": "/docs/skills/leumas-studio/domains/mail.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
