zele 0.3.0 → 0.3.5
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 +1 -1
- package/bin/zele +27 -0
- package/dist/api-utils.d.ts +51 -2
- package/dist/api-utils.js +89 -3
- package/dist/api-utils.js.map +1 -1
- package/dist/auth.d.ts +27 -6
- package/dist/auth.js +185 -129
- package/dist/auth.js.map +1 -1
- package/dist/calendar-client.d.ts +16 -9
- package/dist/calendar-client.js +163 -59
- package/dist/calendar-client.js.map +1 -1
- package/dist/cli.js +26 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/attachment.js +17 -15
- package/dist/commands/attachment.js.map +1 -1
- package/dist/commands/auth-cmd.js +20 -9
- package/dist/commands/auth-cmd.js.map +1 -1
- package/dist/commands/calendar.js +67 -78
- package/dist/commands/calendar.js.map +1 -1
- package/dist/commands/draft.js +25 -18
- package/dist/commands/draft.js.map +1 -1
- package/dist/commands/label.js +33 -45
- package/dist/commands/label.js.map +1 -1
- package/dist/commands/mail-actions.js +11 -13
- package/dist/commands/mail-actions.js.map +1 -1
- package/dist/commands/mail.js +112 -126
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/profile.js +18 -21
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/watch.js +33 -261
- package/dist/commands/watch.js.map +1 -1
- package/dist/db.js +12 -13
- package/dist/db.js.map +1 -1
- package/dist/generated/browser.d.ts +12 -27
- package/dist/generated/client.d.ts +13 -28
- package/dist/generated/client.js +1 -1
- package/dist/generated/commonInputTypes.d.ts +90 -26
- package/dist/generated/enums.d.ts +0 -4
- package/dist/generated/enums.js +0 -3
- package/dist/generated/enums.js.map +1 -1
- package/dist/generated/internal/class.d.ts +22 -55
- package/dist/generated/internal/class.js +12 -4
- package/dist/generated/internal/class.js.map +1 -1
- package/dist/generated/internal/prismaNamespace.d.ts +272 -511
- package/dist/generated/internal/prismaNamespace.js +54 -66
- package/dist/generated/internal/prismaNamespace.js.map +1 -1
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +60 -74
- package/dist/generated/internal/prismaNamespaceBrowser.js +50 -62
- package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
- package/dist/generated/models/Account.d.ts +1637 -0
- package/dist/generated/models/Account.js +2 -0
- package/dist/generated/models/Account.js.map +1 -0
- package/dist/generated/models/CalendarList.d.ts +1161 -0
- package/dist/generated/models/CalendarList.js +2 -0
- package/dist/generated/models/CalendarList.js.map +1 -0
- package/dist/generated/models/Label.d.ts +1161 -0
- package/dist/generated/models/Label.js +2 -0
- package/dist/generated/models/Label.js.map +1 -0
- package/dist/generated/models/Profile.d.ts +1269 -0
- package/dist/generated/models/Profile.js +2 -0
- package/dist/generated/models/Profile.js.map +1 -0
- package/dist/generated/models/SyncState.d.ts +1130 -0
- package/dist/generated/models/SyncState.js +2 -0
- package/dist/generated/models/SyncState.js.map +1 -0
- package/dist/generated/models/Thread.d.ts +1608 -0
- package/dist/generated/models/Thread.js +2 -0
- package/dist/generated/models/Thread.js.map +1 -0
- package/dist/generated/models.d.ts +6 -9
- package/dist/gmail-client.d.ts +119 -94
- package/dist/gmail-client.js +862 -322
- package/dist/gmail-client.js.map +1 -1
- package/dist/mail-tui.d.ts +1 -0
- package/dist/mail-tui.js +517 -0
- package/dist/mail-tui.js.map +1 -0
- package/dist/output.d.ts +6 -0
- package/dist/output.js +124 -11
- package/dist/output.js.map +1 -1
- package/package.json +39 -11
- package/schema.prisma +81 -113
- package/src/api-utils.ts +103 -5
- package/src/auth.ts +224 -143
- package/src/calendar-client.ts +196 -89
- package/src/cli.ts +30 -1
- package/src/commands/attachment.ts +18 -19
- package/src/commands/auth-cmd.ts +19 -9
- package/src/commands/calendar.ts +42 -85
- package/src/commands/draft.ts +19 -22
- package/src/commands/label.ts +21 -57
- package/src/commands/mail-actions.ts +11 -19
- package/src/commands/mail.ts +102 -147
- package/src/commands/profile.ts +12 -28
- package/src/commands/watch.ts +37 -304
- package/src/db.ts +13 -16
- package/src/generated/browser.ts +49 -0
- package/src/generated/client.ts +71 -0
- package/src/generated/commonInputTypes.ts +332 -0
- package/src/generated/enums.ts +17 -0
- package/src/generated/internal/class.ts +250 -0
- package/src/generated/internal/prismaNamespace.ts +1198 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +169 -0
- package/src/generated/models/Account.ts +1848 -0
- package/src/generated/models/CalendarList.ts +1331 -0
- package/src/generated/models/Label.ts +1331 -0
- package/src/generated/models/Profile.ts +1439 -0
- package/src/generated/models/SyncState.ts +1300 -0
- package/src/generated/models/Thread.ts +1787 -0
- package/src/generated/models.ts +17 -0
- package/src/gmail-client.test.ts +59 -0
- package/src/gmail-client.ts +1034 -429
- package/src/mail-tui.tsx +1061 -0
- package/src/output.test.ts +1093 -0
- package/src/output.ts +128 -13
- package/src/schema.sql +58 -68
- package/src/test-fixtures/email-html/safe-claude-event.html +28 -0
- package/src/test-fixtures/email-html/safe-product-announcement.html +25 -0
- package/src/test-fixtures/email-html/safe-tracked-links.html +27 -0
- package/src/test-fixtures/email-html-snapshots/safe-claude-event.html.md +9 -0
- package/src/test-fixtures/email-html-snapshots/safe-product-announcement.html.md +13 -0
- package/src/test-fixtures/email-html-snapshots/safe-tracked-links.html.md +7 -0
- package/AGENTS.md +0 -26
- package/CHANGELOG.md +0 -43
- package/dist/generated/models/accounts.d.ts +0 -2000
- package/dist/generated/models/accounts.js +0 -2
- package/dist/generated/models/accounts.js.map +0 -1
- package/dist/generated/models/calendar_events.d.ts +0 -1433
- package/dist/generated/models/calendar_events.js +0 -2
- package/dist/generated/models/calendar_events.js.map +0 -1
- package/dist/generated/models/calendar_lists.d.ts +0 -1131
- package/dist/generated/models/calendar_lists.js +0 -2
- package/dist/generated/models/calendar_lists.js.map +0 -1
- package/dist/generated/models/label_counts.d.ts +0 -1131
- package/dist/generated/models/label_counts.js +0 -2
- package/dist/generated/models/label_counts.js.map +0 -1
- package/dist/generated/models/labels.d.ts +0 -1131
- package/dist/generated/models/labels.js +0 -2
- package/dist/generated/models/labels.js.map +0 -1
- package/dist/generated/models/profiles.d.ts +0 -1131
- package/dist/generated/models/profiles.js +0 -2
- package/dist/generated/models/profiles.js.map +0 -1
- package/dist/generated/models/sync_states.d.ts +0 -1107
- package/dist/generated/models/sync_states.js +0 -2
- package/dist/generated/models/sync_states.js.map +0 -1
- package/dist/generated/models/thread_lists.d.ts +0 -1404
- package/dist/generated/models/thread_lists.js +0 -2
- package/dist/generated/models/thread_lists.js.map +0 -1
- package/dist/generated/models/threads.d.ts +0 -1247
- package/dist/generated/models/threads.js +0 -2
- package/dist/generated/models/threads.js.map +0 -1
- package/dist/gmail-cache.d.ts +0 -60
- package/dist/gmail-cache.js +0 -264
- package/dist/gmail-cache.js.map +0 -1
- package/docs/gogcli-gmail-implementation.md +0 -599
- package/scripts/test-device-code-clients.ts +0 -186
- package/scripts/test-micropython-scopes.ts +0 -72
- package/scripts/test-oauth-clients.ts +0 -257
- package/src/gmail-cache.ts +0 -339
- package/tsconfig.json +0 -16
|
@@ -1,36 +1,31 @@
|
|
|
1
1
|
// Mail action commands: star, unstar, archive, trash, untrash, mark read/unread, label modify.
|
|
2
|
-
// Bulk operations on threads —
|
|
2
|
+
// Bulk operations on threads — cache invalidation is handled by the client methods.
|
|
3
3
|
|
|
4
4
|
import type { Goke } from 'goke'
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
import { getClient } from '../auth.js'
|
|
7
|
-
import { GmailClient } from '../gmail-client.js'
|
|
8
|
-
import * as cache from '../gmail-cache.js'
|
|
7
|
+
import type { GmailClient } from '../gmail-client.js'
|
|
9
8
|
import * as out from '../output.js'
|
|
9
|
+
import { handleCommandError } from '../output.js'
|
|
10
10
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
|
-
// Helper: run a bulk action
|
|
12
|
+
// Helper: run a bulk action
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
15
|
async function bulkAction(
|
|
16
16
|
threadIds: string[],
|
|
17
17
|
actionName: string,
|
|
18
|
-
|
|
19
|
-
fn: (client: GmailClient, ids: string[]) => Promise<void>,
|
|
18
|
+
accountFilter: string[] | undefined,
|
|
19
|
+
fn: (client: GmailClient, ids: string[]) => Promise<void | Error>,
|
|
20
20
|
) {
|
|
21
21
|
if (threadIds.length === 0) {
|
|
22
22
|
out.error('No thread IDs provided')
|
|
23
23
|
process.exit(1)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Invalidate caches
|
|
31
|
-
await cache.invalidateThreads(email, threadIds)
|
|
32
|
-
await cache.invalidateThreadLists(email)
|
|
33
|
-
await cache.invalidateLabelCounts(email)
|
|
26
|
+
const { client } = await getClient(accountFilter)
|
|
27
|
+
const result = await fn(client, threadIds)
|
|
28
|
+
if (result instanceof Error) handleCommandError(result)
|
|
34
29
|
|
|
35
30
|
out.printYaml({ action: actionName, thread_ids: threadIds, success: true })
|
|
36
31
|
}
|
|
@@ -106,12 +101,9 @@ export function registerMailActionCommands(cli: Goke) {
|
|
|
106
101
|
cli
|
|
107
102
|
.command('mail trash-spam', 'Trash all spam threads')
|
|
108
103
|
.action(async (options) => {
|
|
109
|
-
const {
|
|
110
|
-
|
|
104
|
+
const { client } = await getClient(options.account)
|
|
111
105
|
const result = await client.trashAllSpam()
|
|
112
|
-
|
|
113
|
-
await cache.invalidateThreadLists(email)
|
|
114
|
-
await cache.invalidateLabelCounts(email)
|
|
106
|
+
if (result instanceof Error) handleCommandError(result)
|
|
115
107
|
|
|
116
108
|
out.printYaml(result)
|
|
117
109
|
out.success(`Trashed ${result.count} spam thread(s)`)
|
package/src/commands/mail.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// Mail commands: list, search, read, send, reply, forward.
|
|
2
|
-
// Core email operations wrapping GmailClient with
|
|
3
|
-
//
|
|
2
|
+
// Core email operations wrapping GmailClient with YAML output for list views.
|
|
3
|
+
// Cache is handled by the client — commands just call methods and use data.
|
|
4
4
|
// Multi-account: list/search fetch all accounts concurrently and merge by date.
|
|
5
5
|
|
|
6
6
|
import type { Goke } from 'goke'
|
|
7
7
|
import { z } from 'zod'
|
|
8
8
|
import fs from 'node:fs'
|
|
9
|
+
import React from 'react'
|
|
9
10
|
import { getClients, getClient } from '../auth.js'
|
|
10
|
-
import
|
|
11
|
-
import
|
|
11
|
+
import type { ThreadListResult } from '../gmail-client.js'
|
|
12
|
+
import { AuthError } from '../api-utils.js'
|
|
12
13
|
import * as out from '../output.js'
|
|
14
|
+
import { handleCommandError } from '../output.js'
|
|
13
15
|
import pc from 'picocolors'
|
|
14
16
|
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +19,18 @@ import pc from 'picocolors'
|
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
18
20
|
|
|
19
21
|
export function registerMailCommands(cli: Goke) {
|
|
22
|
+
// =========================================================================
|
|
23
|
+
// mail (TUI)
|
|
24
|
+
// =========================================================================
|
|
25
|
+
|
|
26
|
+
cli
|
|
27
|
+
.command('mail', 'Browse emails in TUI')
|
|
28
|
+
.action(async () => {
|
|
29
|
+
const { renderWithProviders } = await import('termcast')
|
|
30
|
+
const { default: Command } = await import('../mail-tui.js')
|
|
31
|
+
await renderWithProviders(React.createElement(Command))
|
|
32
|
+
})
|
|
33
|
+
|
|
20
34
|
// =========================================================================
|
|
21
35
|
// mail list
|
|
22
36
|
// =========================================================================
|
|
@@ -27,7 +41,6 @@ export function registerMailCommands(cli: Goke) {
|
|
|
27
41
|
.option('--max [max]', 'Max results per page (default: 20)')
|
|
28
42
|
.option('--page <page>', 'Pagination token')
|
|
29
43
|
.option('--label <label>', 'Filter by label name')
|
|
30
|
-
.option('--no-cache', 'Skip cache')
|
|
31
44
|
.action(async (options) => {
|
|
32
45
|
const folder = options.folder ?? 'inbox'
|
|
33
46
|
const max = options.max ? Number(options.max) : 20
|
|
@@ -38,47 +51,25 @@ export function registerMailCommands(cli: Goke) {
|
|
|
38
51
|
process.exit(1)
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
maxResults: max,
|
|
44
|
-
labelIds: options.label ? [options.label] : undefined,
|
|
45
|
-
pageToken: options.page,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Fetch from all accounts concurrently, tolerating individual failures
|
|
49
|
-
const settled = await Promise.allSettled(
|
|
54
|
+
// Fetch from all accounts concurrently
|
|
55
|
+
const results = await Promise.all(
|
|
50
56
|
clients.map(async ({ email, client }) => {
|
|
51
|
-
if (!options.noCache) {
|
|
52
|
-
const cached = await cache.getCachedThreadList<ThreadListResult>(email, cacheParams)
|
|
53
|
-
if (cached) {
|
|
54
|
-
return { email, result: cached }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
57
|
const result = await client.listThreads({
|
|
59
58
|
folder,
|
|
60
59
|
maxResults: max,
|
|
61
60
|
labelIds: options.label ? [options.label] : undefined,
|
|
62
61
|
pageToken: options.page,
|
|
63
62
|
})
|
|
64
|
-
|
|
65
|
-
if (!options.noCache) {
|
|
66
|
-
await cache.cacheThreadList(email, cacheParams, result)
|
|
67
|
-
}
|
|
68
|
-
|
|
63
|
+
if (result instanceof Error) return result
|
|
69
64
|
return { email, result }
|
|
70
65
|
}),
|
|
71
66
|
)
|
|
72
67
|
|
|
73
|
-
const allResults =
|
|
74
|
-
|
|
75
|
-
if (r.
|
|
76
|
-
out.error(`Failed to fetch: ${r.reason}`)
|
|
77
|
-
return false
|
|
78
|
-
}
|
|
68
|
+
const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
|
|
69
|
+
if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
|
|
70
|
+
if (r instanceof Error) { out.error(`Failed to fetch: ${r.message}`); return false }
|
|
79
71
|
return true
|
|
80
72
|
})
|
|
81
|
-
.map((r) => r.value)
|
|
82
73
|
|
|
83
74
|
// Merge threads from all accounts, sorted by date descending, capped at max
|
|
84
75
|
const merged = allResults
|
|
@@ -97,6 +88,7 @@ export function registerMailCommands(cli: Goke) {
|
|
|
97
88
|
out.printList(
|
|
98
89
|
merged.map((t) => ({
|
|
99
90
|
...(showAccount ? { account: t.account } : {}),
|
|
91
|
+
id: t.id,
|
|
100
92
|
flags: out.formatFlags(t),
|
|
101
93
|
from: out.formatSender(t.from),
|
|
102
94
|
subject: t.subject,
|
|
@@ -125,27 +117,24 @@ export function registerMailCommands(cli: Goke) {
|
|
|
125
117
|
process.exit(1)
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
// Search all accounts concurrently
|
|
129
|
-
const
|
|
120
|
+
// Search all accounts concurrently
|
|
121
|
+
const results = await Promise.all(
|
|
130
122
|
clients.map(async ({ email, client }) => {
|
|
131
123
|
const result = await client.listThreads({
|
|
132
124
|
query,
|
|
133
125
|
maxResults: max,
|
|
134
126
|
pageToken: options.page,
|
|
135
127
|
})
|
|
128
|
+
if (result instanceof Error) return result
|
|
136
129
|
return { email, result }
|
|
137
130
|
}),
|
|
138
131
|
)
|
|
139
132
|
|
|
140
|
-
const allResults =
|
|
141
|
-
|
|
142
|
-
if (r.
|
|
143
|
-
out.error(`Failed to search: ${r.reason}`)
|
|
144
|
-
return false
|
|
145
|
-
}
|
|
133
|
+
const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
|
|
134
|
+
if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
|
|
135
|
+
if (r instanceof Error) { out.error(`Failed to search: ${r.message}`); return false }
|
|
146
136
|
return true
|
|
147
137
|
})
|
|
148
|
-
.map((r) => r.value)
|
|
149
138
|
|
|
150
139
|
const merged = allResults
|
|
151
140
|
.flatMap(({ email, result }) =>
|
|
@@ -163,6 +152,7 @@ export function registerMailCommands(cli: Goke) {
|
|
|
163
152
|
out.printList(
|
|
164
153
|
merged.map((t) => ({
|
|
165
154
|
...(showAccount ? { account: t.account } : {}),
|
|
155
|
+
id: t.id,
|
|
166
156
|
flags: out.formatFlags(t),
|
|
167
157
|
from: out.formatSender(t.from),
|
|
168
158
|
subject: t.subject,
|
|
@@ -181,65 +171,93 @@ export function registerMailCommands(cli: Goke) {
|
|
|
181
171
|
cli
|
|
182
172
|
.command('mail read <threadId>', 'Read a full email thread')
|
|
183
173
|
.option('--raw', 'Show raw message (first message only)')
|
|
184
|
-
.option('--
|
|
174
|
+
.option('--raw-html', 'Show raw HTML body per message (no markdown conversion)')
|
|
185
175
|
.action(async (threadId, options) => {
|
|
186
|
-
const {
|
|
176
|
+
const { client } = await getClient(options.account)
|
|
177
|
+
|
|
178
|
+
if (options.raw && options.rawHtml) {
|
|
179
|
+
out.error('--raw and --raw-html cannot be used together')
|
|
180
|
+
process.exit(1)
|
|
181
|
+
}
|
|
187
182
|
|
|
188
183
|
if (options.raw) {
|
|
189
|
-
const thread = await client.getThread({ threadId })
|
|
184
|
+
const { parsed: thread } = await client.getThread({ threadId })
|
|
190
185
|
if (thread.messages.length === 0) {
|
|
191
186
|
out.hint('No messages in thread')
|
|
192
187
|
return
|
|
193
188
|
}
|
|
194
189
|
const rawMsg = await client.getRawMessage({ messageId: thread.messages[0]!.id })
|
|
195
|
-
|
|
190
|
+
if (rawMsg instanceof Error) handleCommandError(rawMsg)
|
|
191
|
+
console.log(rawMsg)
|
|
196
192
|
return
|
|
197
193
|
}
|
|
198
194
|
|
|
199
|
-
|
|
200
|
-
let thread: ThreadData | undefined
|
|
201
|
-
if (!options.noCache) {
|
|
202
|
-
thread = await cache.getCachedThread<ThreadData>(email, threadId)
|
|
203
|
-
}
|
|
204
|
-
if (!thread) {
|
|
205
|
-
thread = await client.getThread({ threadId })
|
|
206
|
-
if (!options.noCache) {
|
|
207
|
-
await cache.cacheThread(email, threadId, thread)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
195
|
+
const { parsed: thread } = await client.getThread({ threadId })
|
|
210
196
|
|
|
211
197
|
if (thread.messages.length === 0) {
|
|
212
198
|
out.hint('No messages in thread')
|
|
213
199
|
return
|
|
214
200
|
}
|
|
215
201
|
|
|
202
|
+
if (options.rawHtml) {
|
|
203
|
+
thread.messages.forEach((msg, index) => {
|
|
204
|
+
console.log(msg.body)
|
|
205
|
+
if (index < thread.messages.length - 1) {
|
|
206
|
+
console.log('\n<!-- ZELE_MESSAGE_SEPARATOR -->\n')
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const w = Math.min(process.stdout.columns || 72, 72)
|
|
213
|
+
const rule = pc.dim('─'.repeat(w))
|
|
214
|
+
|
|
216
215
|
// Render thread header
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
console.log(pc.bold(thread.subject))
|
|
217
|
+
// Collect unique participants
|
|
218
|
+
const participants = new Map<string, string>()
|
|
219
|
+
for (const msg of thread.messages) {
|
|
220
|
+
participants.set(msg.from.email, msg.from.name || msg.from.email)
|
|
221
|
+
for (const r of msg.to) participants.set(r.email, r.name || r.email)
|
|
222
|
+
}
|
|
223
|
+
const participantStr = [...participants.values()].join(', ')
|
|
224
|
+
console.log(pc.dim(`${thread.messageCount} message(s) · ${participantStr}`))
|
|
225
|
+
console.log(pc.dim(`ID: ${thread.id}`))
|
|
226
|
+
console.log(rule + '\n')
|
|
220
227
|
|
|
221
228
|
// Render each message
|
|
222
229
|
for (const msg of thread.messages) {
|
|
223
230
|
const fromStr = out.formatSender(msg.from)
|
|
224
231
|
const dateStr = out.formatDate(msg.date)
|
|
225
|
-
const flags = out.formatFlags(msg)
|
|
226
232
|
|
|
227
|
-
|
|
228
|
-
|
|
233
|
+
// Flags as dim tags
|
|
234
|
+
const flagParts: string[] = []
|
|
235
|
+
if (msg.unread) flagParts.push(pc.yellow('[unread]'))
|
|
236
|
+
if (msg.starred) flagParts.push(pc.yellow('[starred]'))
|
|
237
|
+
const flagStr = flagParts.length > 0 ? ' ' + flagParts.join(' ') : ''
|
|
238
|
+
|
|
239
|
+
console.log(pc.bold(`From: `) + fromStr + flagStr)
|
|
240
|
+
console.log(pc.dim(` To: ${msg.to.map((t) => t.email).join(', ')}`))
|
|
229
241
|
if (msg.cc && msg.cc.length > 0) {
|
|
230
|
-
|
|
242
|
+
console.log(pc.dim(` Cc: ${msg.cc.map((c) => c.email).join(', ')}`))
|
|
231
243
|
}
|
|
232
|
-
|
|
244
|
+
console.log(pc.dim(`Date: ${dateStr}`))
|
|
233
245
|
|
|
234
246
|
if (msg.attachments.length > 0) {
|
|
235
|
-
|
|
247
|
+
const attList = msg.attachments.map((a) => {
|
|
248
|
+
const size = a.size < 1024 ? `${a.size} B`
|
|
249
|
+
: a.size < 1048576 ? `${(a.size / 1024).toFixed(1)} KB`
|
|
250
|
+
: `${(a.size / 1048576).toFixed(1)} MB`
|
|
251
|
+
return `${a.filename} (${size})`
|
|
252
|
+
})
|
|
253
|
+
console.log(pc.dim(`Attachments: ${attList.join(', ')}`))
|
|
236
254
|
}
|
|
237
255
|
|
|
238
|
-
|
|
256
|
+
console.log()
|
|
239
257
|
|
|
240
258
|
const body = out.renderEmailBody(msg.body, msg.mimeType)
|
|
241
|
-
|
|
242
|
-
|
|
259
|
+
console.log(body)
|
|
260
|
+
console.log('\n' + rule + '\n')
|
|
243
261
|
}
|
|
244
262
|
})
|
|
245
263
|
|
|
@@ -287,7 +305,7 @@ export function registerMailCommands(cli: Goke) {
|
|
|
287
305
|
const parseEmails = (str: string) =>
|
|
288
306
|
str.split(',').map((e) => e.trim()).filter(Boolean).map((email) => ({ email }))
|
|
289
307
|
|
|
290
|
-
const {
|
|
308
|
+
const { client } = await getClient(options.account)
|
|
291
309
|
|
|
292
310
|
const result = await client.sendMessage({
|
|
293
311
|
to: parseEmails(options.to),
|
|
@@ -298,8 +316,6 @@ export function registerMailCommands(cli: Goke) {
|
|
|
298
316
|
fromEmail: options.from,
|
|
299
317
|
})
|
|
300
318
|
|
|
301
|
-
await cache.invalidateThreadLists(email)
|
|
302
|
-
|
|
303
319
|
out.printYaml(result)
|
|
304
320
|
out.success(`Sent to ${options.to}`)
|
|
305
321
|
})
|
|
@@ -316,16 +332,6 @@ export function registerMailCommands(cli: Goke) {
|
|
|
316
332
|
.option('--all', 'Reply all (include all original recipients)')
|
|
317
333
|
.option('--from <from>', z.string().describe('Send-as alias email'))
|
|
318
334
|
.action(async (threadId, options) => {
|
|
319
|
-
const { email, client } = await getClient(options.account)
|
|
320
|
-
|
|
321
|
-
const thread = await client.getThread({ threadId })
|
|
322
|
-
if (thread.messages.length === 0) {
|
|
323
|
-
out.error('No messages in thread')
|
|
324
|
-
process.exit(1)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const lastMsg = thread.messages[thread.messages.length - 1]!
|
|
328
|
-
|
|
329
335
|
let body = options.body ?? ''
|
|
330
336
|
if (options.bodyFile) {
|
|
331
337
|
if (options.bodyFile === '-') {
|
|
@@ -344,49 +350,20 @@ export function registerMailCommands(cli: Goke) {
|
|
|
344
350
|
process.exit(1)
|
|
345
351
|
}
|
|
346
352
|
|
|
347
|
-
const
|
|
348
|
-
const to = [{ email: replyTo }]
|
|
349
|
-
|
|
350
|
-
let cc: Array<{ email: string }> | undefined
|
|
351
|
-
if (options.all) {
|
|
352
|
-
const profile = await client.getProfile()
|
|
353
|
-
const myEmail = profile.emailAddress.toLowerCase()
|
|
354
|
-
|
|
355
|
-
const allRecipients = [
|
|
356
|
-
...lastMsg.to.map((r) => r.email),
|
|
357
|
-
...(lastMsg.cc?.map((r) => r.email) ?? []),
|
|
358
|
-
]
|
|
359
|
-
.filter((e) => e.toLowerCase() !== myEmail)
|
|
360
|
-
.filter((e) => e.toLowerCase() !== replyTo.toLowerCase())
|
|
361
|
-
|
|
362
|
-
if (allRecipients.length > 0) {
|
|
363
|
-
cc = allRecipients.map((e) => ({ email: e }))
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (options.cc) {
|
|
368
|
-
const extra = options.cc
|
|
369
|
-
.split(',')
|
|
370
|
-
.map((e: string) => ({ email: e.trim() }))
|
|
371
|
-
.filter((e: { email: string }) => e.email)
|
|
372
|
-
cc = [...(cc ?? []), ...extra]
|
|
373
|
-
}
|
|
353
|
+
const { client } = await getClient(options.account)
|
|
374
354
|
|
|
375
|
-
const
|
|
355
|
+
const cc = options.cc
|
|
356
|
+
? options.cc.split(',').map((e: string) => ({ email: e.trim() })).filter((e: { email: string }) => e.email)
|
|
357
|
+
: undefined
|
|
376
358
|
|
|
377
|
-
const result = await client.
|
|
378
|
-
|
|
379
|
-
subject: lastMsg.subject.startsWith('Re:') ? lastMsg.subject : `Re: ${lastMsg.subject}`,
|
|
359
|
+
const result = await client.replyToThread({
|
|
360
|
+
threadId,
|
|
380
361
|
body,
|
|
362
|
+
replyAll: options.all,
|
|
381
363
|
cc,
|
|
382
|
-
threadId,
|
|
383
|
-
inReplyTo: lastMsg.messageId,
|
|
384
|
-
references: refs || undefined,
|
|
385
364
|
fromEmail: options.from,
|
|
386
365
|
})
|
|
387
|
-
|
|
388
|
-
await cache.invalidateThread(email, threadId)
|
|
389
|
-
await cache.invalidateThreadLists(email)
|
|
366
|
+
if (result instanceof Error) handleCommandError(result)
|
|
390
367
|
|
|
391
368
|
out.printYaml(result)
|
|
392
369
|
out.success('Reply sent')
|
|
@@ -407,42 +384,20 @@ export function registerMailCommands(cli: Goke) {
|
|
|
407
384
|
process.exit(1)
|
|
408
385
|
}
|
|
409
386
|
|
|
410
|
-
const { email, client } = await getClient(options.account)
|
|
411
|
-
|
|
412
|
-
const thread = await client.getThread({ threadId })
|
|
413
|
-
if (thread.messages.length === 0) {
|
|
414
|
-
out.error('No messages in thread')
|
|
415
|
-
process.exit(1)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const lastMsg = thread.messages[thread.messages.length - 1]!
|
|
419
|
-
const forwardedBody = out.renderEmailBody(lastMsg.body, lastMsg.mimeType)
|
|
420
|
-
|
|
421
|
-
const fullBody = [
|
|
422
|
-
options.body ?? '',
|
|
423
|
-
'',
|
|
424
|
-
'---------- Forwarded message ----------',
|
|
425
|
-
`From: ${out.formatSender(lastMsg.from)}`,
|
|
426
|
-
`Date: ${lastMsg.date}`,
|
|
427
|
-
`Subject: ${lastMsg.subject}`,
|
|
428
|
-
`To: ${lastMsg.to.map((t) => t.email).join(', ')}`,
|
|
429
|
-
'',
|
|
430
|
-
forwardedBody,
|
|
431
|
-
].join('\n')
|
|
432
|
-
|
|
433
387
|
const recipients = options.to
|
|
434
388
|
.split(',')
|
|
435
389
|
.map((e: string) => ({ email: e.trim() }))
|
|
436
390
|
.filter((e: { email: string }) => e.email)
|
|
437
391
|
|
|
438
|
-
const
|
|
392
|
+
const { client } = await getClient(options.account)
|
|
393
|
+
|
|
394
|
+
const result = await client.forwardThread({
|
|
395
|
+
threadId,
|
|
439
396
|
to: recipients,
|
|
440
|
-
|
|
441
|
-
body: fullBody,
|
|
397
|
+
body: options.body,
|
|
442
398
|
fromEmail: options.from,
|
|
443
399
|
})
|
|
444
|
-
|
|
445
|
-
await cache.invalidateThreadLists(email)
|
|
400
|
+
if (result instanceof Error) handleCommandError(result)
|
|
446
401
|
|
|
447
402
|
out.printYaml(result)
|
|
448
403
|
out.success(`Forwarded to ${options.to}`)
|
package/src/commands/profile.ts
CHANGED
|
@@ -1,52 +1,36 @@
|
|
|
1
1
|
// Profile command: show account info.
|
|
2
2
|
// Displays email address, message/thread counts, and aliases as YAML.
|
|
3
|
+
// Cache is handled by the client — commands just call methods and use data.
|
|
3
4
|
// Multi-account: shows all accounts or filtered by --account.
|
|
4
5
|
|
|
5
6
|
import type { Goke } from 'goke'
|
|
6
7
|
import { getClients } from '../auth.js'
|
|
7
|
-
import {
|
|
8
|
-
import * as cache from '../gmail-cache.js'
|
|
8
|
+
import { AuthError } from '../api-utils.js'
|
|
9
9
|
import * as out from '../output.js'
|
|
10
10
|
|
|
11
11
|
export function registerProfileCommands(cli: Goke) {
|
|
12
12
|
cli
|
|
13
13
|
.command('profile', 'Show Gmail account info')
|
|
14
|
-
.option('--no-cache', 'Skip cache')
|
|
15
14
|
.action(async (options) => {
|
|
16
15
|
const clients = await getClients(options.account)
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let profile: Profile | undefined
|
|
24
|
-
if (!options.noCache) {
|
|
25
|
-
profile = await cache.getCachedProfile<Profile>(email)
|
|
26
|
-
}
|
|
27
|
-
if (!profile) {
|
|
28
|
-
profile = await client.getProfile()
|
|
29
|
-
if (!options.noCache) {
|
|
30
|
-
await cache.cacheProfile(email, profile)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
17
|
+
// Fetch all accounts concurrently
|
|
18
|
+
const allResults = await Promise.all(
|
|
19
|
+
clients.map(async ({ client }) => {
|
|
20
|
+
const profile = await client.getProfile()
|
|
21
|
+
if (profile instanceof Error) return profile
|
|
34
22
|
// Always fetch aliases fresh
|
|
35
23
|
const aliases = await client.getEmailAliases()
|
|
36
|
-
|
|
37
|
-
return {
|
|
24
|
+
if (aliases instanceof Error) return aliases
|
|
25
|
+
return { profile, aliases }
|
|
38
26
|
}),
|
|
39
27
|
)
|
|
40
28
|
|
|
41
|
-
const results =
|
|
42
|
-
|
|
43
|
-
if (r.
|
|
44
|
-
out.error(`Failed to fetch profile: ${r.reason}`)
|
|
45
|
-
return false
|
|
46
|
-
}
|
|
29
|
+
const results = allResults.filter((r): r is Exclude<typeof r, Error> => {
|
|
30
|
+
if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
|
|
31
|
+
if (r instanceof Error) { out.error(`Failed to fetch profile: ${r.message}`); return false }
|
|
47
32
|
return true
|
|
48
33
|
})
|
|
49
|
-
.map((r) => r.value)
|
|
50
34
|
|
|
51
35
|
for (const { profile, aliases } of results) {
|
|
52
36
|
out.printYaml({
|