zohomail 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zohomail-1.1.0/LICENSE +21 -0
- zohomail-1.1.0/PKG-INFO +487 -0
- zohomail-1.1.0/README.md +439 -0
- zohomail-1.1.0/pyproject.toml +45 -0
- zohomail-1.1.0/setup.cfg +4 -0
- zohomail-1.1.0/zohomail/__init__.py +6 -0
- zohomail-1.1.0/zohomail/__main__.py +4 -0
- zohomail-1.1.0/zohomail/cli.py +457 -0
- zohomail-1.1.0/zohomail/cli_profile.py +192 -0
- zohomail-1.1.0/zohomail/cli_store.py +508 -0
- zohomail-1.1.0/zohomail/config.py +173 -0
- zohomail-1.1.0/zohomail/db.py +52 -0
- zohomail-1.1.0/zohomail/digest.py +185 -0
- zohomail-1.1.0/zohomail/folders.py +49 -0
- zohomail-1.1.0/zohomail/imap_client.py +400 -0
- zohomail-1.1.0/zohomail/mcp_server.py +466 -0
- zohomail-1.1.0/zohomail/message.py +208 -0
- zohomail-1.1.0/zohomail/pop_client.py +101 -0
- zohomail-1.1.0/zohomail/productivity.py +287 -0
- zohomail-1.1.0/zohomail/profile.py +161 -0
- zohomail-1.1.0/zohomail/schema.sql +318 -0
- zohomail-1.1.0/zohomail/smtp_client.py +109 -0
- zohomail-1.1.0/zohomail/state.py +40 -0
- zohomail-1.1.0/zohomail/store.py +765 -0
- zohomail-1.1.0/zohomail/sync.py +234 -0
- zohomail-1.1.0/zohomail/templates/config.env +54 -0
- zohomail-1.1.0/zohomail/templates/rules.md +44 -0
- zohomail-1.1.0/zohomail.egg-info/PKG-INFO +487 -0
- zohomail-1.1.0/zohomail.egg-info/SOURCES.txt +30 -0
- zohomail-1.1.0/zohomail.egg-info/dependency_links.txt +1 -0
- zohomail-1.1.0/zohomail.egg-info/entry_points.txt +2 -0
- zohomail-1.1.0/zohomail.egg-info/top_level.txt +1 -0
zohomail-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 alphaolomi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
zohomail-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zohomail
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Local Zoho Mail mirror: IMAP sync into SQLite, a CLI, and an MCP server
|
|
5
|
+
Author-email: alphaolomi <10551599+alphaolomi@users.noreply.github.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 alphaolomi
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/alphaolomi/zoho-mail
|
|
29
|
+
Project-URL: Repository, https://github.com/alphaolomi/zoho-mail
|
|
30
|
+
Project-URL: Issues, https://github.com/alphaolomi/zoho-mail/issues
|
|
31
|
+
Project-URL: Changelog, https://github.com/alphaolomi/zoho-mail/releases
|
|
32
|
+
Keywords: zoho,email,imap,smtp,mcp
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Environment :: Console
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Communications :: Email
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# zohomail
|
|
50
|
+
|
|
51
|
+
An AI email client for Zoho Mail: **IMAP / POP3 / SMTP** access with an app-specific
|
|
52
|
+
password, a **local SQLite mirror** of the mailbox with full-text search and
|
|
53
|
+
deduplicated attachment storage, an **MCP server**, and **Claude Code skills and
|
|
54
|
+
subagents** that read, triage, and draft against it.
|
|
55
|
+
|
|
56
|
+
Standard library only: no third-party packages, no OAuth app, no API key.
|
|
57
|
+
MIT licensed. Mail, passwords and rules stay in a profile outside this
|
|
58
|
+
repository, so the tree itself can be shared.
|
|
59
|
+
|
|
60
|
+
Verified against a Zoho organisation account on the `zoho.com` data centre.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install -e . # from a checkout; or pip install .
|
|
64
|
+
zohomail doctor # prove connectivity
|
|
65
|
+
zohomail sync # mirror the last 90 days into SQLite
|
|
66
|
+
zohomail db stats # what's local now
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`python -m zohomail` is the same program. From this directory it works
|
|
70
|
+
without installing; `pip install` is what puts `zohomail` on `PATH` for
|
|
71
|
+
any other directory, and what a published install uses.
|
|
72
|
+
|
|
73
|
+
Then, in Claude Code: *"triage my inbox"*, *"what am I waiting on?"*,
|
|
74
|
+
*"draft a reply to the bank integration thread"*.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Setup
|
|
79
|
+
|
|
80
|
+
1. **Install.** From a checkout, `pip install -e .`. Once the repository is
|
|
81
|
+
public, `pip install git+https://github.com/alphaolomi/zoho-mail.git`
|
|
82
|
+
installs the engine without the Claude skills. The skills, agents and
|
|
83
|
+
`scripts/` stay with the clone; they are not part of the wheel.
|
|
84
|
+
2. **Generate an app-specific password** at
|
|
85
|
+
[accounts.zoho.com → Security → App Passwords](https://accounts.zoho.com/home#security/apppassword).
|
|
86
|
+
Not your account password; it bypasses 2FA for mail protocols only.
|
|
87
|
+
3. **Enable the protocols** in Zoho Mail → Settings → Mail Accounts → IMAP / POP Access.
|
|
88
|
+
4. **Create your profile** — your config, rules and mail data live outside the
|
|
89
|
+
repo, in `~/.zohomail/<name>/` (see [Profiles](#profiles)):
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
zohomail profile init alice --email you@yourdomain.com
|
|
93
|
+
zohomail profile use alice
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then fill in `~/.zohomail/alice/config.env`:
|
|
97
|
+
|
|
98
|
+
```ini
|
|
99
|
+
ZOHO_EMAIL=you@yourdomain.com
|
|
100
|
+
ZOHO_APP_PASSWORD=xxxxxxxxxxxx
|
|
101
|
+
ZOHO_DC=com # com | eu | in | au | jp | ca | sa | uk
|
|
102
|
+
ZOHO_HOST_STYLE=standard # standard -> imap.zoho.com | pro -> imappro.zoho.com
|
|
103
|
+
SYNC_DAYS=90
|
|
104
|
+
SYNC_FOLDERS=INBOX,Sent # names or globs: "*", "INBOX/*", "!INBOX/Spammy"
|
|
105
|
+
BULK_FOLDERS= # alert folders your Zoho filters fill (see below)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
5. **Write your rules** in `~/.zohomail/alice/rules.md`: who matters, what is
|
|
109
|
+
urgent, how replies should sound. The skills read it before they act.
|
|
110
|
+
6. **Check it** — `zohomail doctor` probes both host styles across all
|
|
111
|
+
three protocols and reports which authenticate.
|
|
112
|
+
|
|
113
|
+
Environment variables override `config.env`.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Profiles
|
|
118
|
+
|
|
119
|
+
The repo is the engine: code, schema, skills, agents and scripts, with nothing
|
|
120
|
+
personal in it. Each person, or each mailbox, gets a profile:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
~/.zohomail/<name>/
|
|
124
|
+
config.env account, app password, folders, bulk folders, mute list
|
|
125
|
+
rules.md their triage priorities, key people, drafting voice, hard limits
|
|
126
|
+
store/ mail.db and attachment blobs
|
|
127
|
+
state/ poll cursors and scheduler logs
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The active profile is, first match wins: `--profile NAME`, `ZOHOMAIL_HOME=<dir>`,
|
|
131
|
+
`ZOHOMAIL_PROFILE=<name>`, the one set by `profile use`, then a legacy `.env` in
|
|
132
|
+
the repo, then `default`. Set `ZOHOMAIL_PROFILES` to keep profiles somewhere
|
|
133
|
+
other than `~/.zohomail`.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m zohomail profile list # * marks the active one
|
|
137
|
+
python -m zohomail profile show # where its config, rules and store are
|
|
138
|
+
python -m zohomail profile rules # what the skills read
|
|
139
|
+
python -m zohomail --profile technical sync
|
|
140
|
+
python -m zohomail.mcp_server --profile technical
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Two people on two machines each run `profile init` once. Two mailboxes on one
|
|
144
|
+
machine (your own and a shared `technical@`, say) are two profiles; pick one
|
|
145
|
+
per command with `--profile`, or register scheduled tasks per profile with
|
|
146
|
+
`-MailProfile`.
|
|
147
|
+
|
|
148
|
+
**Upgrading from the in-repo layout.** Older installs kept `.env`, `store/` and
|
|
149
|
+
`state/` inside the repo. That still works, and one command moves it out:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
python -m zohomail profile migrate alice
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
It copies `.env` to `config.env`, moves `store/` and `state/`, adds a rules
|
|
156
|
+
template, and makes `alice` active. The repo `.env` is removed only after
|
|
157
|
+
everything else has moved; stop the MCP server first so the store is not open.
|
|
158
|
+
|
|
159
|
+
### Servers
|
|
160
|
+
|
|
161
|
+
| Protocol | Standard | Pro (some org accounts) | Port | Security |
|
|
162
|
+
|---|---|---|---|---|
|
|
163
|
+
| IMAP | `imap.zoho.com` | `imappro.zoho.com` | 993 | SSL |
|
|
164
|
+
| POP3 | `pop.zoho.com` | `poppro.zoho.com` | 995 | SSL |
|
|
165
|
+
| SMTP | `smtp.zoho.com` | `smtppro.zoho.com` | 465 | SSL |
|
|
166
|
+
|
|
167
|
+
`ZOHO_SMTP_PORT=587` switches to STARTTLS automatically.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## The local store
|
|
172
|
+
|
|
173
|
+
`python -m zohomail sync` mirrors IMAP into the profile's `store/mail.db`. Sync is incremental —
|
|
174
|
+
each folder keeps a UID cursor, and a UIDVALIDITY change invalidates it correctly.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python -m zohomail sync # configured folders, SYNC_DAYS window
|
|
178
|
+
python -m zohomail sync --days 365 --full # backfill a year
|
|
179
|
+
python -m zohomail sync --folders "INBOX,Sent,Newsletter"
|
|
180
|
+
python -m zohomail sync --folders "INBOX/*" # every folder under INBOX
|
|
181
|
+
python -m zohomail sync --folders "*" --max 500 # every folder, capped
|
|
182
|
+
python -m zohomail db stats # counts, range, per-folder cursors
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Folders and hierarchy
|
|
186
|
+
|
|
187
|
+
Folders are stored under their full server path (`INBOX/Sentry`), so the store
|
|
188
|
+
mirrors the hierarchy you see in Zoho. `SYNC_FOLDERS`, `--folders` and
|
|
189
|
+
`search --folder` all take the same selectors:
|
|
190
|
+
|
|
191
|
+
| Selector | Picks |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `INBOX/Sentry` | that folder (case does not matter) |
|
|
194
|
+
| `Sentry` | the one folder whose path ends `/Sentry`; an error if two do |
|
|
195
|
+
| `Sent`, `Spam`, `Trash`, `Drafts` | the server's special-use folder, whatever it is called |
|
|
196
|
+
| `INBOX/*` | every folder under INBOX, but not INBOX itself |
|
|
197
|
+
| `*` | every folder except Trash, Spam and Drafts (name those to include them) |
|
|
198
|
+
| `!INBOX/Wazuh` | removes a folder the other entries picked |
|
|
199
|
+
|
|
200
|
+
**Bulk folders.** Zoho filters often route alerts (Wazuh, Sentry, CloudWatch)
|
|
201
|
+
into their own folders. List those in `BULK_FOLDERS` and their messages are
|
|
202
|
+
marked automated, so `--no-bots`, the triage queue and briefs skip them. Header
|
|
203
|
+
heuristics miss senders such as UptimeRobot; the folder does not. A bulk
|
|
204
|
+
folder's first sync reaches back only `BULK_SYNC_DAYS` (default 30, `0` = the
|
|
205
|
+
normal window, `--bulk-days` per run). New alerts are always kept.
|
|
206
|
+
|
|
207
|
+
```ini
|
|
208
|
+
SYNC_FOLDERS=*
|
|
209
|
+
BULK_FOLDERS=INBOX/Wazuh,INBOX/AWS Notifications,INBOX/Sentry,INBOX/Grafana
|
|
210
|
+
BULK_SYNC_DAYS=30
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Entities
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
accounts ── folders ── messages ── message_addresses ── addresses
|
|
217
|
+
│
|
|
218
|
+
├── attachments (sha256-deduped blobs on disk)
|
|
219
|
+
├── message_refs (Message-ID graph for threading)
|
|
220
|
+
├── message_labels ── labels
|
|
221
|
+
├── analyses (what the AI concluded)
|
|
222
|
+
└── threads ── tasks, drafts
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- **Threading** stitches messages by `In-Reply-To`/`References` first, merging
|
|
226
|
+
threads when a late message joins two chains. Subject matching is only a
|
|
227
|
+
fallback, and only when a participant overlaps — so two unrelated "Re: Hello"
|
|
228
|
+
chains never merge.
|
|
229
|
+
- **Attachments** are content-addressed: bytes go to `store/blobs/<aa>/<sha256>`,
|
|
230
|
+
so the same signature logo across 40 messages is stored once. Inline parts are
|
|
231
|
+
flagged so you can filter for real documents. Files over `MAX_ATTACHMENT_MB`
|
|
232
|
+
are indexed without their bytes.
|
|
233
|
+
- **Full-text search** is FTS5 over subject, body, sender name and address, kept
|
|
234
|
+
current by triggers and ranked with bm25.
|
|
235
|
+
|
|
236
|
+
### Reading it
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
python -m zohomail search "invoice NOT draft" --no-bots
|
|
240
|
+
python -m zohomail search --sender priya --attachments
|
|
241
|
+
python -m zohomail show 12 # one message, full body
|
|
242
|
+
python -m zohomail thread 10 # the whole conversation
|
|
243
|
+
python -m zohomail threads --awaiting-reply # you wrote last, silence since
|
|
244
|
+
python -m zohomail attachments --name .pdf --extract ./out
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Every command takes `--json` for machine consumption.
|
|
248
|
+
|
|
249
|
+
FTS5 syntax: `invoice payment` (AND), `"purchase order"` (phrase),
|
|
250
|
+
`invoice OR receipt`, `deploy NOT staging`, `integrat*` (prefix).
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## The AI layer
|
|
255
|
+
|
|
256
|
+
The Python holds no model calls. **Claude Code does the reasoning**, and the store
|
|
257
|
+
is its memory — so conclusions survive across sessions and accumulate.
|
|
258
|
+
|
|
259
|
+
| Table | Holds |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `analyses` | priority, category, summary, action items, needs-reply, confidence |
|
|
262
|
+
| `labels` | topic/category tags applied to messages |
|
|
263
|
+
| `tasks` | extracted work with due dates and status |
|
|
264
|
+
| `drafts` | proposed replies awaiting your approval |
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
python -m zohomail queue # messages with no analysis yet
|
|
268
|
+
python -m zohomail annotate --message 12 --priority high --category partner-integration \
|
|
269
|
+
--summary "The bank needs the sandbox endpoints before they can test" \
|
|
270
|
+
--action "Send Priya the endpoint list" --needs-reply --label acme-bank
|
|
271
|
+
python -m zohomail task add --title "Send Acme Bank endpoints" --message 12 --due 2026-09-18
|
|
272
|
+
python -m zohomail task # open tasks by due date
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Skills
|
|
276
|
+
|
|
277
|
+
In `.claude/skills/`, available in any Claude Code session in this directory:
|
|
278
|
+
|
|
279
|
+
| Skill | Use |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `mail-triage` | Work the queue: judge, annotate, label, create tasks |
|
|
282
|
+
| `mail-search` | Find mail, answer questions from it, pull attachments |
|
|
283
|
+
| `mail-draft` | Draft a reply in your voice, saved for review |
|
|
284
|
+
| `mail-brief` | A read briefing: what needs you, what you're waiting on |
|
|
285
|
+
|
|
286
|
+
### Subagents
|
|
287
|
+
|
|
288
|
+
In `.claude/agents/` — dispatchable, each running in its own context:
|
|
289
|
+
|
|
290
|
+
| Agent | Does | Never |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `mail-triage-agent` | Syncs, triages the queue, files tasks, reports | sends, deletes, marks read |
|
|
293
|
+
| `mail-reply-drafter` | Reads the thread, matches your voice, drafts | sends |
|
|
294
|
+
| `mail-archivist` | Syncs/backfills, labels, finds noise and stale threads | sends, deletes server mail |
|
|
295
|
+
|
|
296
|
+
### MCP server
|
|
297
|
+
|
|
298
|
+
`.mcp.json` registers a stdio MCP server exposing 18 tools — `mail_search`,
|
|
299
|
+
`mail_message`, `mail_thread`, `mail_threads`, `mail_queue`, `mail_stats`,
|
|
300
|
+
`mail_attachments`, `mail_attachment_extract`, `mail_annotate`, `mail_task_add`,
|
|
301
|
+
`mail_tasks`, `mail_task_status`, `mail_draft_create`, `mail_drafts`,
|
|
302
|
+
`mail_draft_get`, `mail_draft_send`, `mail_sync`, `mail_flag`.
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
python -m zohomail.mcp_server # stdio; any MCP client can launch it
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Point Claude Desktop or another client at the same command to query this mailbox
|
|
309
|
+
from outside Claude Code.
|
|
310
|
+
|
|
311
|
+
### Sending is always gated
|
|
312
|
+
|
|
313
|
+
Drafts are written to the store, never sent automatically:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
python -m zohomail draft new --to priya@vendor.example --subject "RE: ..." \
|
|
317
|
+
--body-file reply.txt --thread 10 --reply-to 12 --rationale "unblocks testing"
|
|
318
|
+
python -m zohomail draft show 1
|
|
319
|
+
python -m zohomail draft send 1 --yes # --yes is required
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Without `--yes` the CLI refuses and exits 2. The MCP `mail_draft_send` tool
|
|
323
|
+
requires `confirm: true`. Both mean *the user said send it* — not the model's own
|
|
324
|
+
confidence. The agents are instructed never to send.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Live mailbox commands
|
|
329
|
+
|
|
330
|
+
These talk to Zoho directly, without the store:
|
|
331
|
+
|
|
332
|
+
| Command | What it does |
|
|
333
|
+
|---|---|
|
|
334
|
+
| `doctor` | Probe IMAP/SMTP/POP on both host styles |
|
|
335
|
+
| `folders` | List folders with unread counts |
|
|
336
|
+
| `list` | Search live IMAP: `--unread --since 7d --sender x --subject y` |
|
|
337
|
+
| `read UID` | Print a message; `--save-attachments DIR`, `--mark-read` |
|
|
338
|
+
| `send` | Send directly: `--to --subject --body --attach --cc --html-file` |
|
|
339
|
+
| `pop-download` | Bulk-fetch as `.eml`: `--dir --limit --delete` |
|
|
340
|
+
| `digest` | Mechanical digest: `--hours 24 --send --out digest.html` |
|
|
341
|
+
| `reminders` | Action items and awaiting-reply: `--send --include-automated` |
|
|
342
|
+
| `stats` | Volume, top senders, newsletters: `--days 7` |
|
|
343
|
+
| `poll-once` | Report mail since the last poll; `--on-new "cmd {subject}"` |
|
|
344
|
+
| `watch` | Poll in the foreground: `--interval 300` |
|
|
345
|
+
|
|
346
|
+
`digest` and `reminders` filter automated mail — newsletters (`List-Unsubscribe`),
|
|
347
|
+
`no-reply@` senders, `Auto-Submitted` and `Precedence: bulk`. Bots sending from a
|
|
348
|
+
normal mailbox (a CI job on Gmail, say) have no such headers and need
|
|
349
|
+
`MUTE_SENDERS` in `config.env`:
|
|
350
|
+
|
|
351
|
+
```ini
|
|
352
|
+
MUTE_SENDERS=ci-bot@example.com, notify.cloudflare.com
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Entries match a full address or a bare domain.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Scheduling (Windows)
|
|
360
|
+
|
|
361
|
+
```powershell
|
|
362
|
+
powershell -ExecutionPolicy Bypass -File .\scripts\register-tasks.ps1
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
| Task | Schedule | Command |
|
|
366
|
+
|---|---|---|
|
|
367
|
+
| `Digest` | daily 07:30 | `digest --send` |
|
|
368
|
+
| `Reminders` | weekdays 08:30 | `reminders --send` |
|
|
369
|
+
| `Poll` | every 15 min | `poll-once` |
|
|
370
|
+
|
|
371
|
+
Options: `-DigestTime`, `-ReminderTime`, `-PollMinutes`, `-SkipPoll`,
|
|
372
|
+
`-SkipReminders`, `-MailProfile <name>` (tasks under `\ZohoMail\<name>\`). Logs
|
|
373
|
+
land in the profile's `state/logs/`. Remove with
|
|
374
|
+
`.\scripts\unregister-tasks.ps1` (`-MailProfile` too, if you used it).
|
|
375
|
+
|
|
376
|
+
To keep the store fresh, add a sync task:
|
|
377
|
+
|
|
378
|
+
```powershell
|
|
379
|
+
schtasks /create /tn "ZohoMail\Sync" /tr "cmd /c cd /d $PWD && python -m zohomail sync" /sc hourly
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Cron equivalent:
|
|
383
|
+
|
|
384
|
+
```cron
|
|
385
|
+
30 7 * * * cd /path/to/zoho-mail && python -m zohomail digest --send
|
|
386
|
+
0 * * * * cd /path/to/zoho-mail && python -m zohomail sync
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## As a library
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
from zohomail.config import load_config
|
|
395
|
+
from zohomail.store import Store
|
|
396
|
+
from zohomail.imap_client import ZohoIMAP
|
|
397
|
+
from zohomail import sync, smtp_client
|
|
398
|
+
|
|
399
|
+
config = load_config()
|
|
400
|
+
|
|
401
|
+
with Store(config) as store:
|
|
402
|
+
sync.sync(config, store, days=30, progress=print)
|
|
403
|
+
|
|
404
|
+
for row in store.search("invoice", limit=10):
|
|
405
|
+
print(row["date_utc"], row["from_email"], row["subject"])
|
|
406
|
+
|
|
407
|
+
store.add_analysis(message_id=12, priority="high", summary="Needs a reply today")
|
|
408
|
+
|
|
409
|
+
with ZohoIMAP(config) as imap:
|
|
410
|
+
for message in imap.recent("INBOX", hours=24, unread=True):
|
|
411
|
+
print(message.sender, message.subject)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Layout
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
pyproject.toml pip metadata; the wheel is the engine only
|
|
420
|
+
LICENSE MIT
|
|
421
|
+
.env.example same template as zohomail/templates/config.env
|
|
422
|
+
zohomail/
|
|
423
|
+
profile.py which profile is active, and where its files live
|
|
424
|
+
config.py config.env + environment loading, per-DC host derivation
|
|
425
|
+
imap_client.py IMAP4_SSL: search, fetch, flag, move, delete, retry on throttle
|
|
426
|
+
smtp_client.py SMTP_SSL / STARTTLS sending with attachments
|
|
427
|
+
pop_client.py POP3 bulk download with UIDL de-duplication
|
|
428
|
+
message.py header decoding, body extraction, attachment parts
|
|
429
|
+
schema.sql the entity model
|
|
430
|
+
db.py connection, migrations, FTS rebuild, vacuum
|
|
431
|
+
store.py persistence, threading, search, annotation layer
|
|
432
|
+
sync.py incremental IMAP -> SQLite
|
|
433
|
+
productivity.py action items, awaiting-reply detection, statistics
|
|
434
|
+
digest.py digest assembly, text and HTML rendering
|
|
435
|
+
mcp_server.py MCP server over stdio (18 tools)
|
|
436
|
+
cli.py live-mailbox commands
|
|
437
|
+
cli_store.py store-backed commands
|
|
438
|
+
cli_profile.py profile init / use / show / rules / migrate
|
|
439
|
+
folders.py folder selectors: paths, globs, exclusions
|
|
440
|
+
templates/ config.env and rules.md copied by `profile init`
|
|
441
|
+
.claude/
|
|
442
|
+
skills/ mail-triage, mail-search, mail-draft, mail-brief
|
|
443
|
+
agents/ mail-triage-agent, mail-reply-drafter, mail-archivist
|
|
444
|
+
.mcp.json registers the MCP server for this project
|
|
445
|
+
scripts/ register-tasks.ps1 / unregister-tasks.ps1 (clone only)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Personal files live in the profile, never in the repo (see [Profiles](#profiles)).
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Notes and limits
|
|
453
|
+
|
|
454
|
+
- **Zoho throttles IMAP** at roughly 200 logins/day. Each command opens one
|
|
455
|
+
connection; a sync of many folders reuses a single one. On repeated
|
|
456
|
+
`socket error: EOF` the client retries twice with backoff, then tells you to
|
|
457
|
+
back off — that error means throttling, not a bad password.
|
|
458
|
+
- Zoho also drops long sessions mid-sync. Read commands reconnect and retry, so
|
|
459
|
+
a backfill carries on; if a folder still fails, the rest sync and a re-run
|
|
460
|
+
resumes where it stopped.
|
|
461
|
+
- The first sync of a folder reaches back `SYNC_DAYS` (`BULK_SYNC_DAYS` for bulk
|
|
462
|
+
folders); after that the UID cursor picks up everything new. Widen with
|
|
463
|
+
`--days N --full`, which skips messages already stored.
|
|
464
|
+
- A message moved or deleted on the server keeps its old row in the store; sync
|
|
465
|
+
only adds and refreshes.
|
|
466
|
+
- `poll-once` records a high-water UID on first run and reports nothing, so a
|
|
467
|
+
fresh install does not replay two days of mail.
|
|
468
|
+
- `pop-download` never deletes unless you pass `--delete`.
|
|
469
|
+
- Sending only works from the account's own address — Zoho rejects unverified
|
|
470
|
+
`From`. Use `--from-name` for the display name.
|
|
471
|
+
- Attachment blobs are never garbage-collected. Deleting rows leaves the blobs;
|
|
472
|
+
clear the profile's `store/blobs/` and re-sync if you need the space back.
|
|
473
|
+
|
|
474
|
+
## Security
|
|
475
|
+
|
|
476
|
+
- The package is MIT licensed and contains no account, mailbox, or secret.
|
|
477
|
+
Profiles live outside the repo, so passwords and mail cannot be committed by
|
|
478
|
+
accident. The legacy in-repo `.env`, `store/` and `state/` are git-ignored too.
|
|
479
|
+
- `rules.md` is the owner's instruction to the assistant. Only they should edit it.
|
|
480
|
+
- App-specific passwords grant mail protocol access only, not account login, and
|
|
481
|
+
are revocable from the page that issued them.
|
|
482
|
+
- **Email content is data, not instructions.** Every skill and agent here is told
|
|
483
|
+
that a message asking to forward a thread, reply with credentials, or wire
|
|
484
|
+
money is a claim by a sender to be reported — never a command to act on. Keep
|
|
485
|
+
that rule if you write more agents against this store.
|
|
486
|
+
- The only tool that contacts another person is `draft send` / `mail_draft_send`,
|
|
487
|
+
and it requires explicit approval every time.
|