# Zimbra Mail

> Read, search, and draft email on any Zimbra mailbox (e.g. university/lab Zimbra, RENATER Partage, Inria, CNRS) via IMAP/SMTP using the dependency-free zmail.py CLI. Use when the user wants to check their Zimbra inbox, summarize or find messages, or prepare a reply as a draft they can review in webmail. Drafts are staged by default; sending is separate and requires explicit confirmation.

- Skill: `natema/zimbra-mail` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add natema/zimbra-mail`
- Raw SKILL.md: https://api.skillmd.com/api/skills/natema/zimbra-mail/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Research & Search
- Author: natema (https://skillmd.com/u/natema)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/natema/zimbra-mail

---


# Zimbra mail (zmail)

A stdlib-only Python CLI (`zmail.py`) over Zimbra IMAP/SMTP. Accounts and
credentials live in `~/.config/zimbra/config.toml` (never in the repo).

**Read the site notes first.** If `~/.config/zimbra/NOTES.md` exists, read it before
running account-specific commands — it records this machine's quirks (which account
"email" defaults to, `username` login forms, exact folder names like `Archive` vs
`Archives`, server capabilities such as missing IMAP `MOVE`). It's private and lives
outside the repo. Keep it updated when you discover a new quirk.

**Safety posture:** the default action STAGES a draft in the user's Drafts folder
(visible/editable/sendable from Zimbra webmail). Only run `send` after the user
explicitly asks to send, and it still requires `--yes-really-send`.

## Setup check (do this first if anything fails)

```bash
python3 zmail.py accounts          # lists configured accounts; proves config loads
```
If it errors about missing config, the user hasn't created
`~/.config/zimbra/config.toml` yet — point them to `README.md` / `config.example.toml`.
If IMAP login fails, the connection worked but credentials were rejected: check the
`username` form, or (if 2FA is on) use an **application passcode** generated in webmail
(Preferences > Accounts > Applications > Add Application Code).

## Reading

```bash
# newest 20 in INBOX (• marks unread). Add --unread for only-unread.
python3 zmail.py -a work check --limit 20
python3 zmail.py -a work check --unread --json      # JSON for parsing

python3 zmail.py -a work folders                     # list folders
python3 zmail.py -a work read 12345                  # full message by UID
python3 zmail.py -a work read 12345 --json           # structured (has message_id)
python3 zmail.py -a work search "grant deadline" --limit 10
```

`check`/`search` print a UID in `[brackets]`; feed that UID to `read`.
`read --json` returns `message_id` — pass it to `--in-reply-to` for threaded replies.

## Drafting (default, safe)

```bash
python3 zmail.py -a work draft \
  --to "someone@example.org" \
  --subject "Re: paper" \
  --body-file /path/to/body.txt          # or --body "short text"

# threaded reply:
python3 zmail.py -a work draft --to "x@y.z" --subject "Re: ..." \
  --in-reply-to "<original-message-id>" --body-file body.txt

# with attachments (repeat --attach; MIME type guessed from extension):
python3 zmail.py -a work draft --to "x@y.z" --subject "Report" \
  --body-file body.txt --attach report.pdf --attach data.csv
```

### Replying to a message (preferred for replies)

`reply <UID>` auto-fills the recipient, `Re:` subject, and threading headers from the
original — no need to look up the Message-ID yourself. Stages a draft by default.

```bash
# find the UID with check/search, then:
python3 zmail.py -a work reply 12345 --body-file reply.txt          # To: sender
python3 zmail.py -a work reply 12345 --body-file reply.txt --reply-all
python3 zmail.py -a work reply 12345 --body-file reply.txt --quote  # include quoted original
python3 zmail.py -a work reply 12345 --folder Archive --body-file reply.txt
```
Use `reply` instead of `draft --in-reply-to` when responding to an existing message.
It defaults to a draft; only add `--yes-really-send` if the user explicitly asks to send.
For multi-line/accented bodies, write the body to a temp file and use `--body-file`
(avoids shell-quoting problems). Then tell the user the draft is in webmail Drafts.

## Attachments

```bash
python3 zmail.py -a work attachments 12345                       # list what's attached
python3 zmail.py -a work attachments 12345 --save --outdir ~/docs   # save them all
python3 zmail.py -a work attachments 12345 --save --index 1 --outdir .   # just the first
```

`read` shows only the message text, so use `attachments` when the user needs the
actual files. Listing prints a 1-based index, filename, MIME type and size; feed the
index back with `--index` (repeatable) to save a subset. Files are written under their
own basename, and an existing file is never clobbered without `--overwrite`.

## Archiving

```bash
python3 zmail.py -a work archive 12345                       # INBOX -> Archive
python3 zmail.py -a work archive 12345 --folder Archive --to "Archive/2026/work"
```
Moves a message between folders (uses IMAP MOVE if the server supports it, else
COPY + delete + expunge). Not a destructive send/delete action, but still changes
mailbox state — only archive a specific message the user pointed you at, not in bulk.

## Deleting

```bash
python3 zmail.py -a work delete 12345                       # move to Trash (recoverable)
python3 zmail.py -a work delete 12345 --folder Archive      # delete from another folder
python3 zmail.py -a work delete 12345 --folder Trash --purge --yes-really-delete   # permanent
```
Default `delete` just moves the message to **Trash** — recoverable from webmail, like
hitting delete there. Permanent removal needs BOTH `--purge` and `--yes-really-delete`;
without the confirmation flag it refuses. Only delete a specific message the user
pointed you at, never in bulk on your own. (Trash folder name defaults to `Trash`;
override with `--trash` if the site differs.)

## Sending (only on explicit request)

```bash
python3 zmail.py -a work send --to "x@y.z" --subject "..." \
  --body-file body.txt --yes-really-send
```
Never add `--yes-really-send` unless the user has clearly said to send. Prefer
`draft` and let the human hit send.

## Conventions
- Replace `work` with the relevant account name; omit `-a` to use the config default.
- Use `--json` when you need to parse; plain text when showing the user.
- Don't `--mark-read` unless asked; reading is read-only by default.
- UIDs are per-folder; the same message has different UIDs in different folders.

