--- url: /atomic-mail-agentic/getting-started.md description: >- Recommended onboarding flow for Atomic Mail—MCP or AgentSkill install, register, jmap_request, and links to HTTP docs. --- # Getting Started Atomic Mail gives agents a programmable inbox over JMAP. The recommended flow is: 1. Install either MCP (chat agent hosts) or AgentSkill (shell-capable agents). 2. Run `register` once to create or recover an inbox. If a different username is requested while credentials already exist, registration fails unless you explicitly force replacement. 3. Use `jmap_request` for send/read flows. 4. Use `help` for built-in docs. If wrappers are not usable in your environment, use the direct HTTP docs: [`REST Auth`](/rest-auth) and [`Raw JMAP`](/jmap). ## Ideal agent flow 1. **Register** * Create account with PoW (`register --username `) or recover via API key. * Different username over existing credentials is blocked by default; force replacement only after backing up the credential directory. 2. **Persist credentials** * `credentials.json`, `session.jwt`, `capability.jwt` under `~/.atomicmail`. 3. **Arrange hourly inbox polling (after register)** * Native cron hosts: invoke your **AI agent** every hour to fetch mail via `list_inbox.json` (OpenClaw, Hermes, atomic-agent). * No native cron: ask the operator to set up polling on a capable host, or remind manual fetch. Do not work around with OS schedulers or cross-platform scheduling. Do not cron `atomicmail jmap_request` alone. See [`SKILL.md`](/SKILL#hourly-inbox-polling-after-register), [`MCP`](/mcp#hourly-inbox-polling-after-register), or `help` topic `cron`. 4. **Execute JMAP** * Call `jmap_request` with inline `ops` or `ops_file`. 5. **Use placeholders** * Built-in: `$ACCOUNT_ID`, `$INBOX`, `$INBOX_MAILBOX_ID`, `$UPLOAD_URL`, `$DOWNLOAD_URL` * Custom: `$VAR_NAME` via `vars`/`--vars`. ## Install for chat-based agents (MCP) Add to your MCP host configuration: ```json { "mcpServers": { "atomicmail": { "command": "npx", "args": ["-y", "@atomicmail/mcp-gh-pages"] } } } ``` Then call tools in this order: `register` -> `jmap_request` -> `help`. Continue with full docs: [`MCP in-depth`](/mcp). ## Install for shell-capable agents (AgentSkill) ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail register --username "myagent" npx --package=@atomicmail/agent-skill-gh-pages atomicmail jmap_request --ops-file list_inbox.json npx --package=@atomicmail/agent-skill-gh-pages atomicmail help ``` Continue with full docs: [`AgentSkill in-depth`](/skill-install) and [`Skill spec`](/SKILL). ## Next sections * [`MCP in-depth`](/mcp) * [`AgentSkill in-depth`](/skill-install) * [`Dify plugin guide`](/dify) * [`REST authentication`](/rest-auth) * [`Raw JMAP requests`](/jmap) --- --- url: /atomic-mail-agentic/dify.md description: >- Use Atomic Mail in Dify from marketplace install to Agent/Workflow usage, including a practical workflow pattern and polling guidance. --- # Dify Plugin Atomic Mail is available in the Dify marketplace as a tool plugin for Dify Agent and Workflow apps. ## How to install in Dify 1. Open **Plugins** in your Dify workspace. 2. Search for **Atomic Mail** in Marketplace and install it in the workspace. 3. Open the plugin settings and configure credentials. Dify plugin behavior to keep in mind (official docs): * Plugins are workspace-scoped (install once, usable in all apps in that workspace): [Dify Plugins docs](https://docs.dify.ai/en/use-dify/workspace/plugins) * Most plugins need configuration after install (API keys, endpoints, or other provider settings): [Dify Plugins docs](https://docs.dify.ai/en/use-dify/workspace/plugins) ## First-run setup (recommended order) After installing Atomic Mail, use the same operational order as MCP/AgentSkill: 1. `register` once (create/recover inbox credentials) 2. `help` (especially topic `cron` and `presets`) 3. `jmap_request` for inbox/send flows Use `help` early and often inside the plugin tools to avoid guessing JMAP details. ## Using Atomic Mail in Dify apps ### Agent app * Add Atomic Mail tools in the app's tool section. * Start with `register`, then call `jmap_request` for read/send actions. * Keep `help` available so the agent can fetch topic guidance while running. ### Workflow app 1. Add a **Tool** node and choose an Atomic Mail action. 2. If prompted, select/create plugin credentials in node settings. 3. Map workflow variables to the tool inputs (`ops`, `ops_file`, `vars`). Relevant Dify docs for tool-node behavior: * [Tool Node](https://docs.dify.ai/en/use-dify/nodes/tools) * [Tools in workspace](https://docs.dify.ai/en/use-dify/workspace/tools) ## Example workflow pattern Use this minimal pattern for mailbox triage in Dify Workflow: 1. **Start/User Input** node (optional controls such as mailbox scope) 2. **Tool node** -> Atomic Mail `jmap_request` with `ops_file: "list_inbox.json"` 3. **LLM node** -> summarize messages and extract required follow-ups 4. **If/Else** -> route urgent vs non-urgent items 5. **Tool node** (optional) -> send response via Atomic Mail preset 6. **End** node For Dify's general plugin-in-workflow style (install tool, authorize, wire nodes), see: [Workflow lesson: Enhance Workflows (Plugins)](https://docs.dify.ai/en/use-dify/tutorials/workflow-101/lesson-07) ## Hourly inbox polling after `register` After `register`, arrange hourly inbox polling for the inbox. The important rule is to run a full **agent turn** that uses `list_inbox.json`, not a raw `jmap_request` one-shot cron job without agent reasoning. If your runtime has no native agent cron/scheduler, ask the operator to set up polling on a capable host, or use manual fetch reminders. For exact prompt patterns and runtime-specific guidance, use Atomic Mail `help` topic `cron`. --- --- url: /atomic-mail-agentic/mcp.md description: >- Install and configure the @atomicmail/mcp-gh-pages stdio server, tools (register, jmap_request, help), and host-specific notes for chat-based agents. --- # @atomicmail/mcp-gh-pages Atomic Mail MCP server — a local stdio Model Context Protocol server that gives an AI agent a programmable email inbox over JMAP, with automatic Proof-of-Work auth and capability-token rotation. ## For AI agents — call `help` early and often **Use the `help` tool as your primary documentation source.** MCP hosts choose tools from short descriptions; when placeholders, JMAP `using` URNs, attachment uploads, or cron setup are unclear, **call `help` instead of guessing** from general JMAP knowledge or a stale README copy. The topics ship inside the installed package and always match the version your host is running. **Suggested calls:** `help` with no topic (overview) at the start of a mail task; `help` with topic `presets` before your first non-trivial `jmap_request`; `help` with topic `cron` immediately after a successful `register`; `help` with topic `jmap_cheatsheet` when sending mail or using blobs; `help` with topic `troubleshooting` when errors mention missing placeholders, auth, or preset shadowing. If anything disagrees with docs you read elsewhere, **trust `help` from this package**. ## Install ```json // mcp.json { "mcpServers": { "atomicmail": { "command": "npx", "args": ["-y", "@atomicmail/mcp-gh-pages"] } } } ``` Your MCP host spawns this process; see configuration below. For ClawHub, use the MCP-only channel package: ```json { "mcpServers": { "atomicmail": { "command": "npx", "args": ["-y", "@atomicmail/mcp-clawhub"] } } } ``` ## Tools exposed | Tool | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `register` | PoW signup; persists credentials. Usernames are 5–21 characters (local-part of your `@atomicmail.ai` address). Idempotent when username matches inbox. A different username is rejected unless you pass a separate `credentials_dir` or `forced: true`. Optional `credentials_dir` per call (parity with AgentSkill `--credentials-dir`). | | `jmap_request` | JMAP batch via `ops` or `ops_file`. Optional `credentials_dir` per call. Uppercase `$VAR_NAME` tokens are substituted (`$ACCOUNT_ID` / `$INBOX` / `$INBOX_MAILBOX_ID` / `$UPLOAD_URL` / `$DOWNLOAD_URL` from session; others via optional `vars` map). | | `help` | Built-in docs (`topic` optional); use `topic: "readme"` for the published package `README.md`. Use `topic: "multi_account"` for multiple inboxes on one MCP server. | ## Typical MCP workflow 1. Call `register` with a username (or rely on existing `credentials.json`). If credentials already exist for a different username, pass a **separate** `credentials_dir` to add another account, or pass `forced: true` only when you intend to replace credentials in the **same** directory (after backing it up). 2. **After register:** arrange hourly inbox polling per your runtime. Native cron hosts (OpenClaw, Hermes, atomic-agent) schedule an hourly **agent** job whose prompt fetches inbox mail via `{ "ops_file": "list_inbox.json" }`. Hosts without native cron should ask the operator to set up polling on a capable host or remind manual fetch — do **not** work around with OS schedulers or cross-platform scheduling. Do **not** cron `atomicmail jmap_request` alone. Call `help` with topic `cron` for examples. 3. `jmap_request` with `ops` or `ops_file` (optional `vars` for `$TO`, `$SUBJECT`, etc.). 4. `help` when stuck. ### Hourly inbox polling (after register) Invoke a **full agent turn** so you can reply, forward, or follow up — not a raw CLI log or headless one-shot. | Setup | Workflow | | --- | --- | | OpenClaw | `openclaw cron add` + `--announce` | | Hermes | `hermes cron create` + `--deliver` | | Atomic Bot | OpenClaw or Hermes | | atomic-agent | `atomic-agent task create --cron` | | No native cron (Claude, Pi, Cursor, …) | Ask operator to schedule on a capable host, or remind manual fetch | Workflow options and agent prompt: MCP `help` topic `cron`, [`SKILL.md`](./SKILL.md#hourly-inbox-polling-after-register), or `atomicmail help --topic cron`. ## `jmap_request` input patterns `jmap_request` accepts either: * inline `ops` — a JSON **string** whose value is either a **methodCalls array** (for example `[["Mailbox/get", {...}, "m0"]]`) or a full envelope object `{ "using": [...], "methodCalls": [...] }`, or * `ops_file` — path to a JSON file containing the same shapes as `ops`. When using `ops_file`, relative paths first resolve against the credential directory. If a file is not present there, the runtime falls back to bundled presets shipped in the npm package. ### Default `using` for a bare methodCalls array If `ops` is **only** a methodCalls array (no `using` in the JSON), the server merges the tool’s default capability list — today **`urn:ietf:params:jmap:core`** and **`urn:ietf:params:jmap:mail`** only. For **`EmailSubmission/set`**, **`Blob/upload`**, or **`Blob/get`**, either pass a full envelope that includes the right URNs in `using`, or rely on your MCP host passing an extended `using` array on the tool call (when supported). See [`JMAP using and inline ops`](/jmap-using) for the full picture. Successful responses may include a top-level **`_next`** field (suggested follow-ups); that is not part of RFC 8620 — see [`Raw JMAP requests`](/jmap) (“Successful responses and `_next`”). ## Presets and placeholders Pass **`vars`** on the **`jmap_request`** tool next to **`ops`** or **`ops_file`** (not inside the ops JSON string). Examples: `{ "ops_file": "list_inbox.json" }` `{ "ops_file": "send_mail.json", "vars": { "TO": "a@b.com", "SUBJECT": "Hi", "BODY": "..." } }` **Resolution:** relative `ops_file` paths resolve to the credential directory first, then bundled presets in the package. **Preset shadowing:** a file such as `list_inbox.json` in the credential directory replaces the bundled preset with the same name. After upgrading `@atomicmail/mcp-gh-pages`, errors about missing placeholders often mean an **older** preset copy on disk — delete or update it, or pass an absolute `ops_file` path. **Full** placeholder grammar, built-ins (`$INBOX` vs `$INBOX_MAILBOX_ID`, attachment tokens, bundled preset names): use the **`help`** tool with topic **`presets`**. ## Credential files and token lifecycle Mode `0600`: `credentials.json` (includes `apiKey`, `inboxId`, endpoints, blob URL templates), `session.jwt` (session bearer, rotated), `capability.jwt` (JMAP bearer, short TTL). MCP and the AgentSkill CLI create and rotate these automatically. For raw HTTP auth steps, see [`REST authentication flow`](/rest-auth). ## Attachments and blobs * **In-band (RFC 9404):** `Blob/upload` / `Blob/get` in the same JMAP batch as mail methods. Shapes, limits, and copy-paste JSON: [Raw JMAP requests](./jmap.md#attachments-rfc-9404-inline-blob-flow). * **Out-of-band (RFC 8620):** session **`uploadUrl`** / **`downloadUrl`**. MCP **`attachments`** uploads each local file first, then substitutes `$ATTACHMENT_N_BLOB_ID` (and related placeholders) into your ops. Use preset **`send_mail_blob_attachment.json`** with **`attachments`**. When the session advertises blob limits, **`jmap_request`** may **reject before POST** computable oversize `Blob/upload` payloads and attachment file sizes (see [RFC 9404 §3.1](https://www.rfc-editor.org/rfc/rfc9404#section-3.1)). If `maxSizeBlobSet` is `null`, no client octet cap is applied (the server may still reject the request). ## Multiple accounts / agents One MCP server can manage several isolated inboxes. Pass optional `credentials_dir` on **`register`** and **`jmap_request`** (same idea as AgentSkill `--credentials-dir`). When omitted, the default directory applies (`ATOMIC_MAIL_CREDENTIALS_DIR` or `~/.atomicmail`). ```json { "username": "alice", "credentials_dir": "~/.atomicmail/alice" } { "ops_file": "list_inbox.json", "credentials_dir": "~/.atomicmail/bob" } ``` * **Add a second account** without touching the first: use a new path on `register`, not `forced: true`. * **Replace** credentials in one directory: back it up, then `forced: true`. * **Concurrency:** do not run parallel tool calls against the same `credentials_dir` (JWT files have no locking). Full details: MCP `help` topic **`multi_account`**. ## Defaults * auth endpoint: `https://auth.atomicmail.ai` * api endpoint: `https://api.atomicmail.ai` * credentials directory: `~/.atomicmail` ## Overriding defaults ```json { "mcpServers": { "atomicmail": { "command": "npx", "args": ["-y", "@atomicmail/mcp-gh-pages"], "env": { "ATOMIC_MAIL_AUTH_URL": "https://custom-auth.example", "ATOMIC_MAIL_API_URL": "https://custom-api.example", "ATOMIC_MAIL_CREDENTIALS_DIR": "/Users/me/.atomicmail", "ATOMIC_MAIL_INBOX_DOMAIN": "mail.example.com", "ATOMIC_MAIL_SCRYPT_SALT": "hex-salt-override", "ATOMIC_MAIL_API_KEY": "existing-api-key" } } } } ``` --- --- url: /atomic-mail-agentic/skill-install.md description: >- Install and run the @atomicmail/agent-skill-gh-pages CLI (register, jmap_request, help) for shell-capable agents and automation. --- # @atomicmail/agent-skill-gh-pages Atomic Mail AgentSkill CLI for shell-capable AI agents. It exposes three commands: `register`, `jmap_request`, and `help`. **`jmap_request`** uses the same shared library as **`@atomicmail/mcp-gh-pages`**. ## For AI agents — run `atomicmail help` **Invoke `atomicmail help` before improvising JMAP or preset details.** The CLI embeds the topic docs — written for agents, version-matched to your install, and cheaper to fetch on demand than reconstructing placeholder grammar or attachment flows from memory. **When to call help:** at the start of a mail task (`atomicmail help` or `help --topic overview`); before custom batches (`help --topic presets` and `help --topic jmap_cheatsheet`); right after `register` (`help --topic cron` for hourly inbox polling after `register`); when errors mention missing placeholders, auth, or an old preset file on disk (`help --topic troubleshooting`). Prefer the installed binary over static README copies in other repos — **trust `help` from the package you are running**. ## Install / run ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail --help ``` ## Quick start ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail register \ --username "myagent" npx --package=@atomicmail/agent-skill-gh-pages atomicmail jmap_request \ --ops '[["Mailbox/get", {"accountId": "$ACCOUNT_ID"}, "m0"]]' ``` Usernames must be 5–21 characters (local-part of your `@atomicmail.ai` address). If credentials already exist for a different username, `register` fails by default. Pass `--forced` only after backing up the credential directory. ## After register: hourly inbox polling After `register`, arrange hourly inbox polling per your runtime. **Native cron hosts** (OpenClaw, Hermes, atomic-agent) schedule an **hourly AI agent** to fetch and triage mail with preset `list_inbox.json`. **Hosts without native cron** should ask the operator to set up polling on a capable host, or remind manual fetch — do **not** work around with OS schedulers or cross-platform scheduling. Do **not** cron `atomicmail jmap_request` alone. **Hermes users:** follow [Hermes Agent](#hermes-agent) — accept the skill blueprint via `/suggestions` after `register`. Options and agent prompt: [`SKILL.md`](./SKILL.md#hourly-inbox-polling-after-register) · `atomicmail help --topic cron` · MCP `help` topic `cron` ## Hermes Agent Hermes ships a bundled Atomic Mail skill with a launcher CLI and an hourly inbox blueprint. Requires [Hermes](https://hermes-agent.nousresearch.com) with the skills toolset and Node.js 20+ (for the bundled launcher). ### Install Unified in-repo tap (updated on each GitHub release): ```bash hermes skills install Atomic-Mail/atomic-mail-agentic/integrations/skill/atomicmail ``` ### Credentials On Hermes the default credential directory is **`~/.hermes/atomicmail`**, not `~/.atomicmail` (used by npm/npx AgentSkill and MCP defaults). The skill launcher sets `ATOMIC_MAIL_CREDENTIALS_DIR` to `$HOME/.hermes/atomicmail` when that variable is **not** already set. Override explicitly with `ATOMIC_MAIL_CREDENTIALS_DIR` or `atomicmail.credentials_dir` in Hermes config. | Runtime | Default credentials dir | | ------- | ----------------------- | | Hermes skill | `~/.hermes/atomicmail` | | npm/npx AgentSkill, MCP | `~/.atomicmail` | Files in each directory (mode `0600`): `credentials.json`, `session.jwt`, `capability.jwt`. ### Register Use the skill's bundled CLI — no `npx`: ```bash atomicmail register --username "myagent" ``` The launcher handles the credentials directory; omit `--credentials-dir` in the default single-inbox flow. For **multiple inboxes**, pass `--credentials-dir` with a separate directory per account on `register` and `jmap_request`. ### After register (required) 1. Run `/suggestions` in Hermes and **accept** the Atomic Mail hourly inbox blueprint. 2. The blueprint schedules a full **agent** turn (`no_agent: false`) with `list_inbox.json` and `deliver: origin`. Do **not** skip this step. 3. Do **not** cron raw `jmap_request` alone or use `--no-agent` (no LLM triage). **Manual fallback** if you skip the blueprint: ```bash hermes cron create "0 * * * *" \ "Use atomicmail jmap_request --ops-file list_inbox.json to fetch my inbox. Summarize new messages, highlight what needs a reply, and stay available — I may ask you to reply, forward, search, or dig into something important." \ --name "atomicmail-inbox" \ --deliver origin ``` See `atomicmail help --topic cron` for the full prompt and delivery options. ### Links * Hermes creating skills (blueprints): https://hermes-agent.nousresearch.com/docs/developer-guide/creating-skills * Hermes cron (manual fallback): https://hermes-agent.nousresearch.com/docs/user-guide/features/cron * Maintainer publish workflow: [CONTRIBUTING.md](https://github.com/Atomic-Mail/atomic-mail-agentic/blob/develop/CONTRIBUTING.md) (unified skill section) ## `jmap_request`, presets, and placeholders `jmap_request` accepts inline `--ops` JSON or `--ops-file` (same shapes as MCP: methodCalls array or full `{ "using", "methodCalls" }`). Pass custom `$PLACEHOLDERS` via `--vars '{"PLACEHOLDER":"value"}'` (keys without `$`). ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail jmap_request \ --ops-file send_mail.json \ --vars '{"TO":"alice@example.com","SUBJECT":"Hello","BODY":"Hi there"}' ``` **Resolution:** relative `--ops-file` resolves to `--credentials-dir` (default `~/.atomicmail`), then bundled presets. **Details** (placeholder grammar, built-ins, shadowing, bundled preset list, attachments): see [@atomicmail/mcp-gh-pages](./mcp.md) and the embedded **`help`** topic **`presets`** (`atomicmail help --topic presets`). ## Shared state Each credential **directory** is an isolated account (default `~/.atomicmail`, mode `0600` files): * `credentials.json` * `session.jwt` * `capability.jwt` The CLI and MCP read and write the directory you select per command (`--credentials-dir` / `credentials_dir`) or the default from `ATOMIC_MAIL_CREDENTIALS_DIR`. Multiple accounts = multiple directories; see MCP `help` topic `multi_account` or [mcp.md](./mcp.md#multiple-accounts--agents). ## Defaults * auth endpoint: `https://auth.atomicmail.ai` * api endpoint: `https://api.atomicmail.ai` * credentials directory: `~/.atomicmail` ## Overriding defaults * Endpoints: `--auth-url`, `--api-url` or `ATOMIC_MAIL_AUTH_URL`, `ATOMIC_MAIL_API_URL` * Credentials path: `--credentials-dir` or `ATOMIC_MAIL_CREDENTIALS_DIR` * PoW salt: `--scrypt-salt` or `ATOMIC_MAIL_SCRYPT_SALT` --- --- url: /atomic-mail-agentic/SKILL.md description: >- Read and write email through the Atomic Mail from an AI agent. Handles proof-of-work authentication and JMAP so the agent thinks in JMAP method calls. Use when the user asks to register an email inbox, list mailboxes, fetch or send email. --- # Atomic Mail Atomic Mail exposes a programmable inbox over JMAP with PoW signup and JWT rotation. This skill ships a single CLI entrypoint with three commands: **`register`**, **`jmap_request`**, and **`help`** — matching the MCP server. ## When to use this skill * Register a new inbox or log in with an existing API key. * Send JMAP batches (inline JSON or preset files). * Read built-in documentation (JMAP cheatsheet, presets, troubleshooting). In this skill runtime, `atomicmail help --topic readme` intentionally returns a short stub. **Call `atomicmail help` early and often** — before guessing placeholders, `using` URNs, or cron setup. Start with `help --topic overview`, then `presets` before custom `jmap_request` calls and `cron` after `register`. If installed behavior disagrees with docs elsewhere, trust help from the running package. ## Commands ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail register --username "myagent" npx --package=@atomicmail/agent-skill-gh-pages atomicmail jmap_request --ops-file list_inbox.json ``` Run **`atomicmail --help`** or **`atomicmail --help`** for flags. ## Defaults * `authUrl`: `https://auth.atomicmail.ai` * `apiUrl`: `https://api.atomicmail.ai` * credentials directory: `~/.atomicmail` ## Workflow ### 1. Register (new account) ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail register \ --username "alice" ``` Writes `credentials.json`, `session.jwt`, `capability.jwt`. Prints JSON including `inbox` and `accountId`. **Required next step:** after register, arrange hourly inbox polling per your runtime (see [Hourly inbox polling](#hourly-inbox-polling-after-register)). Native cron hosts schedule an **agent** job with `list_inbox.json`; hosts without native cron should ask the operator or remind manual fetch. Do not cron `atomicmail jmap_request` alone. Usernames must be 5–21 characters (local-part of your `@atomicmail.ai` address). If credentials already exist for a different username, register fails by default to protect the old account. To add another inbox without replacing the current one, pass a separate `--credentials-dir` (MCP: `credentials_dir` on `register` / `jmap_request`). Use `--forced` only when you intend to replace credentials in the **same** directory (after backing it up). ### 2. Register (existing API key, in case losing the credentials file) ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail register \ --api-key "..." ``` ### 3. JMAP request ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail jmap_request \ --ops '[["Mailbox/get", {"accountId": "$ACCOUNT_ID"}, "m0"]]' ``` `$ACCOUNT_ID`, `$INBOX`, `$INBOX_MAILBOX_ID`, `$UPLOAD_URL`, and `$DOWNLOAD_URL` resolve from the session/credentials. Other placeholders such as `$TO` or `$SUBJECT` require `--vars` with a JSON object of strings (same substitution applies to `--ops` and `--ops-file`). Preset file: ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail jmap_request \ --ops-file list_inbox.json ``` With custom placeholders: ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail jmap_request \ --ops-file send_mail.json \ --vars '{"TO":"alice@example.com","SUBJECT":"Hello","BODY":"Hi there"}' ``` Bundled presets (no local file creation required): * `send_mail.json` (`$TO`, `$SUBJECT`, `$BODY`) * `send_mail_attachment.json` (`$TO`, `$SUBJECT`, `$BODY`, `$ATTACHMENT_BASE64`, `$ATTACHMENT_TYPE`, `$ATTACHMENT_NAME`) * `send_mail_blob_attachment.json` (`$TO`, `$SUBJECT`, `$BODY`; pair with repeatable **`--attachment PATH`** for RFC 8620 upload → `$ATTACHMENT_0_BLOB_ID`, …) * `list_inbox.json` (latest 50; uses `$INBOX_MAILBOX_ID`) — **used for hourly inbox polling** * `reply.json` (`$MAIL_ID`, `$BODY`) ## Hourly inbox polling (after register) Registration only creates credentials. Inbound mail should be fetched and triaged about once per hour between interactive sessions. **Choose the path that matches your runtime — do not mix platforms.** ### If your host supports scheduled jobs OpenClaw, Hermes, atomic-agent, and similar hosts with native cron: set up an hourly **agent** job after the first successful `register`. Wire Atomic Mail MCP or AgentSkill into that agent. The scheduled turn should call `list_inbox.json` inside the agent prompt. ### If your host does **not** support scheduled jobs Claude, Pi, Cursor, and other chat agents without a cron API: **do not spend tokens trying to work around this** — no wrapper scripts, OS crontab/LaunchAgent setup, or scheduling on a different platform. After register, either **ask your operator** to set up hourly polling on a capable host (share the examples below), or **remind your operator** to fetch mail manually when needed. ### Forbidden: cross-platform scheduling Do not register in one runtime and schedule the cron job on another (e.g. Pi runs `register`, then creates an OpenClaw cron job). **Do not** cron `atomicmail jmap_request` alone (no agent). **Do not** use headless one-shot CLIs (`claude -p`, `pi -p`, `codex exec`) if you want to continue the thread. ### Scheduling examples (capable hosts or operators) | Your setup | Approach | | --- | --- | | OpenClaw | `openclaw cron add` with `--announce` | | Hermes | `hermes cron create` or `/cron` with `--deliver` | | Atomic Bot | Same as OpenClaw or Hermes | | atomic-agent | `atomic-agent task create --cron` | | No native cron (Claude, Pi, Cursor, …) | Ask operator to schedule on a capable host, or remind manual fetch | Full options, agent prompt, and operator OS-scheduling notes: `atomicmail help --topic cron` or MCP `help` topic `cron`. ### Agent prompt (all workflows) ```text Use Atomic Mail to fetch my inbox (MCP jmap_request with ops_file list_inbox.json, or atomicmail jmap_request --ops-file list_inbox.json). Summarize new messages, highlight what needs a reply, and stay available — I may ask you to reply, forward, search, or dig into something important. ``` ### Built-in cron examples **OpenClaw** — [cron docs](https://docs.openclaw.ai/automation/cron-jobs): isolated session, `--announce` for delivery. **Hermes** — [cron docs](https://hermes-agent.nousresearch.com/docs/user-guide/features/cron): `--deliver origin` (or `telegram`, `discord`, `email`, …); not `--no-agent`. **atomic-agent** — `atomic-agent task create --cron "0 * * * *" --message ""` For operator OS-scheduling patterns on terminal hosts, see `help --topic cron`. ### 4. Help ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail help npx --package=@atomicmail/agent-skill-gh-pages atomicmail help --topic jmap_cheatsheet ``` ## Security * `credentials.json` holds the API key (mode `0600`). Do not commit it. * JWT files are bearer secrets — do not log them. ## Attachments and blobs Use **`send_mail_attachment.json`** (in-band base64) or **`send_mail_blob_attachment.json`** with repeatable **`--attachment PATH`** (RFC 8620 upload — same flow as MCP **`attachments`**). Rules, limits, and `Blob/upload` JSON shape: **`atomicmail help --topic jmap_cheatsheet`**. ```bash npx --package=@atomicmail/agent-skill-gh-pages atomicmail jmap_request \ --ops-file send_mail_attachment.json \ --vars '{"TO":"you@example.com","SUBJECT":"Hi","BODY":"See file","ATTACHMENT_BASE64":"SGVsbG8=","ATTACHMENT_TYPE":"text/plain","ATTACHMENT_NAME":"note.txt"}' ``` ## Overriding defaults * Endpoints: `--auth-url`, `--api-url` or `ATOMIC_MAIL_AUTH_URL`, `ATOMIC_MAIL_API_URL` * Credentials path: `--credentials-dir` or `ATOMIC_MAIL_CREDENTIALS_DIR` * PoW salt: `--scrypt-salt` or `ATOMIC_MAIL_SCRYPT_SALT` --- --- url: /atomic-mail-agentic/langchain.md description: >- Use @atomicmail/langchain to run Atomic Mail register, jmap_request, and help as LangChain tools. --- # @atomicmail/langchain `@atomicmail/langchain` exposes Atomic Mail as LangChain tools while reusing the same shared runtime used by MCP and AgentSkill. It provides both: * a ready-to-use tools array (`createAtomicMailTools`) * a toolkit class (`AtomicMailToolkit`) ## Install ```bash npm install @atomicmail/langchain ``` ## Tool surfaces ```ts import { createAtomicMailTools, AtomicMailToolkit } from "@atomicmail/langchain"; const tools = await createAtomicMailTools(); const toolkit = await AtomicMailToolkit.create(); const registerTool = toolkit.registerTool; const jmapTool = toolkit.jmapRequestTool; const helpTool = toolkit.helpTool; ``` ## Available tools | Tool | Purpose | | --- | --- | | `register` | PoW signup / idempotent register with optional `forced` and `credentials_dir`. | | `jmap_request` | Run JMAP request from `ops` or `ops_file` with vars and optional attachments. | | `help` | Return built-in docs topics bundled with the package. | ## Behavior parity guarantees The LangChain wrapper enforces the same core behavior as MCP and AgentSkill: * register idempotency and `forced` semantics are delegated to shared `AgentSession.register` * exactly one of `ops` or `ops_file` is required for `jmap_request` * `dry_run` with attachments is rejected * user vars are validated with `^[A-Z][A-Z0-9_]*$` * post-register flow includes cron guidance (`help` topic `cron`) ## Credentials and environment Defaults match the rest of the stack: * credential directory: `ATOMIC_MAIL_CREDENTIALS_DIR` or `~/.atomicmail` * auth API: `ATOMIC_MAIL_AUTH_URL` * JMAP API: `ATOMIC_MAIL_API_URL` * PoW salt: `ATOMIC_MAIL_SCRYPT_SALT` * API key override: `ATOMIC_MAIL_API_KEY` `credentials_dir` can be passed per tool call for multi-account use. ## Example ```ts import { createAtomicMailTools } from "@atomicmail/langchain"; const [register, jmapRequest, help] = await createAtomicMailTools(); await register.invoke({ username: "myagent" }); const inbox = await jmapRequest.invoke({ ops_file: "list_inbox.json", }); const docs = await help.invoke({ topic: "presets" }); console.log(inbox, docs); ``` --- --- url: /atomic-mail-agentic/rest-auth.md description: >- HTTP-only signup and login—PoW challenge, session JWT, capability JWT, and token TTLs for calling JMAP without MCP or AgentSkill. --- # REST Authentication Flow Use this path when you are integrating directly over HTTP, including custom client libraries and non-wrapper runtimes. Base URLs: * Auth: `https://auth.atomicmail.ai` * API: `https://api.atomicmail.ai` ## PoW and token flow 1. `POST /api/v1/challenge` -> receive challenge JWT in `Authorization: Bearer `. 2. Solve `scrypt` PoW locally. 3. `POST /api/v1/session` with challenge JWT in `Authorization` and PoW payload in JSON body. Receive session JWT from response `Authorization: Bearer `. 4. `POST /api/v1/capability` with session bearer. Receive capability JWT from response `Authorization: Bearer `. 5. Use capability JWT for JMAP requests. Token TTLs: * Session JWT: 1 hour * Capability JWT: 2 minutes ## Agent hints in auth responses Authentication endpoints are designed to be self-guiding for agents. * Auth errors include: * `error.message` (what failed) * `error.hint` (how to fix and retry) * `error.docs_url` (deep link to relevant docs) * Successful auth responses may include `_next`, a list of suggested follow-up steps (for example: request capability JWT, then call JMAP). Example error shape: ```json { "error": { "message": "Invalid or expired challenge", "hint": "Request a fresh challenge from POST /api/v1/challenge, solve PoW again, and retry.", "docs_url": "https://atomicmail.ai/llms.txt#auth-flow-reference" } } ``` Example success hint shape: ```json { "_next": [ "Acquire the capability JWT by presenting your session JWT at POST /api/v1/capability", "Refresh it every 2 minutes", "Use it as a bearer auth token for JMAP requests" ] } ``` ## Request challenge JWT ```bash curl -i -X POST https://auth.atomicmail.ai/api/v1/challenge ``` Read challenge JWT from response header: ```http Authorization: Bearer ``` ## Create session JWT ```bash curl -X POST https://auth.atomicmail.ai/api/v1/session \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"powHex":"","nonce":"","username":"myagent"}' ``` Read session JWT from response header: ```http Authorization: Bearer ``` For login with an existing API key, send: ```json {"powHex":"","nonce":"","apiKey":""} ``` ## Create capability JWT ```bash curl -X POST https://auth.atomicmail.ai/api/v1/capability \ -H "Authorization: Bearer " ``` Read capability JWT from response header: ```http Authorization: Bearer ``` Continue with [`Raw JMAP requests`](/jmap) to execute mail method calls after capability token issuance. --- --- url: /atomic-mail-agentic/jmap.md description: >- Call Atomic Mail JMAP after auth—session discovery, POST to session apiUrl batches, and agent-oriented error hints on auth failures. --- # Raw JMAP Requests > **Using MCP or the AgentSkill CLI?** Start with [Getting started](/getting-started), then use the built-in **`help`** command (or MCP **`help`** tool) for presets and copy-paste JMAP recipes. This page is aimed at **direct HTTP JMAP** once you hold a capability bearer token. After obtaining `capabilityJwt`, run JMAP directly: * Session discovery: `GET /.well-known/jmap` (on your API host, e.g. `https://api.atomicmail.ai/.well-known/jmap`) * Method calls: `POST` to the **`apiUrl`** string from that session JSON (RFC 8620\); do not assume a fixed path such as `/jmap` unless your session says so. * Envelope **`using`** vs a bare `methodCalls` array (MCP/CLI defaults): see [JMAP `using` and inline ops](/jmap-using). ## Successful responses and `_next` When you call JMAP through **Atomic Mail MCP** or **AgentSkill**, a successful JSON body may include a top-level **`_next`** array of short suggested follow-ups (the same “self-documenting” idea as REST responses in [`REST authentication flow`](/rest-auth)). That field is **not** part of RFC 8620’s JMAP response model. If you pipe the body into a strict JMAP-only tool, ignore unknown top-level keys or strip `_next` before parsing `methodResponses`. ## Agent hints on authorization failures For authorization/authentication failures (for example expired or invalid bearer token), JMAP responses may include agent-oriented hints: * `error.message` * `error.hint` * `error.docs_url` This hint behavior applies to authorization errors only. Standard JMAP method errors (business/data validation errors inside `methodResponses`) should be handled as regular JMAP errors and are not guaranteed to carry agent hint fields. ## Discover accountId ```bash curl https://api.atomicmail.ai/.well-known/jmap \ -H "Authorization: Bearer " ``` Use `primaryAccounts["urn:ietf:params:jmap:mail"]` as your `accountId`. Session also provides RFC 8620 blob templates: * `uploadUrl` (contains `{accountId}`) * `downloadUrl` (contains `{accountId}`, `{blobId}`, `{name}`, `{type}`) ## Send email (JMAP batch) Minimal **RFC 8621–credible** flow: draft in at least one mailbox, then submit. Resolve `` with `Mailbox/query` and `filter: { "role": "inbox" }` (see [Read inbox](#read-inbox-query-get)). You may omit `envelope` on `EmailSubmission/set` create; RFC 8621 allows the server to derive it from the Email’s From/Sender and To/Cc/Bcc. Supplying `envelope` explicitly (as below) matches common agent and MTA expectations. ```json { "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission" ], "methodCalls": [ [ "Email/set", { "accountId": "", "create": { "d1": { "mailboxIds": { "": true }, "from": [{ "email": "" }], "to": [{ "email": "" }], "subject": "Hi", "textBody": [{ "partId": "b", "type": "text/plain" }], "bodyValues": { "b": { "value": "Hello." } }, "keywords": { "$draft": true } } } }, "c0" ], [ "EmailSubmission/set", { "accountId": "", "create": { "s1": { "emailId": "#d1", "envelope": { "mailFrom": { "email": "" }, "rcptTo": [{ "email": "" }] } } } }, "c1" ] ] } ``` ## If submission fails: identities (Cyrus JMAP) Atomic Mail’s mail store uses **Cyrus IMAP’s JMAP**. Many flows omit **`identityId`** on `EmailSubmission/set` when the server can infer the identity from the draft’s `from` and/or `envelope`. If you have multiple identities, wildcards, or you see `invalidProperties` / identity-related errors, set **`identityId`** explicitly (`Identity/get`, pick the `id` whose `email` matches the address you send as). See [RFC 8621](https://www.rfc-editor.org/rfc/rfc8621) for submission semantics. ## Read inbox (query + get) `inMailbox` must be the JMAP **mailbox id**. Resolve it once with `Mailbox/query` and `filter: { "role": "inbox" }`, or use the same id the agent substitutes as `$INBOX_MAILBOX_ID`. ```json { "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [ [ "Email/query", { "accountId": "", "filter": { "inMailbox": "" }, "limit": 20 }, "q0" ], [ "Email/get", { "accountId": "", "#ids": { "resultOf": "q0", "name": "Email/query", "path": "/ids" } }, "g0" ] ] } ``` For direct HTTP clients, keep request bodies as standard JSON payloads and send them unchanged to the session **`apiUrl`** with a capability bearer token. ## Attachments: RFC 9404 inline blob flow Use `Blob/upload` and `Blob/get` on the session **`apiUrl`** with `urn:ietf:params:jmap:blob` in `using`. Each `Blob/upload` `create` value is an **UploadObject**: **`data`** is a JSON **array** of **DataSourceObject** entries; each entry uses **exactly one** of `data:asText`, `data:asBase64`, or `blobId` (+ optional range). Optional **`type`** is a media-type hint. Invalid shapes include `data` as a plain string, or `data:asBase64` on the upload object instead of **inside** an array element. Attach in `Email/set` with `attachments[]` and **`blobId`** (for example `"#b1"` for create key `b1`) plus **`type`** / **`name`**. **Further reading:** [RFC 9404 §4.1](https://www.rfc-editor.org/rfc/rfc9404#section-4.1). Bundled **`send_mail_attachment.json`** uses `"data": [{ "data:asBase64": "…" }]` plus **`type`**, as in the example below. ```json { "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission", "urn:ietf:params:jmap:blob" ], "methodCalls": [ [ "Blob/upload", { "accountId": "", "create": { "b1": { "data": [{ "data:asBase64": "SGVsbG8gYXR0YWNobWVudA==" }], "type": "text/plain" } } }, "b0" ], [ "Email/set", { "accountId": "", "create": { "m1": { "mailboxIds": { "": true }, "from": [{ "email": "" }], "to": [{ "email": "" }], "subject": "Inline blob", "bodyValues": { "body1": { "value": "See attachment." } }, "textBody": [{ "partId": "body1", "type": "text/plain" }], "attachments": [ { "blobId": "#b1", "type": "text/plain", "name": "note.txt" } ] } } }, "m0" ], [ "EmailSubmission/set", { "accountId": "", "create": { "s1": { "emailId": "#m1", "envelope": { "mailFrom": { "email": "" }, "rcptTo": [{ "email": "" }] } } } }, "s0" ] ] } ``` Blob retrieval in-band: ```json { "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:blob"], "methodCalls": [ [ "Blob/get", { "accountId": "", "ids": [""], "properties": ["data:asBase64", "size"] }, "g0" ] ] } ``` For **`properties`**, use only names allowed by [RFC 9404 §4.2](https://www.rfc-editor.org/rfc/rfc9404#section-4.2) (for example `data:asBase64`, `size`). Each result still includes `id`; do not list `id` or `type` in `properties`. ## RFC 9404 account blob limits The JMAP session includes per-account blob settings under `accounts[].accountCapabilities["urn:ietf:params:jmap:blob"]` (see [RFC 9404 §3.1](https://www.rfc-editor.org/rfc/rfc9404#section-3.1)): * **`maxSizeBlobSet`**: maximum blob size in octets the server allows you to create (including concatenated `data` sources). `null` means no advertised limit (the server may still reject oversized blobs). * **`maxDataSources`**: maximum `DataSourceObject` entries per `Blob/upload` create. * **`supportedTypeNames`**, **`supportedDigestAlgorithms`**: used for `Blob/lookup` and `Blob/get` digest properties respectively. **Atomic Mail MCP and AgentSkill** read these values from `GET /.well-known/jmap` and, when they are present, **reject before POST** any RFC 8620 attachment file or in-band `Blob/upload` whose size or `data` array length would violate `maxSizeBlobSet` or `maxDataSources`, with an error that suggests using the upload endpoint / MCP `attachments` for large binaries when appropriate. Creates that reference a **literal** (non-`#`) `blobId` slice are not size-checked on the client because the referenced blob’s length is unknown without a round trip. ## Blob/lookup (RFC 9404) `Blob/lookup` reverse-maps blob ids to ids of other types (for example `Email`, `Mailbox`, `Thread`) that reference those blobs. It requires `urn:ietf:params:jmap:blob` in `using`, plus `accountId`, `typeNames`, and `ids`. Unknown types or missing capabilities for a requested type yield the `unknownDataType` error (see [RFC 9404 §4.3](https://www.rfc-editor.org/rfc/rfc9404#section-4.3)). ## Attachments: RFC 8620 upload/download endpoints For out-of-band blob transfer: 1. Resolve `uploadUrl` / `downloadUrl` from session. 2. Expand URI-template variables (`accountId`, and for download: `blobId`, `name`, `type`). 3. Use capability bearer auth for upload/download HTTP requests. 4. Use returned `blobId` in normal JMAP mail methods (`Email/set`, etc.). This path is useful when a client/tool needs direct binary transport outside JMAP method calls. --- --- url: /atomic-mail-agentic/jmap-using.md description: >- How the JMAP envelope `using` array interacts with MCP/CLI defaults and bare methodCalls arrays (RFC 8620). --- # JMAP `using` and inline ops RFC 8620 requires each JMAP request to include a **`using`** array: the set of capability URNs that apply to **all** method calls in that batch. If a method belongs to a URN you did not declare, the request is not valid for a standards-following server. ## Full envelope vs bare `methodCalls` Clients may send either: 1. **Full envelope:** `{ "using": ["urn:ietf:params:jmap:core", ...], "methodCalls": [...] }` 2. **Bare array:** `[["Email/query", {...}, "q0"], ...]` — the host then supplies a default `using` for the envelope it builds. Atomic Mail **MCP** (`jmap_request`) and **AgentSkill / CLI** use the same default `using` when you pass only a bare `methodCalls` array: * `urn:ietf:params:jmap:core` * `urn:ietf:params:jmap:mail` That pair covers **Mailbox/***, **Email/***, **Thread/***, **SearchSnippet/***, and other types declared under the mail capability. For built-in recipes and when to add more URNs, use **`help --topic jmap_cheatsheet`** (CLI) or the MCP `help` tool with topic **`jmap_cheatsheet`**. ## Pitfall: submission, identity, and blob methods If you pass a **bare `methodCalls` array** (no envelope) and rely on the default `using`, you **must** extend `using` whenever the batch includes methods that need other URNs, for example: | Methods (examples) | Add to `using` | | -------------------- | -------------- | | `EmailSubmission/*`, `Identity/*` | `urn:ietf:params:jmap:submission` | | `Blob/upload`, `Blob/get`, `Blob/lookup` | `urn:ietf:params:jmap:blob` | Ways to do that: * Put a full **`{ "using": [...], "methodCalls": [...] }`** object in `ops` / your JSON file, with every URN you need; or * **MCP:** set the tool’s **`using`** input array so it includes `submission` and/or `blob` in addition to core and mail when your inline ops need them; or * Use **bundled presets** (for example `send_mail.json`), which already embed the correct `using` for their method calls. For a narrative send/read example, see [`Raw JMAP requests`](/jmap). --- --- url: /atomic-mail-agentic/examples.md description: >- End-to-end HTTP examples (Python, curl, etc.) for PoW auth, tokens, and JMAP without MCP or AgentSkill wrappers. --- # REST API + JMAP Code Examples This page provides direct HTTP examples for Atomic Mail without MCP/AgentSkill wrappers. * Auth base URL: `https://auth.atomicmail.ai` * API base URL: `https://api.atomicmail.ai` * Session discovery: `GET /.well-known/jmap` * JMAP requests: `POST` to **`apiUrl`** from that JSON (RFC 8620; often under the same API host as discovery) For full protocol details, see [`REST authentication flow`](/rest-auth) and [`Raw JMAP requests`](/jmap). ## End-to-end flow 1. Request PoW challenge from auth service. 2. Solve PoW (`scrypt`, dynamic difficulty). 3. Create session JWT at `POST /api/v1/session` (signup with `username` or login with `apiKey`). 4. Exchange session JWT for short-lived capability JWT. 5. Call JMAP session endpoint, extract `accountId`. 6. Call JMAP `Email/*` methods. *** ## Python: PoW + auth + inbox read This script demonstrates challenge solving and token acquisition, then reads the latest messages from the inbox. ```python import base64 import hashlib import json import requests AUTH_BASE = "https://auth.atomicmail.ai" API_BASE = "https://api.atomicmail.ai" USERNAME = "myagent" SALT_HEX = "" def decode_challenge_jwt(challenge_jwt: str) -> tuple[str, int]: parts = challenge_jwt.split(".") if len(parts) < 2: raise RuntimeError("Malformed challenge JWT") payload_b64 = parts[1] pad_len = (4 - len(payload_b64) % 4) % 4 payload_json = base64.urlsafe_b64decode(payload_b64 + ("=" * pad_len)).decode() payload = json.loads(payload_json) return payload["jti"], int(payload["difficulty"]) def solve_pow(challenge: str, salt_hex: str, difficulty: int) -> tuple[int, str]: """ Find nonce such that scrypt(challenge:nonce) has required leading zero bits. """ # IMPORTANT: use UTF-8 bytes of the hex text, not bytes.fromhex(...). # This mirrors the auth service and TS reference client. salt = salt_hex.encode("utf-8") target_bits = "0" * difficulty nonce = 0 while True: data = f"{challenge}:{nonce}".encode() digest = hashlib.scrypt(data, salt=salt, n=16384, r=8, p=1, dklen=64) bits = bin(int.from_bytes(digest, "big"))[2:].zfill(512) if bits.startswith(target_bits): return nonce, digest.hex() nonce += 1 def parse_bearer_token(header_value: str) -> str: if not header_value: raise RuntimeError("Missing Authorization header") parts = header_value.split(" ", 1) if len(parts) != 2 or parts[0].lower() != "bearer" or not parts[1].strip(): raise RuntimeError(f"Malformed Authorization header: {header_value}") return parts[1].strip() def get_challenge(): r = requests.post(f"{AUTH_BASE}/api/v1/challenge") r.raise_for_status() return parse_bearer_token(r.headers.get("Authorization")) def register_if_needed(challenge_jwt: str, nonce: int, pow_hex: str, username: str): """ First-time flow. Save returned apiKey securely for future sessions. """ payload = { "powHex": pow_hex, "nonce": str(nonce), "username": username, } r = requests.post( f"{AUTH_BASE}/api/v1/session", headers={"Authorization": f"Bearer {challenge_jwt}"}, json=payload, ) r.raise_for_status() return r.json() def create_session(challenge_jwt: str, nonce: int, pow_hex: str, api_key: str): payload = { "powHex": pow_hex, "nonce": str(nonce), "apiKey": api_key, } r = requests.post( f"{AUTH_BASE}/api/v1/session", headers={"Authorization": f"Bearer {challenge_jwt}"}, json=payload, ) r.raise_for_status() return parse_bearer_token(r.headers.get("Authorization")) def create_capability(session_jwt: str): r = requests.post( f"{AUTH_BASE}/api/v1/capability", headers={"Authorization": f"Bearer {session_jwt}"}, ) r.raise_for_status() return parse_bearer_token(r.headers.get("Authorization")) def discover_jmap_context(capability_jwt: str): r = requests.get( f"{API_BASE}/.well-known/jmap", headers={"Authorization": f"Bearer {capability_jwt}"}, ) r.raise_for_status() session = r.json() account_id = session["primaryAccounts"]["urn:ietf:params:jmap:mail"] jmap_api_url = session["apiUrl"] return account_id, jmap_api_url def read_latest_emails(capability_jwt: str, account_id: str, jmap_api_url: str): payload = { "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [ [ "Mailbox/query", {"accountId": account_id, "filter": {"role": "inbox"}}, "mq0", ], [ "Email/query", { "accountId": account_id, "filter": { "inMailbox": { "resultOf": "mq0", "name": "Mailbox/query", "path": "/ids/0", } }, "sort": [{"property": "receivedAt", "isAscending": False}], "limit": 20, }, "q0", ], [ "Email/get", { "accountId": account_id, "#ids": {"resultOf": "q0", "name": "Email/query", "path": "/ids"}, "properties": ["id", "subject", "from", "receivedAt", "preview"], }, "g0", ], ], } r = requests.post( jmap_api_url, headers={"Authorization": f"Bearer {capability_jwt}"}, json=payload, ) r.raise_for_status() return r.json() if __name__ == "__main__": # 1) challenge + PoW challenge_jwt = get_challenge() challenge, difficulty = decode_challenge_jwt(challenge_jwt) nonce, pow_hex = solve_pow(challenge, SALT_HEX, difficulty) # 2) register once, then keep apiKey secure reg = register_if_needed(challenge_jwt, nonce, pow_hex, USERNAME) api_key = reg["apiKey"] print("Inbox:", reg["inbox"]) # 3) session -> capability challenge_jwt2 = get_challenge() challenge2, difficulty2 = decode_challenge_jwt(challenge_jwt2) nonce2, pow_hex2 = solve_pow(challenge2, SALT_HEX, difficulty2) session_jwt = create_session(challenge_jwt2, nonce2, pow_hex2, api_key) capability_jwt = create_capability(session_jwt) # 4) discover accountId + JMAP POST URL, then read inbox account_id, jmap_api_url = discover_jmap_context(capability_jwt) data = read_latest_emails(capability_jwt, account_id, jmap_api_url) emails = data["methodResponses"][2][1].get("list", []) for e in emails: print("-", e.get("subject"), e.get("from")) ``` *** ## Node.js: send email with JMAP This example assumes you already have `capabilityJwt` (from the auth flow). It discovers **`apiUrl`** and **`accountId`** from `GET /.well-known/jmap` (RFC 8620\), then resolves the inbox **mailbox id** with `Mailbox/query` (same pattern as [`Raw JMAP requests`](/jmap)). ```js const API_BASE = "https://api.atomicmail.ai"; async function discoverJmapContext(capabilityJwt) { const r = await fetch(`${API_BASE}/.well-known/jmap`, { headers: { Authorization: `Bearer ${capabilityJwt}` }, }); if (!r.ok) throw new Error(await r.text()); const session = await r.json(); const accountId = session.primaryAccounts["urn:ietf:params:jmap:mail"]; const jmapPostUrl = session.apiUrl; return { accountId, jmapPostUrl }; } /** JMAP mailbox id for the account inbox (`role: "inbox"`). */ async function getInboxMailboxId(capabilityJwt, accountId, jmapPostUrl) { const payload = { using: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], methodCalls: [ [ "Mailbox/query", { accountId, filter: { role: "inbox" } }, "mq0", ], ], }; const r = await fetch(jmapPostUrl, { method: "POST", headers: { Authorization: `Bearer ${capabilityJwt}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!r.ok) throw new Error(await r.text()); const data = await r.json(); const ids = data.methodResponses?.[0]?.[1]?.ids; if (!ids?.length) throw new Error("Mailbox/query returned no inbox id"); return ids[0]; } const SENDER = "myagent@atomicmail.ai"; async function sendEmail(capabilityJwt, to, subject, bodyText) { const { accountId, jmapPostUrl } = await discoverJmapContext(capabilityJwt); const inboxMailboxId = await getInboxMailboxId( capabilityJwt, accountId, jmapPostUrl, ); const payload = { using: [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission", ], methodCalls: [ [ "Email/set", { accountId, create: { draft1: { mailboxIds: { [inboxMailboxId]: true }, from: [{ email: SENDER }], to: [{ email: to }], subject, textBody: [{ partId: "body", type: "text/plain" }], bodyValues: { body: { value: bodyText }, }, keywords: { "$draft": true }, }, }, }, "s0", ], [ "EmailSubmission/set", { accountId, create: { sub1: { emailId: "#draft1", envelope: { mailFrom: { email: SENDER }, rcptTo: [{ email: to }], }, }, }, }, "s1", ], ], }; const res = await fetch(jmapPostUrl, { method: "POST", headers: { Authorization: `Bearer ${capabilityJwt}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!res.ok) { throw new Error(`JMAP request failed: ${res.status} ${await res.text()}`); } return res.json(); } const TOKEN = ""; sendEmail(TOKEN, "user@example.com", "Hello from Atomic Mail", "This was sent via JMAP.") .then((data) => console.log(JSON.stringify(data, null, 2))) .catch((err) => console.error(err)); ``` *** ## cURL: quick auth sequence ```bash # 1) challenge curl -X POST https://auth.atomicmail.ai/api/v1/challenge # 2) session JWT for signup (first time) curl -X POST https://auth.atomicmail.ai/api/v1/session \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"powHex":"","nonce":"","username":"myagent"}' # Read session JWT from response header: # Authorization: Bearer # 3) session JWT curl -X POST https://auth.atomicmail.ai/api/v1/session \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"powHex":"","nonce":"","apiKey":""}' # Read session JWT from response header: # Authorization: Bearer # 4) capability JWT curl -X POST https://auth.atomicmail.ai/api/v1/capability \ -H "Authorization: Bearer " # Read capability JWT from response header: # Authorization: Bearer ``` Use the returned `capabilityJWT` as bearer token for JMAP requests. --- --- url: /atomic-mail-agentic/core.md --- # @atomicmail/agentic-core Shared Atomic Mail runtime for integrations — PoW auth, JMAP batch execution, presets, and help topics. Use this package when building connectors (Activepieces, custom hosts) instead of shelling out to MCP or AgentSkill. ## Install ```bash npm install @atomicmail/agentic-core ``` ## Quick start ```typescript import { createAgentSessionFromKeyValue, runJmapRequest, getHelp, } from "@atomicmail/agentic-core"; const session = await createAgentSessionFromKeyValue({ storage: myHostKeyValueStore, accountId: "default", apiKey: process.env.ATOMIC_MAIL_API_KEY, }); const result = await session.register("myagent01"); // result.apiKey — present on first signup const jmap = await runJmapRequest({ session, opsJson: await Deno.readTextFile("presets/list_inbox.json"), defaultUsing: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], sourceLabel: "list_inbox.json", }); const help = await getHelp("presets"); ``` ## Key exports * `AgentSession` — register, JWT refresh, JMAP session cache * `runJmapRequest` — preset/ops execution with `$VAR` substitution and attachments * `getHelp`, `HELP_TOPIC_LIST` — runtime help topics * `KeyValueCredentialStore` — persist credentials in host storage (Activepieces Store, etc.) * `createAgentSession`, `createAgentSessionFromKeyValue` — integration session factory Bundled assets: `shared/` presets and help topics, `presets/` JMAP JSON files. ## PoW / timeout guidance for integration hosts * Run PoW in **flow actions** (register, jmap\_request), not in connection `validate()`. * Cache session JWTs in host storage; PoW re-runs only when JWT expires (~1h). * Do not use sync webhooks for register or other PoW-heavy steps. See the Atomic Mail Agentic repo for full integration guidelines. --- --- url: /atomic-mail-agentic/n8n.md --- # Atomic Mail on n8n Install the community node `@atomicmail/n8n-nodes-atomicmail` to give n8n workflows a real `@atomicmail.ai` inbox via JMAP. ## Install ### From npm (after publish) In n8n **Settings → Community nodes**, install: ```text @atomicmail/n8n-nodes-atomicmail ``` ### From this monorepo ```bash npm run build:n8n cd integrations/n8n/atomicmail npm install npm run build ``` Copy or link the package into your n8n custom extensions path, or run `npm run dev` for local development. ## Credentials {#credentials} The **Atomic Mail API** credential is optional: * **API Key** — paste an existing Atomic Mail API key, or leave empty and use **Register**. * **Auth URL** — default `https://auth.atomicmail.ai` * **API URL** — default `https://api.atomicmail.ai` The credential **Test** step validates URL shape only. It does **not** run proof-of-work (PoW). To verify an API key end-to-end, run **List Inbox** or activate the polling trigger. ### Register vs credential key You can authenticate in either way (both are supported): 1. Run the **Register** action once per workflow/account namespace. Credentials are stored in n8n **workflow-global** static data (shared across all Atomic Mail nodes in the workflow). 2. Connect an **Atomic Mail API** credential with your API key. The key is checked before stored-credentials guards — you will not be blocked when a connection API key is present. Use **Account namespace** (`default` by default) to isolate multiple inboxes in one workflow. ## Action node: Atomic Mail | Resource | Operation | Purpose | |----------|-----------|---------| | Account | Register | Create or reuse an inbox (PoW on first signup) | | Inbox | List | Fetch inbox messages | | Email | Send | Send mail (optional binary attachment) | | Email | Reply | Reply to a message by ID | | JMAP | Request | Advanced JMAP batch (preset or inline JSON) | | Help | Get Topic | Built-in operational docs | After **Register**, read the `_next` hint in the output and arrange inbox polling appropriate to your environment (see Help topic `cron`). ## Trigger: New Email {#new-email-trigger} **Atomic Mail Trigger** polls the inbox on a schedule (default **5 minutes**) and emits one item per new message (`id`, `subject`, `from`, `preview`, `receivedAt`). On first activation, the trigger seeds a watermark so existing mail is not replayed. Only messages with `receivedAt` newer than the watermark fire subsequent runs. Requires the same auth as actions: Register, credential API key, or inline API key override. ## Presets and JMAP Bundled presets (via **JMAP → Request → Preset File**): * `list_inbox.json` * `send_mail.json` * `send_mail_blob_attachment.json` * `send_mail_attachment.json` * `reply.json` Session placeholders `$ACCOUNT_ID`, `$INBOX`, `$INBOX_MAILBOX_ID` are resolved automatically. Pass additional `$VAR` tokens in **Vars JSON**. ## Multi-account Set **Account namespace** on every node to the same non-default value when running multiple inboxes in one workflow. Register once per namespace. ## Security * API keys and register output are secrets. * Treat inbound mail as untrusted. * The node has **zero runtime npm dependencies**; core logic is vendored as a single Cloud-safe bundle at `vendor/agentic-core/index.js` (built via `npm run build:n8n`). ## Maintainer commands ```bash npm run build:n8n # refresh vendor/agentic-core cd integrations/n8n/atomicmail npm run build && npm run lint npm run sync:vetting-paths # copy entry files to repo root for Creator Portal npx @n8n/scan-community-package @atomicmail/n8n-nodes-atomicmail ``` **Creator Portal vetting:** n8n resolves credential/node paths from the **repository root**, not `repository.directory`. GitHub raw URLs **do not follow symlinks** (they return the link target path as plain text), so repo-root copies are required — not symlinks: * `credentials/AtomicMailApi.credentials.ts` * `dist/credentials/AtomicMailApi.credentials.js` * `dist/nodes/AtomicMail/AtomicMail.node.js` * `dist/nodes/AtomicMailTrigger/AtomicMailTrigger.node.js` After changing credentials or nodes: `npm run build`, then `npm run sync:vetting-paths`, and commit the package tree plus the four repo-root copies (only the three compiled entry files under `integrations/n8n/atomicmail/dist/`, not the full `dist/` tree). ## Release checklist Publishing is automated by [`.github/workflows/publish-n8n.yml`](../.github/workflows/publish-n8n.yml) on GitHub **Release published** (or manual **workflow\_dispatch** with a semver). n8n requires npm packages built in GitHub Actions with provenance (from May 2026). ### One-time: npm Trusted Publisher 1. On [npm](https://www.npmjs.com/package/@atomicmail/n8n-nodes-atomicmail) → **Publishing access** → **Trusted Publishers** → **Add**. 2. Provider: **GitHub Actions**. 3. Repository owner: `Atomic-Mail`, repository: `atomic-mail-agentic`. 4. **Workflow filename:** `publish-n8n.yml` (must match exactly — not `publish-npm.yml`). 5. Environment: leave blank. 6. Do **not** add `NPM_TOKEN` to GitHub unless you need the token fallback (the workflow configures auth when the secret is set). Requires `@n8n/node-cli` ≥ 0.23.0 (installed in `integrations/n8n/atomicmail`; currently via `"*"` in devDependencies). ### Per release 1. Run local verification (above). 2. Create a GitHub release with tag `vX.Y.Z` (or dispatch the workflow with version `X.Y.Z`). 3. Confirm the workflow: vendor build → `npm ci` → `npm run release` (n8n-node lint/build/publish with provenance). 4. On npm, confirm the package shows a **Provenance** badge linked to this workflow run. 5. Submit or update the community node listing per [n8n docs](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/). ## See also * [n8n integration README (monorepo)](https://github.com/Atomic-Mail/atomic-mail-agentic/blob/develop/integrations/n8n/README.md) * [Atomic Mail MCP / CLI overview](./SKILL.md)