# Emailops

> Query, search, chat with, and compose/draft/send email from a local EmailOps mailbox via emailops-cli.

- **Type:** Skill
- **Install:** `agentstack add skill-gerodp-hermes-productivity-skills-emailops`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [gerodp](https://agentstack.voostack.com/s/gerodp)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [gerodp](https://github.com/gerodp)
- **Source:** https://github.com/gerodp/hermes-productivity-skills/tree/main/email/emailops

## Install

```sh
agentstack add skill-gerodp-hermes-productivity-skills-emailops
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# EmailOps CLI

`emailops-cli` is the headless front-end for **EmailOps**, a privacy-first,
AI-native email client. AI runs locally by default (embedded llama.cpp); email
data lives in a local SQLite database. This skill lets the agent read, search,
chat with, and **compose / draft / send** mail without opening the desktop app.

It complements the `himalaya` skill: Himalaya speaks raw IMAP/SMTP, while
emailops-cli adds Gmail/Outlook OAuth accounts plus **local AI** — semantic
chat over your mail, classification, and embeddings.

## The agent contract: always pass `--json`

Every command supports `--json`, which prints exactly **one stable envelope**
to stdout (logs always go to stderr). Parse this single shape regardless of
success or failure — never scrape the human-readable output:

```jsonc
{ "ok": true,  "data": { /* result */ }, "error": null }                                  // success
{ "ok": false, "data": null, "error": { "code": "not_found", "params": {}, "message": "" } } // failure
```

Add `--quiet` to suppress the stderr app-log stream when you only want the
envelope.

## Exit codes (branch on these, not on text)

| Code | Meaning            | Code | Meaning            |
|------|--------------------|------|--------------------|
| 0    | ok                 | 5    | network / sync     |
| 2    | invalid input      | 6    | AI                 |
| 3    | not found          | 130  | cancelled          |
| 4    | auth               | 1    | otherwise          |

## Prerequisites

1. `emailops-cli` on `PATH` (`emailops-cli --version` to verify). It is built
   from the EmailOps repo with the `cli` cargo feature; symlink the built
   binary into `~/.local/bin` if needed.
2. An EmailOps data directory with at least one configured account. Confirm
   with `doctor` before anything else.

## When to use

Use whenever the user asks about *their own email*: searching messages, asking
"what did X say about Y?", listing accounts, reading a specific message, or
checking mailbox health. Also use it to **write mail** — save or edit a draft,
review existing drafts, or send a message. Prefer the cheap read paths
(`doctor`, `accounts`, `emails`, `show`, `search`) over `chat` unless the
question genuinely needs the local LLM.

## Core commands

| Command | Purpose | Cost |
|---------|---------|------|
| `emailops-cli doctor --json` | Env readiness: DB, accounts, AI config. **Loads no model.** | cheap |
| `emailops-cli accounts --json` | List configured accounts (id, email, provider). | cheap |
| `emailops-cli emails --json [--limit N] [--mailbox inbox\|sent\|spam\|trash]` | List recent emails. | cheap |
| `emailops-cli show  --json` | Full single email (headers + body). | cheap |
| `emailops-cli search '' --json [--limit N] [--trace]` | Full-text search across an account's mail. | cheap |
| `emailops-cli chat '' --json [--trace]` | Ask the local AI a question against your mail. | loads LLM |
| `emailops-cli stats --json` | Per-account dashboard totals + coverage. | cheap |
| `emailops-cli compose --to … --subject … (--body …\|--body-file …) --json` | Save a draft (default) or `--send` to deliver. | cheap (write) |
| `emailops-cli drafts --json` | List saved drafts, newest-first. | cheap |
| `emailops-cli draft  --json` | Show a single draft (recipients, subject, body, attachments). | cheap |

Scope to an account with `--account ` (defaults to the single
enabled account). When multiple accounts exist, list them first and pass
`--account` explicitly.

## Procedure

1. **Health check first** (read-only, no model load):
   ```bash
   emailops-cli doctor --json
   ```
   Confirm `data.ok == true` and `data.accountsEnabled > 0`. If `aiEnabled`
   is false, avoid `chat` and stick to search/read commands.

2. **Resolve the account** when there's more than one:
   ```bash
   emailops-cli accounts --json
   ```
   Use the returned `id` or `email` with `--account`.

3. **Read paths** for lookups (fast, safe even while the desktop app is open):
   ```bash
   emailops-cli search 'invoice from acme' --json --limit 5
   emailops-cli show  --json
   emailops-cli emails --json --mailbox inbox --limit 25
   ```

4. **AI path** for natural-language questions (loads the local model):
   ```bash
   emailops-cli chat 'what did the landlord say about the lease renewal?' --json
   ```
   Add `--trace` to inspect routing, retrieval, tool calls, and the sources
   behind the answer under `data.trace`.

5. **Multi-turn**: carry context across one-shot invocations by passing the
   `conversationId` from a previous `chat --json` result:
   ```bash
   emailops-cli chat 'and what date was that?' --json --conversation 
   ```

6. **Parse the envelope**, branch on `ok` / exit code, and answer the user
   from `data`. Surface `error.message` on failure.

## Writing mail: compose, drafts, send

`compose` is the single write path. **By default it saves a draft** (and, on
Gmail/Outlook accounts, also pushes it to the provider's Drafts folder — no
extra flag). Add `--send` to deliver immediately instead. Both paths return the
resulting record under `data`.

> **🚫 NEVER send without explicit user confirmation.** Do not run `compose …
> --send` until you have shown the user the **full email about to be sent** —
> the sending account, **every** To/Cc recipient, the subject, the complete
> body, and any attachments — and the user has **explicitly approved that exact
> message**. No exceptions: not for "obvious" replies, not because the user said
> "send it" earlier about a different draft, not to save a round-trip. A vague
> or prior "go ahead" is **not** sufficient — re-confirm after showing the final
> content. When unsure whether you have approval, **save a draft and ask** — you
> never need permission to save a draft, and you always need it to send.

```bash
# Save a draft (default). --to / --cc / --attach are repeatable.
emailops-cli --account  compose \
  --to a@x.com --to b@y.com --cc c@z.com \
  --subject "Q3 numbers" \
  --body "Hi — draft here." \
  --attach /abs/path/report.pdf --json

# Long/rich body: read it from a file instead of --body (mutually exclusive).
emailops-cli --account  compose \
  --to a@x.com --subject "Notes" --body-file /abs/path/body.txt --json

# Update an existing draft in place rather than creating a new one.
emailops-cli --account  compose --draft  \
  --subject "Q3 numbers (rev)" --body "Updated." --to a@x.com --json

# Send now — ONLY after showing the full email and getting explicit approval
# (see "Mandatory send procedure" below). Delivers immediately; irreversible.
emailops-cli --account  compose \
  --to a@x.com --subject "Q3 numbers" --body "Final." --send --json
```

Rules and shapes:

- **`--to`, `--cc`, `--attach` are repeatable**; pass the flag once per value.
- **Body is `--body` XOR `--body-file`.** Supplying both is invalid input
  (exit 2, no JSON envelope — it's rejected at the argument layer). `--body-file`
  reads the body verbatim from the path.
- **Use real line breaks — write bodies like a normal email.** The CLI stores
  the body **verbatim and does not interpret escape sequences**, so a plain
  `--body "Hi\n\nText"` is stored as the literal characters `\n` and the
  recipient sees `\n` in the message. Format the body the way a person would:
  greeting on its own line, a blank line between paragraphs, and a blank line
  before the sign-off. Two reliable ways to pass **actual** newlines:
  - **`--body-file` (preferred for anything multi-line):** write the body to a
    file with real newlines, then reference it —
    `printf 'Hi there,\n\nFirst paragraph.\n\nThanks,\nGero\n' > /tmp/body.txt`
    then `compose … --body-file /tmp/body.txt`.
  - **ANSI-C quoting for short inline bodies:** `--body $'Hi there,\n\nText'`
    (the `$'…'` form makes the shell expand `\n` into real newlines; ordinary
    `"…"` double quotes do **not**).
- **Attachments are path references**, resolved at compose/send time. Use
  **absolute paths readable now**; `filename` and `mimeType` are inferred. Each
  returned attachment is `{id, draftId, filePath, filename, mimeType}`.
- **`compose` (draft) returns the draft object**: `id`, `accountId`, `status`
  ("draft"), `subject`, `toAddresses[]`, `ccAddresses[]`, `body`, `attachments[]`,
  `providerDraftId` (null on IMAP/local; set once pushed to Gmail/Outlook),
  `createdAt`/`updatedAt`. `drafts` returns an array of these (newest-first);
  `draft ` returns one.
- **Drafts don't require recipients** (a bare draft with empty `toAddresses` is
  saved fine) — but confirm the recipient list before `--send`.

Review drafts:

```bash
emailops-cli --account  drafts --json      # newest-first list
emailops-cli --account  draft  --json  # one draft, full
```

### Mandatory send procedure (never skip a step)

`--send` is irreversible and there is **no undo**. Every send MUST follow this
order:

1. **Save it as a draft first** (`compose …`, no `--send`) — never build the
   send command straight from user text.
2. **Show the user the full outgoing email** by reading it back with
   `draft  --json`: the sending account, **all** To/Cc recipients, the
   subject, the **entire** body, and the filename of each attachment. Present it
   plainly, not just a summary.
3. **Ask for explicit approval of that exact message** and wait for a clear
   "yes, send this". If the user asks for any change, edit the draft
   (`compose --draft  …`) and repeat from step 2 — approval of an earlier
   version does **not** carry over.
4. Only then run `compose --draft  --send --json` (or an equivalent
   `--send` with the identical, already-approved fields).

If you cannot show the full message and get a fresh, explicit yes, **do not
send** — leave the draft saved and tell the user it's ready to review.

## Pitfalls

- **Heavy writes need the app closed.** `sync`, `classify`, `embed`, and the
  heavy `compose` write paths — **`--send`, and provider draft push on
  Gmail/Outlook** — mutate the SQLite DB (and hit the network); run them only
  when the EmailOps desktop app is **not** running (WAL contention). A plain
  **save-as-draft on an IMAP/local account is safe even while the app is open**
  (the draft stays local — `providerDraftId` is null). Read commands are always
  safe.
- **Don't scrape pretty output.** Human output is styled (color/tables) only on
  an interactive TTY and is not a stable contract. Always `--json`.
- **Logs are on stderr.** Don't merge stderr into stdout when parsing JSON;
  use `--quiet` to silence the app-log stream.
- **No model for `doctor`.** It's intentionally cheap — use it as the gate
  before any AI command rather than calling `chat` blindly.
- **Never send without explicit user confirmation.** `--send` is irreversible;
  always follow the **Mandatory send procedure** above — save a draft, show the
  full email (account, all recipients, subject, body, attachments), get a fresh
  explicit yes, then send. When in doubt, leave it as a draft.
- **Same commands in the REPL.** Running `emailops-cli` with no subcommand opens
  an interactive REPL that exposes these as `/compose`, `/drafts`, and
  `/draft `. For the agent, prefer the one-shot subcommands with `--json`.
- **Double-encoded saved files.** When emailops-cli output is written to a file
  and ends up double-escaped (the outer JSON has an `"output"` key whose value
  is a string containing the inner JSON), unwrap it first:

    ```bash
    cat saved_result.json | jq -r '.output' | jq ...
    ```

  Without `-r .output`, `jq` will try to parse the escaped string as literal
  JSON tokens and silently fail or return wrong results.
- **Outlook/Hotmail control characters break `jq`.** Emails from Outlook/Hotmail
  providers can inject raw `\r\n` sequences that cause `jq: parse error: Invalid
  string: control characters …` errors. Pre-filter before piping to jq:

    ```bash
    cat output.json | sed 's/\\r//g' | jq ...
    ```

## Verification

```bash
emailops-cli doctor --json   # expect ok:true, accountsEnabled > 0, exit 0
emailops-cli accounts --json # expect a non-empty data array
```

If `doctor` returns `ok:true` with at least one enabled account, the skill is
correctly wired and downstream commands can be trusted.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [gerodp](https://github.com/gerodp)
- **Source:** [gerodp/hermes-productivity-skills](https://github.com/gerodp/hermes-productivity-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-gerodp-hermes-productivity-skills-emailops
- Seller: https://agentstack.voostack.com/s/gerodp
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
