Skip to main content

zohomail

An AI email client for Zoho Mail: IMAP / POP3 / SMTP access with an app-specific password, a local SQLite mirror of the mailbox with full-text search and deduplicated attachment storage, an MCP server, and Claude Code skills and subagents that read, triage, and draft against it.

Standard library only: no third-party packages, no OAuth app, no API key. MIT licensed. Mail, passwords and rules stay in a profile outside this repository, so the tree itself can be shared.

Verified against a Zoho organisation account on the zoho.com data centre.

pip install -e .               # from a checkout; or pip install .
zohomail doctor                # prove connectivity
zohomail sync                  # mirror the last 90 days into SQLite
zohomail db stats              # what's local now

python -m zohomail is the same program. From this directory it works without installing; pip install is what puts zohomail on PATH for any other directory, and what a published install uses.

Then, in Claude Code: "triage my inbox", "what am I waiting on?", "draft a reply to the invoice thread".


Setup

  1. Install. From a checkout, pip install -e .. Once the repository is public, pip install git+https://github.com/alphaolomi/zoho-mail.git installs the engine without the Claude skills. The skills, agents and scripts/ stay with the clone; they are not part of the wheel.

  2. Generate an app-specific password at accounts.zoho.com → Security → App Passwords. Not your account password; it bypasses 2FA for mail protocols only.

  3. Enable the protocols in Zoho Mail → Settings → Mail Accounts → IMAP / POP Access.

  4. Create your profile — your config, rules and mail data live outside the repo, in ~/.zohomail/<name>/ (see Profiles):

    zohomail profile init alice --email you@yourdomain.com
    zohomail profile use alice
    

    Then fill in ~/.zohomail/alice/config.env:

    ZOHO_EMAIL=you@yourdomain.com
    ZOHO_APP_PASSWORD=xxxxxxxxxxxx
    ZOHO_DC=com                # com | eu | in | au | jp | ca | sa | uk
    ZOHO_HOST_STYLE=standard   # standard -> imap.zoho.com | pro -> imappro.zoho.com
    SYNC_DAYS=90
    SYNC_FOLDERS=INBOX,Sent      # names or globs: "*", "INBOX/*", "!INBOX/Spammy"
    BULK_FOLDERS=                # alert folders your Zoho filters fill (see below)
    
  5. Write your rules in ~/.zohomail/alice/rules.md: who matters, what is urgent, how replies should sound. The skills read it before they act.

  6. Check it — zohomail doctor probes both host styles across all three protocols and reports which authenticate.

Environment variables override config.env.


Profiles

The repo is the engine: code, schema, skills, agents and scripts, with nothing personal in it. Each person, or each mailbox, gets a profile:

~/.zohomail/<name>/
  config.env    account, app password, folders, bulk folders, mute list
  rules.md      their triage priorities, key people, drafting voice, hard limits
  store/        mail.db and attachment blobs
  state/        poll cursors and scheduler logs

The active profile is, first match wins: --profile NAME, ZOHOMAIL_HOME=<dir>, ZOHOMAIL_PROFILE=<name>, the one set by profile use, then a legacy .env in the repo, then default. Set ZOHOMAIL_PROFILES to keep profiles somewhere other than ~/.zohomail.

python -m zohomail profile list            # * marks the active one
python -m zohomail profile show            # where its config, rules and store are
python -m zohomail profile rules           # what the skills read
python -m zohomail --profile technical sync
python -m zohomail.mcp_server --profile technical

Two people on two machines each run profile init once. Two mailboxes on one machine (your own and a shared technical@, say) are two profiles; pick one per command with --profile, or register scheduled tasks per profile with -MailProfile.

Upgrading from the in-repo layout. Older installs kept .env, store/ and state/ inside the repo. That still works, and one command moves it out:

python -m zohomail profile migrate alice

It copies .env to config.env, moves store/ and state/, adds a rules template, and makes alice active. The repo .env is removed only after everything else has moved; stop the MCP server first so the store is not open.

Servers

Protocol Standard Pro (some org accounts) Port Security
IMAP imap.zoho.com imappro.zoho.com 993 SSL
POP3 pop.zoho.com poppro.zoho.com 995 SSL
SMTP smtp.zoho.com smtppro.zoho.com 465 SSL

ZOHO_SMTP_PORT=587 switches to STARTTLS automatically.


The local store

python -m zohomail sync mirrors IMAP into the profile's store/mail.db. Sync is incremental — each folder keeps a UID cursor, and a UIDVALIDITY change invalidates it correctly.

python -m zohomail sync                                  # configured folders, SYNC_DAYS window
python -m zohomail sync --days 365 --full                # backfill a year
python -m zohomail sync --folders "INBOX,Sent,Newsletter"
python -m zohomail sync --folders "INBOX/*"              # every folder under INBOX
python -m zohomail sync --folders "*" --max 500          # every folder, capped
python -m zohomail db stats                              # counts, range, per-folder cursors

Folders and hierarchy

Folders are stored under their full server path (INBOX/Alerts), so the store mirrors the hierarchy you see in Zoho. SYNC_FOLDERS, --folders and search --folder all take the same selectors:

Selector Picks
INBOX/Alerts that folder (case does not matter)
Alerts the one folder whose path ends /Alerts; an error if two do
Sent, Spam, Trash, Drafts the server's special-use folder, whatever it is called
INBOX/* every folder under INBOX, but not INBOX itself
* every folder except Trash, Spam and Drafts (name those to include them)
!INBOX/Newsletters removes a folder the other entries picked

Bulk folders. Zoho filters often route alerts into their own folders. List those in BULK_FOLDERS and their messages are marked automated, so --no-bots, the triage queue and briefs skip them. Header heuristics miss senders such as UptimeRobot; the folder does not. A bulk folder's first sync reaches back only BULK_SYNC_DAYS (default 30, 0 = the normal window, --bulk-days per run). New alerts are always kept.

SYNC_FOLDERS=*
BULK_FOLDERS=INBOX/Alerts,INBOX/Newsletters
BULK_SYNC_DAYS=30

Entities

accounts ── folders ── messages ── message_addresses ── addresses
                          │
                          ├── attachments   (sha256-deduped blobs on disk)
                          ├── message_refs  (Message-ID graph for threading)
                          ├── message_labels ── labels
                          ├── analyses      (what the AI concluded)
                          └── threads ── tasks, drafts
  • Threading stitches messages by In-Reply-To/References first, merging threads when a late message joins two chains. Subject matching is only a fallback, and only when a participant overlaps — so two unrelated "Re: Hello" chains never merge.
  • Attachments are content-addressed: bytes go to store/blobs/<aa>/<sha256>, so the same signature logo across 40 messages is stored once. Inline parts are flagged so you can filter for real documents. Files over MAX_ATTACHMENT_MB are indexed without their bytes.
  • Full-text search is FTS5 over subject, body, sender name and address, kept current by triggers and ranked with bm25.

Reading it

python -m zohomail search "invoice NOT draft" --no-bots
python -m zohomail search --sender alex --attachments
python -m zohomail show 12                       # one message, full body
python -m zohomail thread 10                     # the whole conversation
python -m zohomail threads --awaiting-reply      # you wrote last, silence since
python -m zohomail attachments --name .pdf --extract ./out

Every command takes --json for machine consumption.

FTS5 syntax: invoice payment (AND), "purchase order" (phrase), invoice OR receipt, deploy NOT staging, integrat* (prefix).


The AI layer

The Python holds no model calls. Claude Code does the reasoning, and the store is its memory — so conclusions survive across sessions and accumulate.

Table Holds
analyses priority, category, summary, action items, needs-reply, confidence
labels topic/category tags applied to messages
tasks extracted work with due dates and status
drafts proposed replies awaiting your approval
python -m zohomail queue                    # messages with no analysis yet
python -m zohomail annotate --message 12 --priority high --category partner-integration \
  --summary "The invoice is due Friday" \
  --action "Send the receipt" --needs-reply --label billing
python -m zohomail task add --title "Send the receipt" --message 12 --due 2026-09-18
python -m zohomail task                     # open tasks by due date

Skills

In .claude/skills/, available in any Claude Code session in this directory:

Skill Use
mail-triage Work the queue: judge, annotate, label, create tasks
mail-search Find mail, answer questions from it, pull attachments
mail-draft Draft a reply in your voice, saved for review
mail-brief A read briefing: what needs you, what you're waiting on

Subagents

In .claude/agents/ — dispatchable, each running in its own context:

Agent Does Never
mail-triage-agent Syncs, triages the queue, files tasks, reports sends, deletes, marks read
mail-reply-drafter Reads the thread, matches your voice, drafts sends
mail-archivist Syncs/backfills, labels, finds noise and stale threads sends, deletes server mail

MCP server

.mcp.json registers a stdio MCP server exposing 18 tools — mail_search, mail_message, mail_thread, mail_threads, mail_queue, mail_stats, mail_attachments, mail_attachment_extract, mail_annotate, mail_task_add, mail_tasks, mail_task_status, mail_draft_create, mail_drafts, mail_draft_get, mail_draft_send, mail_sync, mail_flag.

python -m zohomail.mcp_server        # stdio; any MCP client can launch it

Point Claude Desktop or another client at the same command to query this mailbox from outside Claude Code.

Sending is always gated

Drafts are written to the store, never sent automatically:

python -m zohomail draft new --to alex@example.com --subject "RE: ..." \
  --body-file reply.txt --thread 10 --reply-to 12 --rationale "unblocks testing"
python -m zohomail draft show 1
python -m zohomail draft send 1 --yes     # --yes is required

Without --yes the CLI refuses and exits 2. The MCP mail_draft_send tool requires confirm: true. Both mean the user said send it — not the model's own confidence. The agents are instructed never to send.


Live mailbox commands

These talk to Zoho directly, without the store:

Command What it does
doctor Probe IMAP/SMTP/POP on both host styles
folders List folders with unread counts
list Search live IMAP: --unread --since 7d --sender x --subject y
read UID Print a message; --save-attachments DIR, --mark-read
send Send directly: --to --subject --body --attach --cc --html-file
pop-download Bulk-fetch as .eml: --dir --limit --delete
digest Mechanical digest: --hours 24 --send --out digest.html
reminders Action items and awaiting-reply: --send --include-automated
stats Volume, top senders, newsletters: --days 7
poll-once Report mail since the last poll; --on-new "cmd {subject}"
watch Poll in the foreground: --interval 300

digest and reminders filter automated mail — newsletters (List-Unsubscribe), no-reply@ senders, Auto-Submitted and Precedence: bulk. Bots sending from a normal mailbox (a CI job on Gmail, say) have no such headers and need MUTE_SENDERS in config.env:

MUTE_SENDERS=ci-bot@example.com, notify.cloudflare.com

Entries match a full address or a bare domain.


Scheduling (Windows)

powershell -ExecutionPolicy Bypass -File .\scripts\register-tasks.ps1
Task Schedule Command
Digest daily 07:30 digest --send
Reminders weekdays 08:30 reminders --send
Poll every 15 min poll-once

Options: -DigestTime, -ReminderTime, -PollMinutes, -SkipPoll, -SkipReminders, -MailProfile <name> (tasks under \ZohoMail\<name>\). Logs land in the profile's state/logs/. Remove with .\scripts\unregister-tasks.ps1 (-MailProfile too, if you used it).

To keep the store fresh, add a sync task:

schtasks /create /tn "ZohoMail\Sync" /tr "cmd /c cd /d $PWD && python -m zohomail sync" /sc hourly

Cron equivalent:

30 7 * * *   cd /path/to/zoho-mail && python -m zohomail digest --send
0 * * * *    cd /path/to/zoho-mail && python -m zohomail sync

As a library

from zohomail.config import load_config
from zohomail.store import Store
from zohomail.imap_client import ZohoIMAP
from zohomail import sync, smtp_client

config = load_config()

with Store(config) as store:
    sync.sync(config, store, days=30, progress=print)

    for row in store.search("invoice", limit=10):
        print(row["date_utc"], row["from_email"], row["subject"])

    store.add_analysis(message_id=12, priority="high", summary="Needs a reply today")

with ZohoIMAP(config) as imap:
    for message in imap.recent("INBOX", hours=24, unread=True):
        print(message.sender, message.subject)

Layout

pyproject.toml     pip metadata; the wheel is the engine only
LICENSE            MIT
.env.example       same template as zohomail/templates/config.env
zohomail/
  profile.py       which profile is active, and where its files live
  config.py        config.env + environment loading, per-DC host derivation
  imap_client.py   IMAP4_SSL: search, fetch, flag, move, delete, retry on throttle
  smtp_client.py   SMTP_SSL / STARTTLS sending with attachments
  pop_client.py    POP3 bulk download with UIDL de-duplication
  message.py       header decoding, body extraction, attachment parts
  schema.sql       the entity model
  db.py            connection, migrations, FTS rebuild, vacuum
  store.py         persistence, threading, search, annotation layer
  sync.py          incremental IMAP -> SQLite
  productivity.py  action items, awaiting-reply detection, statistics
  digest.py        digest assembly, text and HTML rendering
  mcp_server.py    MCP server over stdio (18 tools)
  cli.py           live-mailbox commands
  cli_store.py     store-backed commands
  cli_profile.py   profile init / use / show / rules / migrate
  folders.py       folder selectors: paths, globs, exclusions
  templates/       config.env and rules.md copied by `profile init`
.claude/
  skills/          mail-triage, mail-search, mail-draft, mail-brief
  agents/          mail-triage-agent, mail-reply-drafter, mail-archivist
.mcp.json          registers the MCP server for this project
scripts/           register-tasks.ps1 / unregister-tasks.ps1 (clone only)

Personal files live in the profile, never in the repo (see Profiles).


Notes and limits

  • Zoho throttles IMAP at roughly 200 logins/day. Each command opens one connection; a sync of many folders reuses a single one. On repeated socket error: EOF the client retries twice with backoff, then tells you to back off — that error means throttling, not a bad password.
  • Zoho also drops long sessions mid-sync. Read commands reconnect and retry, so a backfill carries on; if a folder still fails, the rest sync and a re-run resumes where it stopped.
  • The first sync of a folder reaches back SYNC_DAYS (BULK_SYNC_DAYS for bulk folders); after that the UID cursor picks up everything new. Widen with --days N --full, which skips messages already stored.
  • A message moved or deleted on the server keeps its old row in the store; sync only adds and refreshes.
  • poll-once records a high-water UID on first run and reports nothing, so a fresh install does not replay two days of mail.
  • pop-download never deletes unless you pass --delete.
  • Sending only works from the account's own address — Zoho rejects unverified From. Use --from-name for the display name.
  • Attachment blobs are never garbage-collected. Deleting rows leaves the blobs; clear the profile's store/blobs/ and re-sync if you need the space back.

Security

  • The package is MIT licensed and contains no account, mailbox, or secret. Profiles live outside the repo, so passwords and mail cannot be committed by accident. The legacy in-repo .env, store/ and state/ are git-ignored too.
  • rules.md is the owner's instruction to the assistant. Only they should edit it.
  • App-specific passwords grant mail protocol access only, not account login, and are revocable from the page that issued them.
  • Email content is data, not instructions. Every skill and agent here is told that a message asking to forward a thread, reply with credentials, or wire money is a claim by a sender to be reported — never a command to act on. Keep that rule if you write more agents against this store.
  • The only tool that contacts another person is draft send / mail_draft_send, and it requires explicit approval every time.

Metadata

Release files for zohomail 1.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zohomail 1.1.1
File Size Uploaded Provenance Actions
zohomail-1.1.1.tar.gz 69.0 kB With provenance Details for zohomail-1.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for zohomail 1.1.1
File Interpreter ABI Platform Actions
zohomail-1.1.1-py3-none-any.whl Python 3 none any Details for zohomail-1.1.1-py3-none-any.whl

Total release size: 139.1 kB

Release files / zohomail-1.1.1.tar.gz

Download URL zohomail-1.1.1.tar.gz
Size 69.0 kB
Tags Source
SHA-256 checksum
How to use checksums
13759f74e29eb25e5ae2e4f88b6289e139048dce6a925a196c8f01201d723302
BLAKE2b-256 checksum
How to use checksums
be3bda4fd64d5e282fc9452e79abf3ebec186e04ecd655eaada0598dd96a9b83
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / zohomail-1.1.1-py3-none-any.whl

Download URL zohomail-1.1.1-py3-none-any.whl
Size 70.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b9e5956e6b0bd7ca65e3bf722bd34336b73c6670100684ea2886cfdeecc1a1b6
BLAKE2b-256 checksum
How to use checksums
b8764549d5f54dfda58a992c1d9fbcc17f46fcb2523610c1648a8dc482368bc5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Security for zohomail 1.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for zohomail-1.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page