Read, search, send, and manage Zoho Mail from the terminal. JSON output for scripting and agents. No third-party service required.
---
name: zoho-mail
version: 0.1.1
description: Read, search, send, and manage Zoho Mail from the terminal. JSON output for scripting and agents. Requires the 'zoho' binary (install via brew/uv/pipx), one-time OAuth setup (zoho config init; zoho login), and stores credentials locally (config file and OS keyring). Optional env ZOHO_ACCOUNT, ZOHO_CONFIG, ZOHO_TOKEN_PASSWORD. No third-party service required.
compatibility: Requires the 'zoho' CLI (install via brew/uv/pipx), one-time OAuth setup (zoho config init; zoho login), and network access. Primary credentials: OAuth client_id/client_secret and access/refresh tokens; stored locally in config.json and/or OS keyring — users should be aware these are sensitive secrets. Optional env: ZOHO_ACCOUNT, ZOHO_CONFIG, ZOHO_TOKEN_PASSWORD.
homepage: https://github.com/robsannaa/zoho-cli
user-invocable: false
requires:
bins:
- zoho
env:
- name: ZOHO_ACCOUNT
description: Default Zoho account email
required: false
- name: ZOHO_CONFIG
description: Path to config.json
required: false
- name: ZOHO_TOKEN_PASSWORD
description: Passphrase for encrypted file token storage (CI/headless)
required: false
install:
- kind: brew
formula: robsannaa/tap/zoho-cli
bins: [zoho]
- kind: uv
package: git+https://github.com/robsannaa/zoho-cli
bins: [zoho]
- kind: pipx
package: git+https://github.com/robsannaa/zoho-cli
bins: [zoho]
---
# zoho — Zoho Mail CLI
**Requirements:** the `zoho` binary (install via brew/uv/pipx below), one-time OAuth setup, and local credential storage (config + OS keyring).
`zoho` is a command-line tool for Zoho Mail. All output is **JSON by default** — structured, pipe-friendly, and agent-ready. Use `--md` for markdown tables.
## Install
**Requires Python 3.11+. Works on macOS, Linux, and Windows.**
```bash
# Homebrew (macOS / Linux)
brew install robsannaa/tap/zoho-cli
# uv (all platforms)
uv tool install git+https://github.com/robsannaa/zoho-cli
# pipx (all platforms)
pipx install git+https://github.com/robsannaa/zoho-cli
```
After install, run the one-time setup:
```bash
zoho config init # save your Zoho OAuth client_id / client_secret
zoho login # open browser, region auto-detected
```
See the full setup guide in the [README](https://github.com/robsannaa/zoho-cli#setup).
---
## Prerequisites
- `zoho` must be installed and authenticated (`zoho login` completed).
- Check: `zoho config show` — must return a JSON object with `default_account` and a non-empty `accounts` map.
- If not logged in: tell the user to follow the setup in the README.
## Output
- **stdout** → JSON (default) or Markdown (`--md`). Always machine-readable.
- **stderr** → errors (as JSON), debug logs, interactive prompts. Never in stdout.
- **Exit 0** = success. **Exit 1** = error (JSON error object on stderr).
```bash
# errors land on stderr, not stdout
zoho mail list 2>/dev/null # stdout empty on error
zoho mail list 2>&1 >/dev/null # see error JSON
```
## Authentication
Tokens are stored in the OS keyring and refreshed automatically before every command. No manual token management needed.
**Credential storage (sensitive):** Users must be aware that the CLI stores sensitive data locally:
- **OAuth client_id and client_secret** — saved via `zoho config init` in the config file (e.g. `~/.config/zoho-cli/config.json`).
- **Access and refresh tokens** — stored in the OS keyring (or, if `ZOHO_TOKEN_PASSWORD` is set, in an encrypted file). These are used to access Zoho Mail on the user's behalf.
Do not expose config files or keyring entries to untrusted parties.
If a command fails with `not_logged_in` or `token_refresh_failed`, tell the user to run:
```bash
zoho login
```
## Global flags
```
--account EMAIL select account (env: ZOHO_ACCOUNT)
--config PATH config file path (env: ZOHO_CONFIG)
--md markdown table output instead of JSON
--debug HTTP + debug logs to stderr
```
---
## Commands
### `zoho mail list`
List messages in a folder.
```bash
zoho mail list # Inbox, 50 messages
zoho mail list --folder Sent --limit 20
zoho mail list -f "My Project" -n 100
```
**Output** — array of message summaries:
```json
[
{
"messageId": "1771333290108014300",
"folderId": "8072279000000002014",
"subject": "Q1 invoice",
"from": "billing@acme.com",
"to": ["you@example.com"],
"date": "1740067200000",
"unread": true,
"hasAttachments": true,
"tags": []
}
]
```
`date` is a Unix timestamp in **milliseconds**.
### `zoho mail search`
```bash
zoho mail search "invoice 2025"
zoho mail search "from:boss@example.com" --limit 10
```
Same output shape as `mail list`.
### `zoho mail get`
Full message content including body.
```bash
zoho mail get MESSAGE_ID
zoho mail get MESSAGE_ID --folder-id FOLDER_ID # faster: skips auto-scan
zoho mail get MESSAGE_ID | jq '.textBody'
```
**Output:**
```json
{
"messageId": "...",
"folderId": "...",
"subject": "Q1 invoice",
"from": "billing@acme.com",
"to": ["you@example.com"],
"cc": [],
"bcc": [],
"date": "1740067200000",
"unread": false,
"hasAttachments": true,
"tags": [],
"textBody": "Please find the invoice attached.",
"htmlBody": "<p>Please find...</p>"
}
```
### `zoho mail attachments`
```bash
zoho mail attachments MESSAGE_ID
zoho mail attachments MESSAGE_ID --folder-id FOLDER_ID
```
**Output:**
```json
[
{ "attachmentId": "256063600000020001", "fileName": "invoice.pdf", "size": 123456 }
]
```
### `zoho mail download-attachment`
```bash
zoho mail download-attachment MESSAGE_ID ATTACHMENT_ID --out /tmp/invoice.pdf
```
**Output:**
```json
{ "status": "ok", "path": "/tmp/invoice.pdf", "size": 123456 }
```
### `zoho mail send`
```bash
zoho mail send --to alice@example.com --subject "Hi" --text "Hello!"
# HTML + multiple recipients + attachments
zoho mail send \
--to alice@example.com --to bob@example.com \
--cc manager@example.com \
--subject "Report" \
--html-file report.html \
--attach report.pdf --attach data.csv
```
**Output:**
```json
{ "status": "ok", "messageId": "..." }
```
### Flag / move / delete operations
All accept one or more `MESSAGE_ID` arguments.
```bash
zoho mail mark-read ID [ID …]
zoho mail mark-unread ID [ID …]
zoho mail move ID [ID …] --to "Archive"
zoho mail spam ID [ID …]
zoho mail not-spam ID [ID …]
zoho mail archive ID [ID …]
zoho mail unarchive ID [ID …]
zoho mail delete ID [ID …] # → Trash
zoho mail delete ID [ID …] --permanent # hard delete
```
**Output:**
```json
{ "status": "ok", "updated": ["ID1", "ID2"], "failed": [] }
```
### `zoho folders list`
```bash
zoho folders list
```
**Output:**
```json
[
{ "folderId": "8072279000000002014", "folderName": "Inbox", "folderType": "Inbox",
"path": "/Inbox", "isArchived": 0, "unreadCount": 3, "messageCount": 42 }
]
```
### `zoho folders create / rename / delete`
```bash
zoho folders create "Project X" [--parent-id FOLDER_ID]
zoho folders rename FOLDER_ID "New Name"
zoho folders delete FOLDER_ID
```
### `zoho config show / path / init`
```bash
zoho config show # JSON config dump (secret redacted)
zoho config path # {"config_path": "...", "exists": true}
zoho config init # interactive wizard (prompts on stderr)
```
---
## Common patterns for agents
```bash
# All unread message subjects
zoho mail list | jq -r '.[] | select(.unread) | .subject'
# Get the latest message ID
zoho mail list -n 1 | jq -r '.[0].messageId'
# Get plain-text body of a specific message
zoho mail get "$MSG_ID" | jq -r '.textBody'
# Download all attachments from a message
zoho mail attachments "$MSG_ID" | jq -r '.[].attachmentId' | while read id; do
zoho mail download-attachment "$MSG_ID" "$id" --out "/tmp/${id}"
done
# Find unread emails from a sender
zoho mail search "from:boss@example.com" | jq '[.[] | select(.unread)]'
# Mark all results as read
zoho mail search "invoice" | jq -r '.[].messageId' | xargs zoho mail mark-read
# Move search results to a folder
zoho mail search "newsletter" | jq -r '.[].messageId' | xargs zoho mail move --to "Archive"
# Send with computed body
zoho mail send \
--to report-reader@example.com \
--subject "Daily digest $(date +%F)" \
--text "$(zoho mail list | jq -r '.[].subject' | head -10 | nl)"
```
## Error response shape
All errors are JSON on stderr:
```json
{
"status": "error",
"error": "not_logged_in",
"details": "No stored token for you@example.com. Run: zoho login --account you@example.com"
}
```
Common error codes:
| Code | Meaning |
|---|---|
| `not_logged_in` | No token stored — user must run `zoho login` |
| `token_refresh_failed` | Token revoked — user must run `zoho login` |
| `no_account_id` | accountId not saved — run `zoho login` again |
| `folder_not_found` | Folder name doesn't match any folder |
| `message_not_found` | Message ID not found in any folder |
| `api_error` | Upstream Zoho API error (details in message) |
don't have the plugin yet? install it then click "run inline in claude" again.
added explicit intent, comprehensive inputs with credential storage details and optional env vars, numbered procedure steps with edge cases, decision points for auth failures and folder/message lookup, structured output contract with error json shapes and timestamp format, and concrete outcome signals with debug guidance.
zoho is a command-line tool for reading, searching, sending, and managing Zoho Mail messages and folders. use it when you need to automate email workflows, build agent integrations, or script email operations without third-party API gateways. all output is json by default, making it ideal for piping into jq, agents, or other tools. you need the zoho binary installed, one-time oauth setup (zoho config init && zoho login), and local credential storage (config file + os keyring). tokens refresh automatically before every command.
binary requirement:
zoho CLI tool (Python 3.11+, macOS/Linux/Windows). install via:brew install robsannaa/tap/zoho-cliuv tool install git+https://github.com/robsannaa/zoho-clipipx install git+https://github.com/robsannaa/zoho-clioauth setup (one-time):
zoho config init to provide Zoho OAuth client_id and client_secret (obtain from https://accounts.zoho.com/developerconsole)zoho login to authenticate (opens browser, auto-detects region)zoho config show (should return json with default_account and non-empty accounts map)credential storage (sensitive):
~/.config/zoho-cli/config.json)ZOHO_TOKEN_PASSWORD env var is setenvironment variables (optional):
ZOHO_ACCOUNT: default Zoho account email (overrides default_account in config)ZOHO_CONFIG: path to config.json (overrides default location)ZOHO_TOKEN_PASSWORD: passphrase for encrypted file token storage (useful in ci/headless environments where os keyring is unavailable)network access:
verify cli is installed and authenticated
zoho binary availablezoho config showdefault_account and accounts map, or error if not logged innot_logged_in error, user must run zoho login before proceedinglist messages in a folder
zoho mail list [--folder FOLDER_NAME] [--limit N] [--account EMAIL]messageId, subject, from, to, date (unix ms), unread, hasAttachments, tags[], not an errorsearch messages
zoho mail search "QUERY" [--limit N]retrieve full message
zoho mail get MESSAGE_ID [--folder-id FOLDER_ID]textBody, htmlBody, cc, bccmessage_not_found errorlist attachments in a message
zoho mail attachments MESSAGE_ID [--folder-id FOLDER_ID]attachmentId, fileName, size (bytes)download single attachment
zoho mail download-attachment MESSAGE_ID ATTACHMENT_ID --out /path/to/filestatus: "ok", path, sizesend message
zoho mail send --to ADDR [--to ADDR …] --subject "SUBJ" [--text "BODY" | --html-file FILE] [--cc ADDR] [--bcc ADDR] [--attach FILE …]status: "ok" and messageIdmark messages as read/unread
zoho mail mark-read ID [ID …] or zoho mail mark-unread ID [ID …]status: "ok", updated array of ids, failed arrayfailed but command exits 0move messages to folder
zoho mail move ID [ID …] --to "FOLDER_NAME"status: "ok", updated array, failed arrayfolder_not_found errorspam / not-spam / archive / delete
zoho mail spam ID [ID …] or zoho mail not-spam ID [ID …] or zoho mail archive ID [ID …] or zoho mail unarchive ID [ID …] or zoho mail delete ID [ID …] or zoho mail delete ID [ID …] --permanentstatus: "ok", updated array, failed arrayupdated arraylist folders
zoho folders listfolderId, folderName, folderType (e.g. "Inbox", "Sent", "Custom"), path, isArchived, unreadCount, messageCountcreate / rename / delete folder
zoho folders create "NAME" [--parent-id PARENT_ID] or zoho folders rename FOLDER_ID "NEW_NAME" or zoho folders delete FOLDER_IDstatus: "ok" and created/updated folderIdinspect or reset configuration
zoho config show or zoho config path or zoho config initconfig show returns json config (secrets redacted); config path returns {"config_path": "...", "exists": true}; config init is interactive (prompts on stderr, no json output)handle authentication failures
error field value: not_logged_in, token_refresh_failed, no_account_idzoho login [--account EMAIL] to re-authenticateZOHO_TOKEN_PASSWORD not set, login fails; user must set env var or unlock keyringif user is not authenticated:
"error": "not_logged_in" or "error": "token_refresh_failed"zoho login [--account EMAIL]if message_id not found:
"error": "message_not_found"if folder name doesn't match:
"error": "folder_not_found"zoho folders list to get exact folder name; names are case-sensitiveif output format requested:
--md flag present, output markdown table (human-readable) instead of jsonif attachment file path does not exist (on send):
if target file already exists (on download-attachment):
if api rate limit hit:
"error": "api_error" with details mentioning rate limitfailed arrayif network timeout or connection lost:
"error": "api_error" or connection error on stderrif os keyring is unavailable or locked (macOS/Linux):
zoho login or token refreshZOHO_TOKEN_PASSWORD env var to enable encrypted file storage insteadZOHO_TOKEN_PASSWORD before running commandssuccess responses (exit 0):
--md flag)status: "ok", updated array of ids, optional failed arrayerror responses (exit 1):
status: "error", error code string, details explanationnot_logged_in, token_refresh_failed, no_account_id, folder_not_found, message_not_found, api_error, etc.{"status": "error", "error": "message_not_found", "details": "..."}file downloads (download-attachment):
--out pathtimestamps:
date fields are unix timestamps in milliseconds (not seconds)jq '.date | tonumber / 1000 | strftime("%Y-%m-%d %H:%M:%S")'the skill worked if:
--md used)messageId, subject, from in a message list)common success checks:
zoho mail list | jq '.[] | .messageId' prints message ids without errorszoho mail send --to user@example.com --subject "test" --text "hi" | jq '.status' returns "ok"zoho mail mark-read $ID | jq '.updated | length' returns count of updated messageszoho config show | jq '.default_account' returns a non-null email addresshow to know it failed:
not_logged_in, token_refresh_failed, message_not_found, or folder_not_found--debug flag to see http logs, or interrupt and check network)debug mode:
--debug flag to see http requests, responses, and internal logs on stderrcredits: original CLI by robsannaa (https://github.com/robsannaa/zoho-cli). skill documentation enriched for Implexa standards.