zele 0.3.0 → 0.3.6
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 +34 -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 +119 -127
- 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 +39 -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 +109 -148
- 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
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { getClients, getClient, listAccounts, login } from '../auth.js'
|
|
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,24 @@ 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 accounts = await listAccounts()
|
|
30
|
+
if (accounts.length === 0) {
|
|
31
|
+
const result = await login()
|
|
32
|
+
if (result instanceof Error) handleCommandError(result)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { renderWithProviders } = await import('termcast')
|
|
36
|
+
const { default: Command } = await import('../mail-tui.js')
|
|
37
|
+
await renderWithProviders(React.createElement(Command))
|
|
38
|
+
})
|
|
39
|
+
|
|
20
40
|
// =========================================================================
|
|
21
41
|
// mail list
|
|
22
42
|
// =========================================================================
|
|
@@ -27,7 +47,6 @@ export function registerMailCommands(cli: Goke) {
|
|
|
27
47
|
.option('--max [max]', 'Max results per page (default: 20)')
|
|
28
48
|
.option('--page <page>', 'Pagination token')
|
|
29
49
|
.option('--label <label>', 'Filter by label name')
|
|
30
|
-
.option('--no-cache', 'Skip cache')
|
|
31
50
|
.action(async (options) => {
|
|
32
51
|
const folder = options.folder ?? 'inbox'
|
|
33
52
|
const max = options.max ? Number(options.max) : 20
|
|
@@ -38,47 +57,25 @@ export function registerMailCommands(cli: Goke) {
|
|
|
38
57
|
process.exit(1)
|
|
39
58
|
}
|
|
40
59
|
|
|
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(
|
|
60
|
+
// Fetch from all accounts concurrently
|
|
61
|
+
const results = await Promise.all(
|
|
50
62
|
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
63
|
const result = await client.listThreads({
|
|
59
64
|
folder,
|
|
60
65
|
maxResults: max,
|
|
61
66
|
labelIds: options.label ? [options.label] : undefined,
|
|
62
67
|
pageToken: options.page,
|
|
63
68
|
})
|
|
64
|
-
|
|
65
|
-
if (!options.noCache) {
|
|
66
|
-
await cache.cacheThreadList(email, cacheParams, result)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
+
if (result instanceof Error) return result
|
|
69
70
|
return { email, result }
|
|
70
71
|
}),
|
|
71
72
|
)
|
|
72
73
|
|
|
73
|
-
const allResults =
|
|
74
|
-
|
|
75
|
-
if (r.
|
|
76
|
-
out.error(`Failed to fetch: ${r.reason}`)
|
|
77
|
-
return false
|
|
78
|
-
}
|
|
74
|
+
const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
|
|
75
|
+
if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
|
|
76
|
+
if (r instanceof Error) { out.error(`Failed to fetch: ${r.message}`); return false }
|
|
79
77
|
return true
|
|
80
78
|
})
|
|
81
|
-
.map((r) => r.value)
|
|
82
79
|
|
|
83
80
|
// Merge threads from all accounts, sorted by date descending, capped at max
|
|
84
81
|
const merged = allResults
|
|
@@ -97,6 +94,7 @@ export function registerMailCommands(cli: Goke) {
|
|
|
97
94
|
out.printList(
|
|
98
95
|
merged.map((t) => ({
|
|
99
96
|
...(showAccount ? { account: t.account } : {}),
|
|
97
|
+
id: t.id,
|
|
100
98
|
flags: out.formatFlags(t),
|
|
101
99
|
from: out.formatSender(t.from),
|
|
102
100
|
subject: t.subject,
|
|
@@ -125,27 +123,24 @@ export function registerMailCommands(cli: Goke) {
|
|
|
125
123
|
process.exit(1)
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
// Search all accounts concurrently
|
|
129
|
-
const
|
|
126
|
+
// Search all accounts concurrently
|
|
127
|
+
const results = await Promise.all(
|
|
130
128
|
clients.map(async ({ email, client }) => {
|
|
131
129
|
const result = await client.listThreads({
|
|
132
130
|
query,
|
|
133
131
|
maxResults: max,
|
|
134
132
|
pageToken: options.page,
|
|
135
133
|
})
|
|
134
|
+
if (result instanceof Error) return result
|
|
136
135
|
return { email, result }
|
|
137
136
|
}),
|
|
138
137
|
)
|
|
139
138
|
|
|
140
|
-
const allResults =
|
|
141
|
-
|
|
142
|
-
if (r.
|
|
143
|
-
out.error(`Failed to search: ${r.reason}`)
|
|
144
|
-
return false
|
|
145
|
-
}
|
|
139
|
+
const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
|
|
140
|
+
if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
|
|
141
|
+
if (r instanceof Error) { out.error(`Failed to search: ${r.message}`); return false }
|
|
146
142
|
return true
|
|
147
143
|
})
|
|
148
|
-
.map((r) => r.value)
|
|
149
144
|
|
|
150
145
|
const merged = allResults
|
|
151
146
|
.flatMap(({ email, result }) =>
|
|
@@ -163,6 +158,7 @@ export function registerMailCommands(cli: Goke) {
|
|
|
163
158
|
out.printList(
|
|
164
159
|
merged.map((t) => ({
|
|
165
160
|
...(showAccount ? { account: t.account } : {}),
|
|
161
|
+
id: t.id,
|
|
166
162
|
flags: out.formatFlags(t),
|
|
167
163
|
from: out.formatSender(t.from),
|
|
168
164
|
subject: t.subject,
|
|
@@ -181,65 +177,93 @@ export function registerMailCommands(cli: Goke) {
|
|
|
181
177
|
cli
|
|
182
178
|
.command('mail read <threadId>', 'Read a full email thread')
|
|
183
179
|
.option('--raw', 'Show raw message (first message only)')
|
|
184
|
-
.option('--
|
|
180
|
+
.option('--raw-html', 'Show raw HTML body per message (no markdown conversion)')
|
|
185
181
|
.action(async (threadId, options) => {
|
|
186
|
-
const {
|
|
182
|
+
const { client } = await getClient(options.account)
|
|
183
|
+
|
|
184
|
+
if (options.raw && options.rawHtml) {
|
|
185
|
+
out.error('--raw and --raw-html cannot be used together')
|
|
186
|
+
process.exit(1)
|
|
187
|
+
}
|
|
187
188
|
|
|
188
189
|
if (options.raw) {
|
|
189
|
-
const thread = await client.getThread({ threadId })
|
|
190
|
+
const { parsed: thread } = await client.getThread({ threadId })
|
|
190
191
|
if (thread.messages.length === 0) {
|
|
191
192
|
out.hint('No messages in thread')
|
|
192
193
|
return
|
|
193
194
|
}
|
|
194
195
|
const rawMsg = await client.getRawMessage({ messageId: thread.messages[0]!.id })
|
|
195
|
-
|
|
196
|
+
if (rawMsg instanceof Error) handleCommandError(rawMsg)
|
|
197
|
+
console.log(rawMsg)
|
|
196
198
|
return
|
|
197
199
|
}
|
|
198
200
|
|
|
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
|
-
}
|
|
201
|
+
const { parsed: thread } = await client.getThread({ threadId })
|
|
210
202
|
|
|
211
203
|
if (thread.messages.length === 0) {
|
|
212
204
|
out.hint('No messages in thread')
|
|
213
205
|
return
|
|
214
206
|
}
|
|
215
207
|
|
|
208
|
+
if (options.rawHtml) {
|
|
209
|
+
thread.messages.forEach((msg, index) => {
|
|
210
|
+
console.log(msg.body)
|
|
211
|
+
if (index < thread.messages.length - 1) {
|
|
212
|
+
console.log('\n<!-- ZELE_MESSAGE_SEPARATOR -->\n')
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const w = Math.min(process.stdout.columns || 72, 72)
|
|
219
|
+
const rule = pc.dim('─'.repeat(w))
|
|
220
|
+
|
|
216
221
|
// Render thread header
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
console.log(pc.bold(thread.subject))
|
|
223
|
+
// Collect unique participants
|
|
224
|
+
const participants = new Map<string, string>()
|
|
225
|
+
for (const msg of thread.messages) {
|
|
226
|
+
participants.set(msg.from.email, msg.from.name || msg.from.email)
|
|
227
|
+
for (const r of msg.to) participants.set(r.email, r.name || r.email)
|
|
228
|
+
}
|
|
229
|
+
const participantStr = [...participants.values()].join(', ')
|
|
230
|
+
console.log(pc.dim(`${thread.messageCount} message(s) · ${participantStr}`))
|
|
231
|
+
console.log(pc.dim(`ID: ${thread.id}`))
|
|
232
|
+
console.log(rule + '\n')
|
|
220
233
|
|
|
221
234
|
// Render each message
|
|
222
235
|
for (const msg of thread.messages) {
|
|
223
236
|
const fromStr = out.formatSender(msg.from)
|
|
224
237
|
const dateStr = out.formatDate(msg.date)
|
|
225
|
-
const flags = out.formatFlags(msg)
|
|
226
238
|
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
// Flags as dim tags
|
|
240
|
+
const flagParts: string[] = []
|
|
241
|
+
if (msg.unread) flagParts.push(pc.yellow('[unread]'))
|
|
242
|
+
if (msg.starred) flagParts.push(pc.yellow('[starred]'))
|
|
243
|
+
const flagStr = flagParts.length > 0 ? ' ' + flagParts.join(' ') : ''
|
|
244
|
+
|
|
245
|
+
console.log(pc.bold(`From: `) + fromStr + flagStr)
|
|
246
|
+
console.log(pc.dim(` To: ${msg.to.map((t) => t.email).join(', ')}`))
|
|
229
247
|
if (msg.cc && msg.cc.length > 0) {
|
|
230
|
-
|
|
248
|
+
console.log(pc.dim(` Cc: ${msg.cc.map((c) => c.email).join(', ')}`))
|
|
231
249
|
}
|
|
232
|
-
|
|
250
|
+
console.log(pc.dim(`Date: ${dateStr}`))
|
|
233
251
|
|
|
234
252
|
if (msg.attachments.length > 0) {
|
|
235
|
-
|
|
253
|
+
const attList = msg.attachments.map((a) => {
|
|
254
|
+
const size = a.size < 1024 ? `${a.size} B`
|
|
255
|
+
: a.size < 1048576 ? `${(a.size / 1024).toFixed(1)} KB`
|
|
256
|
+
: `${(a.size / 1048576).toFixed(1)} MB`
|
|
257
|
+
return `${a.filename} (${size})`
|
|
258
|
+
})
|
|
259
|
+
console.log(pc.dim(`Attachments: ${attList.join(', ')}`))
|
|
236
260
|
}
|
|
237
261
|
|
|
238
|
-
|
|
262
|
+
console.log()
|
|
239
263
|
|
|
240
264
|
const body = out.renderEmailBody(msg.body, msg.mimeType)
|
|
241
|
-
|
|
242
|
-
|
|
265
|
+
console.log(body)
|
|
266
|
+
console.log('\n' + rule + '\n')
|
|
243
267
|
}
|
|
244
268
|
})
|
|
245
269
|
|
|
@@ -287,7 +311,7 @@ export function registerMailCommands(cli: Goke) {
|
|
|
287
311
|
const parseEmails = (str: string) =>
|
|
288
312
|
str.split(',').map((e) => e.trim()).filter(Boolean).map((email) => ({ email }))
|
|
289
313
|
|
|
290
|
-
const {
|
|
314
|
+
const { client } = await getClient(options.account)
|
|
291
315
|
|
|
292
316
|
const result = await client.sendMessage({
|
|
293
317
|
to: parseEmails(options.to),
|
|
@@ -298,8 +322,6 @@ export function registerMailCommands(cli: Goke) {
|
|
|
298
322
|
fromEmail: options.from,
|
|
299
323
|
})
|
|
300
324
|
|
|
301
|
-
await cache.invalidateThreadLists(email)
|
|
302
|
-
|
|
303
325
|
out.printYaml(result)
|
|
304
326
|
out.success(`Sent to ${options.to}`)
|
|
305
327
|
})
|
|
@@ -316,16 +338,6 @@ export function registerMailCommands(cli: Goke) {
|
|
|
316
338
|
.option('--all', 'Reply all (include all original recipients)')
|
|
317
339
|
.option('--from <from>', z.string().describe('Send-as alias email'))
|
|
318
340
|
.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
341
|
let body = options.body ?? ''
|
|
330
342
|
if (options.bodyFile) {
|
|
331
343
|
if (options.bodyFile === '-') {
|
|
@@ -344,49 +356,20 @@ export function registerMailCommands(cli: Goke) {
|
|
|
344
356
|
process.exit(1)
|
|
345
357
|
}
|
|
346
358
|
|
|
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
|
-
}
|
|
359
|
+
const { client } = await getClient(options.account)
|
|
374
360
|
|
|
375
|
-
const
|
|
361
|
+
const cc = options.cc
|
|
362
|
+
? options.cc.split(',').map((e: string) => ({ email: e.trim() })).filter((e: { email: string }) => e.email)
|
|
363
|
+
: undefined
|
|
376
364
|
|
|
377
|
-
const result = await client.
|
|
378
|
-
|
|
379
|
-
subject: lastMsg.subject.startsWith('Re:') ? lastMsg.subject : `Re: ${lastMsg.subject}`,
|
|
365
|
+
const result = await client.replyToThread({
|
|
366
|
+
threadId,
|
|
380
367
|
body,
|
|
368
|
+
replyAll: options.all,
|
|
381
369
|
cc,
|
|
382
|
-
threadId,
|
|
383
|
-
inReplyTo: lastMsg.messageId,
|
|
384
|
-
references: refs || undefined,
|
|
385
370
|
fromEmail: options.from,
|
|
386
371
|
})
|
|
387
|
-
|
|
388
|
-
await cache.invalidateThread(email, threadId)
|
|
389
|
-
await cache.invalidateThreadLists(email)
|
|
372
|
+
if (result instanceof Error) handleCommandError(result)
|
|
390
373
|
|
|
391
374
|
out.printYaml(result)
|
|
392
375
|
out.success('Reply sent')
|
|
@@ -407,42 +390,20 @@ export function registerMailCommands(cli: Goke) {
|
|
|
407
390
|
process.exit(1)
|
|
408
391
|
}
|
|
409
392
|
|
|
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
393
|
const recipients = options.to
|
|
434
394
|
.split(',')
|
|
435
395
|
.map((e: string) => ({ email: e.trim() }))
|
|
436
396
|
.filter((e: { email: string }) => e.email)
|
|
437
397
|
|
|
438
|
-
const
|
|
398
|
+
const { client } = await getClient(options.account)
|
|
399
|
+
|
|
400
|
+
const result = await client.forwardThread({
|
|
401
|
+
threadId,
|
|
439
402
|
to: recipients,
|
|
440
|
-
|
|
441
|
-
body: fullBody,
|
|
403
|
+
body: options.body,
|
|
442
404
|
fromEmail: options.from,
|
|
443
405
|
})
|
|
444
|
-
|
|
445
|
-
await cache.invalidateThreadLists(email)
|
|
406
|
+
if (result instanceof Error) handleCommandError(result)
|
|
446
407
|
|
|
447
408
|
out.printYaml(result)
|
|
448
409
|
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({
|