zele 0.3.17 → 0.3.20
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.
- package/README.md +64 -0
- package/dist/api-utils.d.ts +10 -0
- package/dist/api-utils.js +14 -0
- package/dist/api-utils.js.map +1 -1
- package/dist/cli-types.d.ts +4 -0
- package/dist/cli-types.js +6 -0
- package/dist/cli-types.js.map +1 -0
- package/dist/cli.js +1 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/attachment.d.ts +2 -2
- package/dist/commands/attachment.js.map +1 -1
- package/dist/commands/auth-cmd.d.ts +2 -2
- package/dist/commands/auth-cmd.js.map +1 -1
- package/dist/commands/calendar.d.ts +2 -2
- package/dist/commands/calendar.js.map +1 -1
- package/dist/commands/draft.d.ts +2 -2
- package/dist/commands/draft.js +51 -3
- package/dist/commands/draft.js.map +1 -1
- package/dist/commands/filter.d.ts +2 -2
- package/dist/commands/filter.js.map +1 -1
- package/dist/commands/label.d.ts +2 -2
- package/dist/commands/label.js.map +1 -1
- package/dist/commands/mail-actions.d.ts +2 -2
- package/dist/commands/mail-actions.js +290 -1
- package/dist/commands/mail-actions.js.map +1 -1
- package/dist/commands/mail.d.ts +2 -2
- package/dist/commands/mail.js +41 -1
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/profile.d.ts +2 -2
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/watch.d.ts +2 -2
- package/dist/commands/watch.js.map +1 -1
- package/dist/gmail-client.d.ts +59 -3
- package/dist/gmail-client.js +119 -5
- package/dist/gmail-client.js.map +1 -1
- package/dist/imap-smtp-client.d.ts +75 -4
- package/dist/imap-smtp-client.js +131 -7
- package/dist/imap-smtp-client.js.map +1 -1
- package/dist/unsubscribe.d.ts +76 -0
- package/dist/unsubscribe.js +224 -0
- package/dist/unsubscribe.js.map +1 -0
- package/package.json +2 -2
- package/skills/zele/SKILL.md +26 -125
- package/src/api-utils.ts +14 -0
- package/src/cli-types.ts +8 -0
- package/src/cli.ts +2 -7
- package/src/commands/attachment.ts +2 -2
- package/src/commands/auth-cmd.ts +2 -2
- package/src/commands/calendar.ts +2 -2
- package/src/commands/draft.ts +60 -5
- package/src/commands/filter.ts +2 -2
- package/src/commands/label.ts +2 -2
- package/src/commands/mail-actions.ts +315 -4
- package/src/commands/mail.ts +45 -3
- package/src/commands/profile.ts +2 -2
- package/src/commands/watch.ts +2 -2
- package/src/gmail-client.ts +193 -6
- package/src/imap-smtp-client.ts +186 -7
- package/src/unsubscribe.test.ts +487 -0
- package/src/unsubscribe.ts +255 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Unsubscribe header parsing and mechanism planning.
|
|
2
|
+
// Implements RFC 2369 (List-Unsubscribe) + RFC 8058 (List-Unsubscribe-Post
|
|
3
|
+
// One-Click). Pure functions only — no network, no client access.
|
|
4
|
+
//
|
|
5
|
+
// RFC 2369 says List-Unsubscribe contains one or more angle-bracket-enclosed
|
|
6
|
+
// URIs, comma-separated. Each URI is either mailto: (send an email) or
|
|
7
|
+
// http(s): (a landing page).
|
|
8
|
+
//
|
|
9
|
+
// RFC 8058 adds one-click: when both List-Unsubscribe and
|
|
10
|
+
// List-Unsubscribe-Post (with the single value "List-Unsubscribe=One-Click")
|
|
11
|
+
// are present, a client can POST `List-Unsubscribe=One-Click` to the https
|
|
12
|
+
// URL with no cookies, no auth, and no redirects allowed.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Parsing
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Split a List-Unsubscribe header into its `<URI>` entries. Commas only
|
|
18
|
+
* separate entries when they are outside angle brackets; whitespace and
|
|
19
|
+
* line folding inside the header are tolerated per RFC 2369 §2.
|
|
20
|
+
*/
|
|
21
|
+
export function parseListUnsubscribeEntries(header) {
|
|
22
|
+
if (!header)
|
|
23
|
+
return [];
|
|
24
|
+
const entries = [];
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let current = '';
|
|
27
|
+
for (const ch of header) {
|
|
28
|
+
if (ch === '<') {
|
|
29
|
+
depth++;
|
|
30
|
+
current += ch;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (ch === '>') {
|
|
34
|
+
depth = Math.max(0, depth - 1);
|
|
35
|
+
current += ch;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === ',' && depth === 0) {
|
|
39
|
+
entries.push(current);
|
|
40
|
+
current = '';
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
current += ch;
|
|
44
|
+
}
|
|
45
|
+
if (current.trim().length > 0)
|
|
46
|
+
entries.push(current);
|
|
47
|
+
// Extract the URI inside each angle bracket pair. Anything outside is ignored
|
|
48
|
+
// (comments, trailing text) per RFC 2369 §2 guideline 2.
|
|
49
|
+
const result = [];
|
|
50
|
+
for (const raw of entries) {
|
|
51
|
+
const match = raw.match(/<([^>]*)>/);
|
|
52
|
+
if (!match)
|
|
53
|
+
continue;
|
|
54
|
+
const uri = match[1].replace(/\s+/g, '').trim();
|
|
55
|
+
if (uri.length > 0)
|
|
56
|
+
result.push(uri);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse a mailto: URI into its target address and optional subject/body/cc.
|
|
62
|
+
* Returns null if the URI is not a valid mailto.
|
|
63
|
+
*
|
|
64
|
+
* Supports percent-encoding per RFC 6068. Note: `mailto:` is NOT
|
|
65
|
+
* application/x-www-form-urlencoded, so `+` is preserved literally (this
|
|
66
|
+
* matters for plus-addressing like `foo+tag@example.com`).
|
|
67
|
+
*/
|
|
68
|
+
export function parseMailto(uri) {
|
|
69
|
+
if (!/^mailto:/i.test(uri))
|
|
70
|
+
return null;
|
|
71
|
+
const afterScheme = uri.slice('mailto:'.length);
|
|
72
|
+
const qIndex = afterScheme.indexOf('?');
|
|
73
|
+
const rawTo = qIndex === -1 ? afterScheme : afterScheme.slice(0, qIndex);
|
|
74
|
+
const query = qIndex === -1 ? '' : afterScheme.slice(qIndex + 1);
|
|
75
|
+
const decode = (s) => {
|
|
76
|
+
try {
|
|
77
|
+
// RFC 6068: only percent-decode. Do NOT rewrite `+` → space.
|
|
78
|
+
return decodeURIComponent(s);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
// Strip CRLF (defense-in-depth against header injection through mailto
|
|
85
|
+
// fields that get passed to SMTP/mimetext). Only body is allowed to keep
|
|
86
|
+
// newlines because it becomes message content.
|
|
87
|
+
const sanitizeHeader = (s) => s.replace(/[\r\n]/g, '').trim();
|
|
88
|
+
const to = sanitizeHeader(decode(rawTo));
|
|
89
|
+
if (!to)
|
|
90
|
+
return null;
|
|
91
|
+
const spec = { to };
|
|
92
|
+
if (!query)
|
|
93
|
+
return spec;
|
|
94
|
+
const cc = [];
|
|
95
|
+
for (const pair of query.split('&')) {
|
|
96
|
+
if (!pair)
|
|
97
|
+
continue;
|
|
98
|
+
const eq = pair.indexOf('=');
|
|
99
|
+
const key = (eq === -1 ? pair : pair.slice(0, eq)).toLowerCase();
|
|
100
|
+
const value = eq === -1 ? '' : decode(pair.slice(eq + 1));
|
|
101
|
+
if (key === 'subject')
|
|
102
|
+
spec.subject = sanitizeHeader(value);
|
|
103
|
+
else if (key === 'body')
|
|
104
|
+
spec.body = value;
|
|
105
|
+
else if (key === 'cc') {
|
|
106
|
+
for (const addr of value.split(',')) {
|
|
107
|
+
const trimmed = sanitizeHeader(addr);
|
|
108
|
+
if (trimmed)
|
|
109
|
+
cc.push(trimmed);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (cc.length > 0)
|
|
114
|
+
spec.cc = cc;
|
|
115
|
+
return spec;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parse a List-Unsubscribe-Post header value. Per RFC 8058 §5 the only legal
|
|
119
|
+
* value is exactly `List-Unsubscribe=One-Click`. Whitespace-tolerant.
|
|
120
|
+
*/
|
|
121
|
+
export function parseListUnsubscribePost(header) {
|
|
122
|
+
if (!header)
|
|
123
|
+
return false;
|
|
124
|
+
return header.replace(/\s+/g, '').toLowerCase() === 'list-unsubscribe=one-click';
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Lightweight check for whether a message has any standardized unsubscribe
|
|
128
|
+
* mechanism advertised. Used by list views that want to surface a
|
|
129
|
+
* `can_unsubscribe` boolean without building a full plan. Returns true when
|
|
130
|
+
* List-Unsubscribe contains at least one valid mailto: or http(s): entry.
|
|
131
|
+
*/
|
|
132
|
+
export function hasUnsubscribeMechanism(listUnsubscribe) {
|
|
133
|
+
if (!listUnsubscribe)
|
|
134
|
+
return false;
|
|
135
|
+
const entries = parseListUnsubscribeEntries(listUnsubscribe);
|
|
136
|
+
return entries.some((e) => /^(mailto|https?):/i.test(e));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Check whether a message advertises RFC 8058 one-click unsubscribe: both
|
|
140
|
+
* List-Unsubscribe-Post=One-Click and at least one https: URL in
|
|
141
|
+
* List-Unsubscribe. Does not check DKIM — the executor is responsible for
|
|
142
|
+
* that gating at the point of actually POSTing.
|
|
143
|
+
*/
|
|
144
|
+
export function hasOneClickUnsubscribe(listUnsubscribe, listUnsubscribePost) {
|
|
145
|
+
if (!parseListUnsubscribePost(listUnsubscribePost ?? undefined))
|
|
146
|
+
return false;
|
|
147
|
+
if (!listUnsubscribe)
|
|
148
|
+
return false;
|
|
149
|
+
return parseListUnsubscribeEntries(listUnsubscribe).some((e) => /^https:/i.test(e));
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Planning
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* Build an unsubscribe plan from the raw headers + DKIM authenticity flag.
|
|
156
|
+
*
|
|
157
|
+
* Preference order:
|
|
158
|
+
* 1. RFC 8058 one-click (List-Unsubscribe-Post present + https URL).
|
|
159
|
+
* Emitted first because it is the spec-preferred programmatic path.
|
|
160
|
+
* 2. The remaining entries in sender-declared order (RFC 2369 §2 says
|
|
161
|
+
* clients should prefer left-to-right order). Each http(s): entry
|
|
162
|
+
* becomes a `url` mechanism, each mailto: entry becomes a `mailto`.
|
|
163
|
+
*/
|
|
164
|
+
export function planUnsubscribe({ listUnsubscribe, listUnsubscribePost, dkimAuthentic, }) {
|
|
165
|
+
const warnings = [];
|
|
166
|
+
const mechanisms = [];
|
|
167
|
+
const entries = listUnsubscribe ? parseListUnsubscribeEntries(listUnsubscribe) : [];
|
|
168
|
+
const postOneClick = parseListUnsubscribePost(listUnsubscribePost);
|
|
169
|
+
// First pass: RFC 8058 one-click extraction. Any https entries become
|
|
170
|
+
// one-click candidates if the Post header is present.
|
|
171
|
+
const oneClickUrls = new Set();
|
|
172
|
+
let hasOneClick = false;
|
|
173
|
+
if (postOneClick) {
|
|
174
|
+
const httpsEntries = entries.filter((e) => /^https:/i.test(e));
|
|
175
|
+
if (httpsEntries.length > 0) {
|
|
176
|
+
hasOneClick = true;
|
|
177
|
+
for (const url of httpsEntries) {
|
|
178
|
+
mechanisms.push({ kind: 'one-click', url });
|
|
179
|
+
oneClickUrls.add(url);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (entries.some((e) => /^http:/i.test(e))) {
|
|
183
|
+
warnings.push('List-Unsubscribe-Post is present but no https URL (only http); RFC 8058 requires https');
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
warnings.push('List-Unsubscribe-Post is present but no http(s) URL to POST to');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Second pass: legacy fallbacks in sender-declared order (RFC 2369 §2
|
|
190
|
+
// left-to-right preference). http(s) URLs already claimed by one-click
|
|
191
|
+
// are skipped so we don't emit duplicate mechanisms.
|
|
192
|
+
for (const uri of entries) {
|
|
193
|
+
if (/^mailto:/i.test(uri)) {
|
|
194
|
+
const mailto = parseMailto(uri);
|
|
195
|
+
if (mailto)
|
|
196
|
+
mechanisms.push({ kind: 'mailto', mailto });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (/^https?:/i.test(uri)) {
|
|
200
|
+
if (oneClickUrls.has(uri))
|
|
201
|
+
continue;
|
|
202
|
+
mechanisms.push({ kind: 'url', url: uri });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// DKIM safety notes for one-click. RFC 8058 §4 says the message SHOULD have
|
|
207
|
+
// a valid DKIM signature covering both headers; we approximate with a
|
|
208
|
+
// DKIM=pass verdict from the receiving MTA.
|
|
209
|
+
if (hasOneClick) {
|
|
210
|
+
if (dkimAuthentic === false) {
|
|
211
|
+
warnings.push('DKIM did not pass; one-click may be spoofed by an attacker');
|
|
212
|
+
}
|
|
213
|
+
else if (dkimAuthentic === null) {
|
|
214
|
+
warnings.push('DKIM status unknown (no authentication info on this message)');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
mechanisms,
|
|
219
|
+
hasOneClick,
|
|
220
|
+
dkimAuthentic,
|
|
221
|
+
warnings,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=unsubscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unsubscribe.js","sourceRoot":"","sources":["../src/unsubscribe.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zele",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.20",
|
|
4
4
|
"description": "Email & Calendar CLI — Gmail, IMAP/SMTP, Google Calendar from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"email-reply-parser": "^2.3.5",
|
|
36
36
|
"errore": "^0.11.0",
|
|
37
37
|
"fkill": "^10.0.3",
|
|
38
|
-
"goke": "^6.
|
|
38
|
+
"goke": "^6.6.0",
|
|
39
39
|
"google-auth-library": "^10.5.0",
|
|
40
40
|
"imapflow": "^1.2.18",
|
|
41
41
|
"js-yaml": "^4.1.1",
|
package/skills/zele/SKILL.md
CHANGED
|
@@ -1,141 +1,42 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: zele
|
|
3
3
|
description: >
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
zele is a multi-account email and calendar CLI for Gmail, IMAP/SMTP
|
|
5
|
+
(Fastmail, Outlook, any provider), and Google Calendar. It reads,
|
|
6
|
+
searches, sends, replies, forwards, archives, stars, and trashes emails,
|
|
7
|
+
manages drafts, labels, attachments, and Gmail filters, and creates,
|
|
8
|
+
updates, and deletes calendar events with RSVP and free/busy support.
|
|
9
|
+
Output is YAML so commands can be piped through yq and xargs. ALWAYS
|
|
10
|
+
load this skill when the user asks to check email, read/send messages,
|
|
11
|
+
reply or forward, archive or trash threads, manage drafts or labels,
|
|
12
|
+
download attachments, schedule meetings, check their calendar, RSVP
|
|
13
|
+
to events, or when they run any `zele` command. Load it before writing
|
|
14
|
+
any code or shell commands that touch zele so you know the correct
|
|
15
|
+
subcommand structure, the Google vs IMAP feature matrix, the headless
|
|
16
|
+
login flow, and the agent-specific rules.
|
|
9
17
|
---
|
|
10
18
|
|
|
11
|
-
# zele
|
|
19
|
+
# zele
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Setup
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
# install (requires bun)
|
|
19
|
-
bun install -g zele
|
|
20
|
-
|
|
21
|
-
# show connected accounts
|
|
22
|
-
zele whoami
|
|
23
|
-
|
|
24
|
-
# authenticate with Google (opens browser, supports multiple accounts)
|
|
25
|
-
zele login
|
|
26
|
-
|
|
27
|
-
# authenticate with IMAP/SMTP (non-interactive, designed for agents)
|
|
28
|
-
zele login imap \
|
|
29
|
-
--email you@fastmail.com \
|
|
30
|
-
--imap-host imap.fastmail.com --imap-port 993 \
|
|
31
|
-
--smtp-host smtp.fastmail.com --smtp-port 465 \
|
|
32
|
-
--password "your-app-password"
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
**IMAP/SMTP login options:** `--imap-user` / `--smtp-user` if the login username differs from email. Omit `--smtp-host` for read-only (no sending). Use `--imap-password` / `--smtp-password` for separate credentials.
|
|
36
|
-
|
|
37
|
-
**Remote/headless Google login:** `zele login` is interactive — it prints an authorization URL and waits for a redirect URL to be pasted back. In agent/headless environments, run it inside tmux so the process persists:
|
|
21
|
+
Every time you use zele, you MUST fetch the latest README:
|
|
38
22
|
|
|
39
23
|
```bash
|
|
40
|
-
#
|
|
41
|
-
tmux new-session -d -s zele-login 'zele login'
|
|
42
|
-
|
|
43
|
-
# read the authorization URL from tmux output
|
|
44
|
-
tmux capture-pane -t zele-login -p
|
|
45
|
-
|
|
46
|
-
# after the user completes consent in their browser, paste the redirect URL
|
|
47
|
-
tmux send-keys -t zele-login 'http://localhost:...?code=...' Enter
|
|
48
|
-
|
|
49
|
-
# verify login succeeded
|
|
50
|
-
tmux capture-pane -t zele-login -p
|
|
51
|
-
tmux kill-session -t zele-login
|
|
24
|
+
curl -s https://raw.githubusercontent.com/remorses/zele/main/README.md # NEVER pipe to head/tail, read the full output
|
|
52
25
|
```
|
|
53
26
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
**Always run `zele --help` before using.** The help output is the source of truth for all commands, options, and syntax. Run `zele <command> --help` for subcommand details (e.g. `zele mail send --help`). NEVER use head to truncate the output. read it fully.
|
|
57
|
-
|
|
58
|
-
Running `zele` with no subcommand launches a human-friendly TUI for browsing email. **Agents should not use the TUI** — always use the CLI subcommands (`zele mail list`, `zele cal events`, etc.) which output structured YAML.
|
|
59
|
-
|
|
60
|
-
## Capabilities
|
|
61
|
-
|
|
62
|
-
- **Mail:** list, search, read, send, reply, forward, star, archive, trash, watch for new emails (Google + IMAP)
|
|
63
|
-
- **Drafts:** list, create, get, send, delete (Google + IMAP)
|
|
64
|
-
- **Attachments:** list per thread, download (Google + IMAP)
|
|
65
|
-
- **Labels:** list, create, delete, unread counts (Google only)
|
|
66
|
-
- **Filters:** list server-side filters (Google only)
|
|
67
|
-
- **Calendar:** list calendars, list/search events, create/update/delete events, RSVP, free/busy (Google only)
|
|
68
|
-
- **Multi-account:** all commands support `--account <email>` to filter; list/search merge across all account types
|
|
69
|
-
|
|
70
|
-
## Account discovery
|
|
71
|
-
|
|
72
|
-
When the user asks to check emails **for a specific account** (e.g. "check my work email", "what's new on my personal Gmail?"), always run `zele whoami` first to list the connected accounts and find the exact email address to pass to `--account`. Never guess the email — use the output of `zele whoami` to pick the right one. The output also shows account type (`google` or `imap_smtp`) and capabilities.
|
|
27
|
+
Then run the CLI help once — it already includes every subcommand, option, and flag:
|
|
73
28
|
|
|
74
29
|
```bash
|
|
75
|
-
#
|
|
76
|
-
zele whoami
|
|
77
|
-
|
|
78
|
-
# then use the email from the output
|
|
79
|
-
zele mail list --account user@work.com
|
|
30
|
+
zele --help # NEVER pipe to head/tail, read the full output
|
|
80
31
|
```
|
|
81
32
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
These commands only work with Google accounts. IMAP/SMTP accounts show a helpful error:
|
|
33
|
+
The README and `zele --help` output are the source of truth for commands, options, flags, the Google vs IMAP feature matrix, search operators, and the headless login flow.
|
|
85
34
|
|
|
86
|
-
|
|
87
|
-
- `zele mail label` — adding/removing labels
|
|
88
|
-
- `zele mail filter list` — server-side Gmail filters
|
|
89
|
-
- `zele cal *` — calendar requires Google OAuth
|
|
90
|
-
- `zele profile` — shows limited info for IMAP (email only, no message counts)
|
|
35
|
+
## Rules
|
|
91
36
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Unsupported on IMAP: `cc:`, `-` (negate), `label:`, `in:`, `filename:`, `size:`/`larger:`/`smaller:`, `OR`, `{ }`.
|
|
99
|
-
|
|
100
|
-
## Examples
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
# list inbox
|
|
104
|
-
zele mail list
|
|
105
|
-
|
|
106
|
-
# list only unread emails
|
|
107
|
-
zele mail list --filter "is:unread"
|
|
108
|
-
|
|
109
|
-
# list emails from last 7 days (works for both Google and IMAP)
|
|
110
|
-
zele mail list --filter "newer_than:7d"
|
|
111
|
-
|
|
112
|
-
# combine filter with folder
|
|
113
|
-
zele mail list --filter "from:github" --folder sent
|
|
114
|
-
|
|
115
|
-
# search mail
|
|
116
|
-
zele mail search "from:github subject:review"
|
|
117
|
-
|
|
118
|
-
# read a thread (thread IDs come from list/search output)
|
|
119
|
-
zele mail read <threadId>
|
|
120
|
-
|
|
121
|
-
# send an email with attachment
|
|
122
|
-
zele mail send --to alice@example.com --subject "Report" --body "See attached" --attach report.pdf
|
|
123
|
-
|
|
124
|
-
# reply all
|
|
125
|
-
zele mail reply <threadId> --body "Thanks!" --all
|
|
126
|
-
|
|
127
|
-
# watch inbox for new mail (polls every 15s)
|
|
128
|
-
zele mail watch
|
|
129
|
-
|
|
130
|
-
# add an IMAP account (non-interactive, for agents)
|
|
131
|
-
zele login imap --email you@fastmail.com --imap-host imap.fastmail.com --smtp-host smtp.fastmail.com --password "app-pass"
|
|
132
|
-
|
|
133
|
-
# today's calendar events (Google only)
|
|
134
|
-
zele cal events --today --all
|
|
135
|
-
|
|
136
|
-
# create a meeting with Google Meet (Google only)
|
|
137
|
-
zele cal create --summary "Standup" --from tomorrow --to +30m --meet --attendees bob@example.com
|
|
138
|
-
|
|
139
|
-
# list Gmail filters (Google only)
|
|
140
|
-
zele mail filter list
|
|
141
|
-
```
|
|
37
|
+
1. **Never use the TUI.** Running `zele` with no subcommand launches a human-facing TUI. Agents must use the CLI subcommands (`zele mail list`, `zele cal events`, etc.) which output structured YAML.
|
|
38
|
+
2. **Always run `zele whoami` first** when the user asks to operate on a specific account. Pick the exact email from the output and pass it with `--account`. Never guess account emails.
|
|
39
|
+
3. **Never truncate `--help` or README output** with `head`, `tail`, `sed`, `awk`, or `less`. Critical rules are spread throughout. Read them in full.
|
|
40
|
+
4. **Parse YAML output with `yq`**, not regex. Pipe IDs through `xargs` for bulk actions: `zele mail list --filter "is:unread" | yq '.[].id' | xargs zele mail archive`.
|
|
41
|
+
5. **Google-only features** (labels, Gmail filters, `zele cal *`, full profile) fail on IMAP accounts with a clear error. Check `zele whoami` output for account type before using them.
|
|
42
|
+
6. **Headless Google login** requires a tmux wrapper because `zele login` is interactive. See the README "Remote / headless login" section for the exact pattern.
|
package/src/api-utils.ts
CHANGED
|
@@ -126,6 +126,20 @@ export class UnsupportedError extends errore.createTaggedError({
|
|
|
126
126
|
message: '$feature is not available for $accountType accounts. $hint',
|
|
127
127
|
}) {}
|
|
128
128
|
|
|
129
|
+
/** Returned when a thread has no List-Unsubscribe header (RFC 2369) so no
|
|
130
|
+
* standardized unsubscribe mechanism is available. */
|
|
131
|
+
export class UnsubscribeUnavailableError extends errore.createTaggedError({
|
|
132
|
+
name: 'UnsubscribeUnavailableError',
|
|
133
|
+
message: 'No List-Unsubscribe header on thread $threadId',
|
|
134
|
+
}) {}
|
|
135
|
+
|
|
136
|
+
/** Returned when the unsubscribe attempt itself failed (HTTP error, SMTP
|
|
137
|
+
* send failure, redirect returned when RFC 8058 forbids it, etc.). */
|
|
138
|
+
export class UnsubscribeFailedError extends errore.createTaggedError({
|
|
139
|
+
name: 'UnsubscribeFailedError',
|
|
140
|
+
message: 'Unsubscribe via $mechanism failed: $reason',
|
|
141
|
+
}) {}
|
|
142
|
+
|
|
129
143
|
/** Detect auth-like errors from underlying libraries (tsdav string errors, googleapis structured errors).
|
|
130
144
|
* Used inside clients to decide whether to return an AuthError.
|
|
131
145
|
* NOTE: String matching here is intentional — this is the boundary layer that converts
|
package/src/cli-types.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Shared goke type for the zele CLI, including the global options declared
|
|
2
|
+
// on the root `goke('zele')` instance. Living in its own file avoids the
|
|
3
|
+
// circular import that would arise from command modules importing from
|
|
4
|
+
// `./cli.js` (which itself imports every command module).
|
|
5
|
+
|
|
6
|
+
import type { Goke } from 'goke'
|
|
7
|
+
|
|
8
|
+
export type ZeleCli = Goke<{ account?: string[] }>
|
package/src/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { goke } from 'goke'
|
|
|
9
9
|
import { z } from 'zod'
|
|
10
10
|
import React from 'react'
|
|
11
11
|
import { listAccounts, login } from './auth.js'
|
|
12
|
+
import type { ZeleCli } from './cli-types.js'
|
|
12
13
|
import { registerAuthCommands } from './commands/auth-cmd.js'
|
|
13
14
|
import { registerMailCommands } from './commands/mail.js'
|
|
14
15
|
import { registerMailActionCommands } from './commands/mail-actions.js'
|
|
@@ -21,13 +22,7 @@ import { registerWatchCommands } from './commands/watch.js'
|
|
|
21
22
|
import { registerFilterCommands } from './commands/filter.js'
|
|
22
23
|
import { handleCommandError } from './output.js'
|
|
23
24
|
|
|
24
|
-
const cli = goke('zele')
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Global options
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
cli.option(
|
|
25
|
+
const cli: ZeleCli = goke('zele').option(
|
|
31
26
|
'--account <account>',
|
|
32
27
|
z.array(z.string()).describe('Filter by email account (repeatable)'),
|
|
33
28
|
)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Lists attachments for a thread and downloads them to disk.
|
|
3
3
|
// Skips re-download if file already exists with same size (like gogcli).
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { ZeleCli } from '../cli-types.js'
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
import fs from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
@@ -10,7 +10,7 @@ import { getClient } from '../auth.js'
|
|
|
10
10
|
import * as out from '../output.js'
|
|
11
11
|
import { handleCommandError } from '../output.js'
|
|
12
12
|
|
|
13
|
-
export function registerAttachmentCommands(cli:
|
|
13
|
+
export function registerAttachmentCommands(cli: ZeleCli) {
|
|
14
14
|
// =========================================================================
|
|
15
15
|
// attachment list
|
|
16
16
|
// =========================================================================
|
package/src/commands/auth-cmd.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Manages authentication for zele (Google OAuth and IMAP/SMTP credentials).
|
|
3
3
|
// Supports multiple accounts: login adds accounts, logout removes one.
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { ZeleCli } from '../cli-types.js'
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
import pc from 'picocolors'
|
|
8
8
|
import { login, loginImap, logout, listAccounts, getAuthStatuses } from '../auth.js'
|
|
@@ -10,7 +10,7 @@ import { closePrisma } from '../db.js'
|
|
|
10
10
|
import * as out from '../output.js'
|
|
11
11
|
import { handleCommandError } from '../output.js'
|
|
12
12
|
|
|
13
|
-
export function registerAuthCommands(cli:
|
|
13
|
+
export function registerAuthCommands(cli: ZeleCli) {
|
|
14
14
|
cli
|
|
15
15
|
.command('login', 'Authenticate with Google (opens browser) or show IMAP/SMTP login instructions')
|
|
16
16
|
.action(async () => {
|
package/src/commands/calendar.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Cache is handled by the client — commands just call methods and use data.
|
|
4
4
|
// Multi-account: list/events fetch all accounts concurrently and merge by start time.
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { ZeleCli } from '../cli-types.js'
|
|
7
7
|
import { z } from 'zod'
|
|
8
8
|
import readline from 'node:readline'
|
|
9
9
|
import { getCalendarClients, getCalendarClient } from '../auth.js'
|
|
@@ -17,7 +17,7 @@ import { resolveTimeRange, parseTimeExpression, parseDuration, isDateOnly } from
|
|
|
17
17
|
// Register commands
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
|
-
export function registerCalendarCommands(cli:
|
|
20
|
+
export function registerCalendarCommands(cli: ZeleCli) {
|
|
21
21
|
// =========================================================================
|
|
22
22
|
// cal list
|
|
23
23
|
// =========================================================================
|
package/src/commands/draft.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Cache invalidation is handled by the client (sendDraft invalidates threadLists).
|
|
4
4
|
// Multi-account: list fetches all accounts concurrently and merges by date.
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { ZeleCli } from '../cli-types.js'
|
|
7
7
|
import { z } from 'zod'
|
|
8
8
|
import fs from 'node:fs'
|
|
9
9
|
import { getClients, getClient } from '../auth.js'
|
|
@@ -14,7 +14,7 @@ import * as out from '../output.js'
|
|
|
14
14
|
import { handleCommandError } from '../output.js'
|
|
15
15
|
import pc from 'picocolors'
|
|
16
16
|
|
|
17
|
-
export function registerDraftCommands(cli:
|
|
17
|
+
export function registerDraftCommands(cli: ZeleCli) {
|
|
18
18
|
// =========================================================================
|
|
19
19
|
// draft list
|
|
20
20
|
// =========================================================================
|
|
@@ -89,14 +89,17 @@ export function registerDraftCommands(cli: Goke) {
|
|
|
89
89
|
const draft = await client.getDraft({ draftId })
|
|
90
90
|
if (draft instanceof Error) handleCommandError(draft)
|
|
91
91
|
|
|
92
|
+
const fmtRecipients = (list: Array<{ name?: string; email: string }>) =>
|
|
93
|
+
list.map((r) => r.name && r.name !== r.email ? `${r.name} <${r.email}>` : r.email).join(', ')
|
|
94
|
+
|
|
92
95
|
console.log(pc.bold(`Draft: ${draft.message.subject}`))
|
|
93
96
|
console.log(pc.dim(`Draft ID: ${draft.id}`))
|
|
94
|
-
console.log(`To: ${draft.to
|
|
97
|
+
console.log(`To: ${fmtRecipients(draft.to) || '(none)'}`)
|
|
95
98
|
if (draft.cc.length > 0) {
|
|
96
|
-
console.log(`Cc: ${draft.cc
|
|
99
|
+
console.log(`Cc: ${fmtRecipients(draft.cc)}`)
|
|
97
100
|
}
|
|
98
101
|
if (draft.bcc.length > 0) {
|
|
99
|
-
console.log(`Bcc: ${draft.bcc
|
|
102
|
+
console.log(`Bcc: ${fmtRecipients(draft.bcc)}`)
|
|
100
103
|
}
|
|
101
104
|
console.log()
|
|
102
105
|
|
|
@@ -161,6 +164,58 @@ export function registerDraftCommands(cli: Goke) {
|
|
|
161
164
|
out.success('Draft created')
|
|
162
165
|
})
|
|
163
166
|
|
|
167
|
+
// =========================================================================
|
|
168
|
+
// draft update
|
|
169
|
+
// =========================================================================
|
|
170
|
+
|
|
171
|
+
cli
|
|
172
|
+
.command('draft update <draftId>', 'Update an existing draft')
|
|
173
|
+
.option('--to <to>', z.string().describe('New recipient email(s), comma-separated'))
|
|
174
|
+
.option('--subject <subject>', z.string().describe('New subject'))
|
|
175
|
+
.option('--body <body>', z.string().describe('New body text'))
|
|
176
|
+
.option('--body-file <bodyFile>', z.string().describe('Read new body from file (use - for stdin)'))
|
|
177
|
+
.option('--cc <cc>', z.string().describe('New CC recipients (comma-separated)'))
|
|
178
|
+
.option('--bcc <bcc>', z.string().describe('New BCC recipients (comma-separated)'))
|
|
179
|
+
.option('--from <from>', z.string().describe('Send-as alias email'))
|
|
180
|
+
.action(async (draftId, options) => {
|
|
181
|
+
const { client } = await getClient(options.account)
|
|
182
|
+
|
|
183
|
+
// Fetch existing draft to merge unchanged fields
|
|
184
|
+
const existing = await client.getDraft({ draftId })
|
|
185
|
+
if (existing instanceof Error) handleCommandError(existing)
|
|
186
|
+
|
|
187
|
+
let body = options.body
|
|
188
|
+
if (options.bodyFile) {
|
|
189
|
+
if (options.bodyFile === '-') {
|
|
190
|
+
const chunks: Buffer[] = []
|
|
191
|
+
for await (const chunk of process.stdin) {
|
|
192
|
+
chunks.push(chunk)
|
|
193
|
+
}
|
|
194
|
+
body = Buffer.concat(chunks).toString('utf-8')
|
|
195
|
+
} else {
|
|
196
|
+
body = fs.readFileSync(options.bodyFile, 'utf-8')
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const parseEmails = (str: string) =>
|
|
201
|
+
str.split(',').map((e) => e.trim()).filter(Boolean).map((email) => ({ email }))
|
|
202
|
+
|
|
203
|
+
const result = await client.updateDraft({
|
|
204
|
+
draftId,
|
|
205
|
+
to: options.to ? parseEmails(options.to) : existing.to,
|
|
206
|
+
subject: options.subject ?? existing.message.subject,
|
|
207
|
+
body: body ?? existing.message.body,
|
|
208
|
+
cc: options.cc ? parseEmails(options.cc) : existing.cc,
|
|
209
|
+
bcc: options.bcc ? parseEmails(options.bcc) : existing.bcc,
|
|
210
|
+
threadId: existing.message.threadId || undefined,
|
|
211
|
+
fromEmail: options.from ?? existing.message.from.email,
|
|
212
|
+
})
|
|
213
|
+
if (result instanceof Error) handleCommandError(result)
|
|
214
|
+
|
|
215
|
+
out.printYaml(result)
|
|
216
|
+
out.success('Draft updated')
|
|
217
|
+
})
|
|
218
|
+
|
|
164
219
|
// =========================================================================
|
|
165
220
|
// draft send
|
|
166
221
|
// =========================================================================
|
package/src/commands/filter.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Filter commands: list, create, delete Gmail filters.
|
|
2
2
|
// Multi-account support via getClients/getClient like label.ts.
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { ZeleCli } from '../cli-types.js'
|
|
5
5
|
import { getClients } from '../auth.js'
|
|
6
6
|
import { AuthError, UnsupportedError, isScopeError } from '../api-utils.js'
|
|
7
7
|
import type { GmailClient } from '../gmail-client.js'
|
|
8
8
|
import * as out from '../output.js'
|
|
9
9
|
import { handleCommandError } from '../output.js'
|
|
10
10
|
|
|
11
|
-
export function registerFilterCommands(cli:
|
|
11
|
+
export function registerFilterCommands(cli: ZeleCli) {
|
|
12
12
|
// =========================================================================
|
|
13
13
|
// filter list
|
|
14
14
|
// =========================================================================
|