zele 0.2.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.
Files changed (158) hide show
  1. package/README.md +38 -1
  2. package/bin/zele +27 -0
  3. package/dist/api-utils.d.ts +51 -2
  4. package/dist/api-utils.js +89 -3
  5. package/dist/api-utils.js.map +1 -1
  6. package/dist/auth.d.ts +27 -6
  7. package/dist/auth.js +185 -129
  8. package/dist/auth.js.map +1 -1
  9. package/dist/calendar-client.d.ts +16 -9
  10. package/dist/calendar-client.js +163 -59
  11. package/dist/calendar-client.js.map +1 -1
  12. package/dist/cli.js +28 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/attachment.js +17 -15
  15. package/dist/commands/attachment.js.map +1 -1
  16. package/dist/commands/auth-cmd.js +20 -9
  17. package/dist/commands/auth-cmd.js.map +1 -1
  18. package/dist/commands/calendar.js +67 -78
  19. package/dist/commands/calendar.js.map +1 -1
  20. package/dist/commands/draft.js +25 -18
  21. package/dist/commands/draft.js.map +1 -1
  22. package/dist/commands/label.js +33 -45
  23. package/dist/commands/label.js.map +1 -1
  24. package/dist/commands/mail-actions.js +11 -13
  25. package/dist/commands/mail-actions.js.map +1 -1
  26. package/dist/commands/mail.js +114 -128
  27. package/dist/commands/mail.js.map +1 -1
  28. package/dist/commands/profile.js +18 -21
  29. package/dist/commands/profile.js.map +1 -1
  30. package/dist/commands/watch.d.ts +2 -0
  31. package/dist/commands/watch.js +73 -0
  32. package/dist/commands/watch.js.map +1 -0
  33. package/dist/db.js +12 -13
  34. package/dist/db.js.map +1 -1
  35. package/dist/generated/browser.d.ts +12 -27
  36. package/dist/generated/client.d.ts +13 -28
  37. package/dist/generated/client.js +1 -1
  38. package/dist/generated/commonInputTypes.d.ts +90 -26
  39. package/dist/generated/enums.d.ts +0 -4
  40. package/dist/generated/enums.js +0 -3
  41. package/dist/generated/enums.js.map +1 -1
  42. package/dist/generated/internal/class.d.ts +22 -55
  43. package/dist/generated/internal/class.js +12 -4
  44. package/dist/generated/internal/class.js.map +1 -1
  45. package/dist/generated/internal/prismaNamespace.d.ts +272 -511
  46. package/dist/generated/internal/prismaNamespace.js +54 -66
  47. package/dist/generated/internal/prismaNamespace.js.map +1 -1
  48. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +60 -74
  49. package/dist/generated/internal/prismaNamespaceBrowser.js +50 -62
  50. package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  51. package/dist/generated/models/Account.d.ts +1637 -0
  52. package/dist/generated/models/Account.js +2 -0
  53. package/dist/generated/models/Account.js.map +1 -0
  54. package/dist/generated/models/CalendarList.d.ts +1161 -0
  55. package/dist/generated/models/CalendarList.js +2 -0
  56. package/dist/generated/models/CalendarList.js.map +1 -0
  57. package/dist/generated/models/Label.d.ts +1161 -0
  58. package/dist/generated/models/Label.js +2 -0
  59. package/dist/generated/models/Label.js.map +1 -0
  60. package/dist/generated/models/Profile.d.ts +1269 -0
  61. package/dist/generated/models/Profile.js +2 -0
  62. package/dist/generated/models/Profile.js.map +1 -0
  63. package/dist/generated/models/SyncState.d.ts +1130 -0
  64. package/dist/generated/models/SyncState.js +2 -0
  65. package/dist/generated/models/SyncState.js.map +1 -0
  66. package/dist/generated/models/Thread.d.ts +1608 -0
  67. package/dist/generated/models/Thread.js +2 -0
  68. package/dist/generated/models/Thread.js.map +1 -0
  69. package/dist/generated/models.d.ts +6 -9
  70. package/dist/gmail-client.d.ts +119 -94
  71. package/dist/gmail-client.js +862 -315
  72. package/dist/gmail-client.js.map +1 -1
  73. package/dist/mail-tui.d.ts +1 -0
  74. package/dist/mail-tui.js +517 -0
  75. package/dist/mail-tui.js.map +1 -0
  76. package/dist/output.d.ts +6 -4
  77. package/dist/output.js +124 -17
  78. package/dist/output.js.map +1 -1
  79. package/package.json +39 -11
  80. package/schema.prisma +81 -113
  81. package/src/api-utils.ts +103 -5
  82. package/src/auth.ts +224 -143
  83. package/src/calendar-client.ts +196 -89
  84. package/src/cli.ts +32 -1
  85. package/src/commands/attachment.ts +18 -19
  86. package/src/commands/auth-cmd.ts +19 -9
  87. package/src/commands/calendar.ts +42 -85
  88. package/src/commands/draft.ts +19 -22
  89. package/src/commands/label.ts +21 -57
  90. package/src/commands/mail-actions.ts +11 -19
  91. package/src/commands/mail.ts +104 -149
  92. package/src/commands/profile.ts +12 -28
  93. package/src/commands/watch.ts +88 -0
  94. package/src/db.ts +13 -16
  95. package/src/generated/browser.ts +49 -0
  96. package/src/generated/client.ts +71 -0
  97. package/src/generated/commonInputTypes.ts +332 -0
  98. package/src/generated/enums.ts +17 -0
  99. package/src/generated/internal/class.ts +250 -0
  100. package/src/generated/internal/prismaNamespace.ts +1198 -0
  101. package/src/generated/internal/prismaNamespaceBrowser.ts +169 -0
  102. package/src/generated/models/Account.ts +1848 -0
  103. package/src/generated/models/CalendarList.ts +1331 -0
  104. package/src/generated/models/Label.ts +1331 -0
  105. package/src/generated/models/Profile.ts +1439 -0
  106. package/src/generated/models/SyncState.ts +1300 -0
  107. package/src/generated/models/Thread.ts +1787 -0
  108. package/src/generated/models.ts +17 -0
  109. package/src/gmail-client.test.ts +59 -0
  110. package/src/gmail-client.ts +1034 -422
  111. package/src/mail-tui.tsx +1061 -0
  112. package/src/output.test.ts +1093 -0
  113. package/src/output.ts +128 -20
  114. package/src/schema.sql +58 -68
  115. package/src/test-fixtures/email-html/safe-claude-event.html +28 -0
  116. package/src/test-fixtures/email-html/safe-product-announcement.html +25 -0
  117. package/src/test-fixtures/email-html/safe-tracked-links.html +27 -0
  118. package/src/test-fixtures/email-html-snapshots/safe-claude-event.html.md +9 -0
  119. package/src/test-fixtures/email-html-snapshots/safe-product-announcement.html.md +13 -0
  120. package/src/test-fixtures/email-html-snapshots/safe-tracked-links.html.md +7 -0
  121. package/AGENTS.md +0 -26
  122. package/CHANGELOG.md +0 -36
  123. package/dist/generated/models/accounts.d.ts +0 -2000
  124. package/dist/generated/models/accounts.js +0 -2
  125. package/dist/generated/models/accounts.js.map +0 -1
  126. package/dist/generated/models/calendar_events.d.ts +0 -1433
  127. package/dist/generated/models/calendar_events.js +0 -2
  128. package/dist/generated/models/calendar_events.js.map +0 -1
  129. package/dist/generated/models/calendar_lists.d.ts +0 -1131
  130. package/dist/generated/models/calendar_lists.js +0 -2
  131. package/dist/generated/models/calendar_lists.js.map +0 -1
  132. package/dist/generated/models/label_counts.d.ts +0 -1131
  133. package/dist/generated/models/label_counts.js +0 -2
  134. package/dist/generated/models/label_counts.js.map +0 -1
  135. package/dist/generated/models/labels.d.ts +0 -1131
  136. package/dist/generated/models/labels.js +0 -2
  137. package/dist/generated/models/labels.js.map +0 -1
  138. package/dist/generated/models/profiles.d.ts +0 -1131
  139. package/dist/generated/models/profiles.js +0 -2
  140. package/dist/generated/models/profiles.js.map +0 -1
  141. package/dist/generated/models/sync_states.d.ts +0 -1107
  142. package/dist/generated/models/sync_states.js +0 -2
  143. package/dist/generated/models/sync_states.js.map +0 -1
  144. package/dist/generated/models/thread_lists.d.ts +0 -1404
  145. package/dist/generated/models/thread_lists.js +0 -2
  146. package/dist/generated/models/thread_lists.js.map +0 -1
  147. package/dist/generated/models/threads.d.ts +0 -1247
  148. package/dist/generated/models/threads.js +0 -2
  149. package/dist/generated/models/threads.js.map +0 -1
  150. package/dist/gmail-cache.d.ts +0 -60
  151. package/dist/gmail-cache.js +0 -264
  152. package/dist/gmail-cache.js.map +0 -1
  153. package/docs/gogcli-gmail-implementation.md +0 -599
  154. package/scripts/test-device-code-clients.ts +0 -186
  155. package/scripts/test-micropython-scopes.ts +0 -72
  156. package/scripts/test-oauth-clients.ts +0 -257
  157. package/src/gmail-cache.ts +0 -339
  158. package/tsconfig.json +0 -16
@@ -1,599 +0,0 @@
1
- # gogcli Gmail Implementation Reference
2
-
3
- Reference for reimplementing gogcli's Gmail commands in TypeScript using Google JS APIs.
4
-
5
- Source: `github.com/steipete/gogcli` (Go, v0.9.0)
6
- JS equivalent: `googleapis` npm package (`google.gmail('v1')`)
7
-
8
-
9
- ## Architecture Overview
10
-
11
- ```
12
- CLI args
13
- |
14
- v
15
- Command struct (flags/args via kong parser)
16
- |
17
- v
18
- cmd.Run(ctx, flags)
19
- |
20
- v
21
- newGmailService(ctx, account)
22
- | reads OAuth creds from keyring
23
- | creates oauth2.TokenSource (auto-refresh)
24
- | wraps HTTP transport with RetryTransport
25
- | returns *gmail.Service
26
- v
27
- svc.Users.{Resource}.{Method}("me", ...).Do()
28
- | (official Google Go API client)
29
- v
30
- Output: --json -> JSON to stdout | text -> tab-separated table
31
- ```
32
-
33
- Key design: every command follows the same pattern:
34
- 1. Validate args/flags
35
- 2. Get authenticated Gmail service
36
- 3. Call Google API
37
- 4. Format output (JSON or text)
38
-
39
-
40
- ## Auth & Service Creation
41
-
42
- **Go implementation** (`googleapi/gmail.go`, `googleapi/client.go`):
43
-
44
- ```
45
- NewGmail(ctx, email)
46
- -> optionsForAccount(ctx, ServiceGmail, email)
47
- -> resolve OAuth client name for email
48
- -> read client credentials (clientID, clientSecret) from config
49
- -> load refresh token from keyring/secrets store
50
- -> create oauth2.TokenSource with auto-refresh
51
- -> wrap in RetryTransport (handles 429 + 5xx)
52
- -> return []option.ClientOption with authenticated HTTP client
53
- -> gmail.NewService(ctx, opts...)
54
- ```
55
-
56
- **JS equivalent**:
57
- ```ts
58
- import { google } from 'googleapis';
59
-
60
- const oauth2Client = new google.auth.OAuth2(clientId, clientSecret);
61
- oauth2Client.setCredentials({ refresh_token: refreshToken });
62
- const gmail = google.gmail({ version: 'v1', auth: oauth2Client });
63
- ```
64
-
65
- The Go version uses a package-level `var newGmailService` that can be swapped in tests.
66
- In JS, use dependency injection or a factory function.
67
-
68
-
69
- ## Retry & Error Handling
70
-
71
- **RetryTransport** (`googleapi/transport.go`):
72
- - Wraps HTTP transport at the `RoundTrip` level
73
- - **429 (rate limit)**: up to 3 retries, exponential backoff (1s base) with jitter, respects `Retry-After` header
74
- - **5xx (server error)**: up to 1 retry, 1s delay
75
- - **Circuit breaker**: stops all requests if too many consecutive failures
76
- - Request bodies are buffered for replay on retry
77
-
78
- **JS equivalent**: use `axios-retry` or `gaxios` retry config, or implement manually:
79
- ```ts
80
- const backoff = (attempt: number) => {
81
- const base = 1000 * Math.pow(2, attempt);
82
- const jitter = Math.random() * base / 2;
83
- return base + jitter;
84
- };
85
- ```
86
-
87
- **Error types** (`googleapi/errors.go`):
88
- - `AuthRequiredError` - no token found, need to re-auth
89
- - `RateLimitError` - 429 exceeded after retries
90
- - `CircuitBreakerError` - too many failures
91
- - `QuotaExceededError` - API quota hit
92
- - `NotFoundError` - resource not found
93
- - `PermissionDeniedError` - insufficient scopes
94
-
95
-
96
- ## Output System
97
-
98
- **Two modes** (`outfmt/outfmt.go`):
99
- - `--json`: `outfmt.WriteJSON(os.Stdout, payload)` - pretty-printed JSON, no HTML escaping
100
- - `--plain`: raw TSV output (no alignment)
101
- - Default: aligned tab-separated table via `tabwriter`
102
-
103
- **Pattern in every command**:
104
- ```go
105
- if outfmt.IsJSON(ctx) {
106
- return outfmt.WriteJSON(os.Stdout, map[string]any{
107
- "threads": items,
108
- "nextPageToken": resp.NextPageToken,
109
- })
110
- }
111
- // else: print table
112
- w, flush := tableWriter(ctx)
113
- defer flush()
114
- fmt.Fprintln(w, "ID\tDATE\tFROM\tSUBJECT")
115
- for _, it := range items {
116
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", it.ID, it.Date, it.From, it.Subject)
117
- }
118
- ```
119
-
120
-
121
- ## Pagination
122
-
123
- **Pattern used everywhere**:
124
- - `--max` (alias `--limit`): passed as `MaxResults(n)` to the API call
125
- - `--page`: passed as `PageToken(token)` to the API call
126
- - Response includes `NextPageToken`
127
- - In JSON mode: `nextPageToken` field in output
128
- - In text mode: hint printed to stderr: `# Next page: --page <token>`
129
-
130
- **Go implementation** (`output_helpers.go`):
131
- ```go
132
- func printNextPageHint(u *ui.UI, nextPageToken string) {
133
- if nextPageToken == "" { return }
134
- u.Err().Printf("# Next page: --page %s", nextPageToken)
135
- }
136
- ```
137
-
138
- **JS equivalent**: The Google JS API supports the same pattern:
139
- ```ts
140
- const res = await gmail.users.threads.list({
141
- userId: 'me', q: query, maxResults: max, pageToken: page
142
- });
143
- // res.data.nextPageToken
144
- ```
145
-
146
- The CLI does NOT auto-paginate. It returns one page and tells the user how to get the next one.
147
-
148
-
149
- ## Concurrent Fetching
150
-
151
- Search endpoints only return IDs. The CLI fetches details concurrently.
152
-
153
- **Pattern** (`gmail.go:fetchThreadDetails`, `gmail_messages.go:fetchMessageDetails`):
154
- ```go
155
- const maxConcurrency = 10
156
- sem := make(chan struct{}, maxConcurrency)
157
-
158
- for i, t := range threads {
159
- go func(idx int, threadID string) {
160
- sem <- struct{}{} // acquire
161
- defer func() { <-sem }() // release
162
-
163
- thread, err := svc.Users.Threads.Get("me", threadID).
164
- Format("metadata").
165
- MetadataHeaders("From", "Subject", "Date").
166
- Do()
167
- // ...
168
- }(i, t.Id)
169
- }
170
- ```
171
-
172
- - Bounded to 10 concurrent requests to avoid rate limiting
173
- - Results collected in order (indexed array)
174
- - On error: re-runs sequentially to find first error
175
-
176
- **JS equivalent**: use `Promise.all` with a concurrency limiter like `p-limit`:
177
- ```ts
178
- import pLimit from 'p-limit';
179
- const limit = pLimit(10);
180
- const details = await Promise.all(
181
- threadIds.map(id => limit(() => gmail.users.threads.get({
182
- userId: 'me', id, format: 'metadata',
183
- metadataHeaders: ['From', 'Subject', 'Date']
184
- })))
185
- );
186
- ```
187
-
188
-
189
- ## Label Resolution
190
-
191
- Labels are referenced by name or ID. The CLI resolves names to IDs before API calls.
192
-
193
- **Two helper maps**:
194
- - `fetchLabelNameToID(svc)` - for modifying labels (name -> ID)
195
- - `fetchLabelIDToName(svc)` - for display (ID -> name)
196
-
197
- Both call `svc.Users.Labels.List("me")` and build maps.
198
- Name lookups are case-insensitive (`strings.ToLower`).
199
-
200
- **JS equivalent**:
201
- ```ts
202
- const labelsRes = await gmail.users.labels.list({ userId: 'me' });
203
- const nameToId = new Map(
204
- labelsRes.data.labels.map(l => [l.name.toLowerCase(), l.id])
205
- );
206
- ```
207
-
208
-
209
- ## Command-by-Command Breakdown
210
-
211
-
212
- ### gmail search
213
-
214
- **API**: `GET /gmail/v1/users/me/threads?q={query}&maxResults={max}&pageToken={page}`
215
-
216
- **Go call**: `svc.Users.Threads.List("me").Q(query).MaxResults(max).PageToken(page).Do()`
217
-
218
- **Then**: For each thread in response, concurrently fetch:
219
- `svc.Users.Threads.Get("me", threadID).Format("metadata").MetadataHeaders("From", "Subject", "Date").Do()`
220
-
221
- **Output fields**: `id`, `date`, `from`, `subject`, `labels`, `messageCount`
222
-
223
- **Flags**:
224
- | Flag | Type | Default | Description |
225
- |------|------|---------|-------------|
226
- | `query` | arg[] | required | Gmail search query (joined with spaces) |
227
- | `--max` | int | 10 | Max results per page |
228
- | `--page` | string | - | Page token for next page |
229
- | `--oldest` | bool | false | Show first message date instead of last |
230
- | `--timezone` | string | local | IANA timezone for dates |
231
- | `--local` | bool | false | Force local timezone |
232
-
233
- **Date handling**: Dates parsed with `net/mail.ParseDate()`, formatted as `2006-01-02 15:04` in the output timezone.
234
-
235
-
236
- ### gmail messages search
237
-
238
- **API**: `GET /gmail/v1/users/me/messages?q={query}&maxResults={max}&pageToken={page}`
239
-
240
- **Go call**: `svc.Users.Messages.List("me").Q(query).MaxResults(max).PageToken(page).Fields("messages(id,threadId),nextPageToken").Do()`
241
-
242
- **Then**: For each message, concurrently fetch:
243
- - Without body: `svc.Users.Messages.Get("me", id).Format("metadata").MetadataHeaders("From", "Subject", "Date").Fields("id,threadId,labelIds,payload(headers)").Do()`
244
- - With body (`--include-body`): `svc.Users.Messages.Get("me", id).Format("full").Do()`
245
-
246
- **Output fields**: `id`, `threadId`, `date`, `from`, `subject`, `labels`, `body` (optional)
247
-
248
- **Extra flags**:
249
- | Flag | Description |
250
- |------|-------------|
251
- | `--include-body` | Include decoded message body (truncated to 200 chars in text mode) |
252
-
253
-
254
- ### gmail get
255
-
256
- **API**: `GET /gmail/v1/users/me/messages/{messageId}?format={format}`
257
-
258
- **Go call**: `svc.Users.Messages.Get("me", messageID).Format(format).Do()`
259
-
260
- **Formats**:
261
- - `full` (default): full message with body + attachments
262
- - `metadata`: headers only (default headers: From, To, Subject, Date, List-Unsubscribe)
263
- - `raw`: base64url-encoded RFC822
264
-
265
- **JSON output includes**:
266
- - `message` - raw API response
267
- - `headers` - flattened map: `{from, to, cc, bcc, subject, date}`
268
- - `body` - decoded body text (full format only)
269
- - `attachments` - list of `{filename, size, sizeHuman, mimeType, attachmentId}`
270
- - `unsubscribe` - best unsubscribe link from `List-Unsubscribe` header
271
-
272
- **Body extraction** (`gmail_thread.go`): walks MIME tree, prefers `text/plain` over `text/html`, decodes base64/quoted-printable, handles charset conversion.
273
-
274
- **Unsubscribe link parsing**: extracts `<url>` from `List-Unsubscribe` header, prefers HTTPS > HTTP > mailto.
275
-
276
-
277
- ### gmail send
278
-
279
- **API**: `POST /gmail/v1/users/me/messages/send` with `{raw: base64url(RFC822)}`
280
-
281
- **Go call**: `svc.Users.Messages.Send("me", &gmail.Message{Raw: encoded, ThreadId: threadID}).Do()`
282
-
283
- **RFC822 construction** (`gmail_mime.go`):
284
- - Built from scratch, no external mail library
285
- - Handles: From, To, Cc, Bcc, Reply-To, Subject, Date, Message-ID, MIME-Version
286
- - Reply headers: In-Reply-To, References (from `fetchReplyInfo`)
287
- - Body: `text/plain` | `text/html` | `multipart/alternative` (both)
288
- - Attachments: `multipart/mixed` wrapping `multipart/alternative` + attachment parts
289
- - Encoding: 7bit for text, base64 for attachments
290
- - Subject encoding: RFC 2047 `=?UTF-8?B?...?=` when non-ASCII
291
-
292
- **Reply flow**:
293
- 1. If `--reply-to-message-id`: fetch original message metadata (Message-ID, References, From, To, Cc, Reply-To)
294
- 2. If `--thread-id`: fetch thread, pick latest message
295
- 3. Set `In-Reply-To` and `References` headers
296
- 4. Set `ThreadId` on the sent message
297
- 5. If `--reply-all`: auto-populate To/Cc from original (RFC 5322: Reply-To > From)
298
-
299
- **Send-as alias validation**: `svc.Users.Settings.SendAs.Get("me", fromEmail).Do()` - checks `verificationStatus == "accepted"`
300
-
301
- **Tracking**: injects 1x1 pixel `<img>` before `</body>` in HTML body, generates encrypted tracking ID
302
-
303
- **Flags**:
304
- | Flag | Type | Description |
305
- |------|------|-------------|
306
- | `--to` | string | Recipients (comma-separated) |
307
- | `--cc` | string | CC recipients |
308
- | `--bcc` | string | BCC recipients |
309
- | `--subject` | string | Subject (required) |
310
- | `--body` | string | Plain text body |
311
- | `--body-file` | string | Body from file (`-` for stdin) |
312
- | `--body-html` | string | HTML body |
313
- | `--reply-to-message-id` | string | Reply to message ID |
314
- | `--thread-id` | string | Reply within thread |
315
- | `--reply-all` | bool | Auto-populate recipients from original |
316
- | `--reply-to` | string | Reply-To header |
317
- | `--attach` | string[] | File paths (repeatable) |
318
- | `--from` | string | Send-as alias |
319
- | `--track` | bool | Enable open tracking |
320
- | `--track-split` | bool | Separate sends per recipient for tracking |
321
-
322
-
323
- ### gmail thread get
324
-
325
- **API**: `GET /gmail/v1/users/me/threads/{threadId}?format=full`
326
-
327
- **Go call**: `svc.Users.Threads.Get("me", threadID).Format("full").Do()`
328
-
329
- **Displays**: all messages in thread with headers, body (truncated to 500 chars unless `--full`), attachments
330
-
331
- **Optional download**: for each message with attachments, calls:
332
- `svc.Users.Messages.Attachments.Get("me", messageId, attachmentId).Do()`
333
-
334
- **Attachment caching**: checks if file already exists at destination with matching size before downloading.
335
-
336
- **Flags**:
337
- | Flag | Description |
338
- |------|-------------|
339
- | `--download` | Download all attachments |
340
- | `--full` | Show full message bodies (no truncation) |
341
- | `--out-dir` | Output directory for attachments (default: current dir) |
342
-
343
-
344
- ### gmail thread modify
345
-
346
- **API**: `POST /gmail/v1/users/me/threads/{threadId}/modify` with `{addLabelIds, removeLabelIds}`
347
-
348
- **Go call**: `svc.Users.Threads.Modify("me", tid, &gmail.ModifyThreadRequest{AddLabelIds: addIDs, RemoveLabelIds: removeIDs}).Do()`
349
-
350
- Labels are resolved from names to IDs first via `fetchLabelNameToID`.
351
-
352
-
353
- ### gmail labels list
354
-
355
- **API**: `GET /gmail/v1/users/me/labels`
356
-
357
- **Go call**: `svc.Users.Labels.List("me").Do()`
358
-
359
- **Output**: `id`, `name`, `type` for each label. No pagination (returns all labels).
360
-
361
-
362
- ### gmail labels get
363
-
364
- **API**: `GET /gmail/v1/users/me/labels/{labelId}`
365
-
366
- **Go call**: `svc.Users.Labels.Get("me", id).Do()`
367
-
368
- Resolves name to ID first if needed. Output includes message/thread counts.
369
-
370
-
371
- ### gmail labels create
372
-
373
- **API**: `POST /gmail/v1/users/me/labels` with `{name, labelListVisibility: "labelShow", messageListVisibility: "show"}`
374
-
375
- **Go call**: `svc.Users.Labels.Create("me", &gmail.Label{...}).Do()`
376
-
377
- Pre-checks for duplicate names. Maps 409 Conflict errors to user-friendly messages.
378
-
379
-
380
- ### gmail labels modify
381
-
382
- **API**: `POST /gmail/v1/users/me/threads/{threadId}/modify` (per thread)
383
-
384
- Iterates over multiple thread IDs, modifying labels on each. Reports per-thread success/failure.
385
-
386
-
387
- ### gmail attachment
388
-
389
- **API**: `GET /gmail/v1/users/me/messages/{messageId}/attachments/{attachmentId}`
390
-
391
- **Go call**: `svc.Users.Messages.Attachments.Get("me", messageID, attachmentID).Do()`
392
-
393
- Returns base64url-encoded data. Decoded and written to file.
394
- Caches: skips download if file exists with matching size.
395
-
396
- **Filename**: `{messageId}_{attachmentId[:8]}_{filename}` in attachments dir.
397
-
398
-
399
- ### gmail history
400
-
401
- **API**: `GET /gmail/v1/users/me/history?startHistoryId={id}&maxResults={max}&historyTypes=messageAdded`
402
-
403
- **Go call**: `svc.Users.History.List("me").StartHistoryId(id).MaxResults(max).HistoryTypes("messageAdded").Do()`
404
-
405
- Returns message IDs that were added since the given history ID.
406
-
407
-
408
- ### gmail batch delete
409
-
410
- **API**: `POST /gmail/v1/users/me/messages/batchDelete` with `{ids: [...]}`
411
-
412
- **Go call**: `svc.Users.Messages.BatchDelete("me", &gmail.BatchDeleteMessagesRequest{Ids: ids}).Do()`
413
-
414
- Single API call for multiple message IDs. Permanently deletes (not trash).
415
-
416
-
417
- ### gmail batch modify
418
-
419
- **API**: `POST /gmail/v1/users/me/messages/batchModify` with `{ids, addLabelIds, removeLabelIds}`
420
-
421
- **Go call**: `svc.Users.Messages.BatchModify("me", &gmail.BatchModifyMessagesRequest{...}).Do()`
422
-
423
- Single API call. Resolves label names to IDs first.
424
-
425
-
426
- ### gmail drafts list
427
-
428
- **API**: `GET /gmail/v1/users/me/drafts?maxResults={max}&pageToken={page}`
429
-
430
- **Go call**: `svc.Users.Drafts.List("me").MaxResults(max).PageToken(page).Do()`
431
-
432
- **Output**: `id`, `messageId`, `threadId` per draft. Supports pagination.
433
-
434
-
435
- ### gmail drafts get
436
-
437
- **API**: `GET /gmail/v1/users/me/drafts/{draftId}?format=full`
438
-
439
- **Go call**: `svc.Users.Drafts.Get("me", draftID).Format("full").Do()`
440
-
441
- Shows headers, body, attachments. Optional `--download` for attachments.
442
-
443
-
444
- ### gmail drafts create
445
-
446
- **API**: `POST /gmail/v1/users/me/drafts` with `{message: {raw: base64url(RFC822)}}`
447
-
448
- **Go call**: `svc.Users.Drafts.Create("me", &gmail.Draft{Message: msg}).Do()`
449
-
450
- Same RFC822 building as `send`, but `To` is optional (`allowMissingTo: true`).
451
-
452
-
453
- ### gmail drafts update
454
-
455
- **API**: `PUT /gmail/v1/users/me/drafts/{draftId}` with `{id, message: {raw: base64url(RFC822)}}`
456
-
457
- **Go call**: `svc.Users.Drafts.Update("me", draftID, &gmail.Draft{Id: draftID, Message: msg}).Do()`
458
-
459
- Fetches existing draft first to preserve thread ID and To if not explicitly set.
460
-
461
-
462
- ### gmail drafts delete
463
-
464
- **API**: `DELETE /gmail/v1/users/me/drafts/{draftId}`
465
-
466
- **Go call**: `svc.Users.Drafts.Delete("me", draftID).Do()`
467
-
468
- Requires confirmation (unless `--force`).
469
-
470
-
471
- ### gmail drafts send
472
-
473
- **API**: `POST /gmail/v1/users/me/drafts/send` with `{id: draftId}`
474
-
475
- **Go call**: `svc.Users.Drafts.Send("me", &gmail.Draft{Id: draftID}).Do()`
476
-
477
-
478
- ### gmail url
479
-
480
- **API**: none (computed locally)
481
-
482
- **Format**: `https://mail.google.com/mail/u/0/#inbox/{threadId}`
483
-
484
-
485
- ### gmail settings filters list/get/create/delete
486
-
487
- **APIs**:
488
- - `GET /gmail/v1/users/me/settings/filters`
489
- - `GET /gmail/v1/users/me/settings/filters/{filterId}`
490
- - `POST /gmail/v1/users/me/settings/filters`
491
- - `DELETE /gmail/v1/users/me/settings/filters/{filterId}`
492
-
493
- Create takes criteria (from, to, subject, query, hasAttachment) and actions (addLabel, removeLabel, archive, markRead, star, forward, trash, neverSpam, important).
494
-
495
-
496
- ### gmail settings delegates list/get/add/remove
497
-
498
- **APIs**:
499
- - `GET /gmail/v1/users/me/settings/delegates`
500
- - `GET /gmail/v1/users/me/settings/delegates/{delegateEmail}`
501
- - `POST /gmail/v1/users/me/settings/delegates`
502
- - `DELETE /gmail/v1/users/me/settings/delegates/{delegateEmail}`
503
-
504
-
505
- ### gmail settings forwarding list/get/create/delete
506
-
507
- **APIs**:
508
- - `GET /gmail/v1/users/me/settings/forwardingAddresses`
509
- - `GET /gmail/v1/users/me/settings/forwardingAddresses/{email}`
510
- - `POST /gmail/v1/users/me/settings/forwardingAddresses`
511
- - `DELETE /gmail/v1/users/me/settings/forwardingAddresses/{email}`
512
-
513
-
514
- ### gmail settings autoforward get/update
515
-
516
- **APIs**:
517
- - `GET /gmail/v1/users/me/settings/autoForwarding`
518
- - `PUT /gmail/v1/users/me/settings/autoForwarding`
519
-
520
- Update takes: enabled, emailAddress, disposition (leaveInInbox, archive, trash, markRead).
521
-
522
-
523
- ### gmail settings sendas list/get/create/verify/delete/update
524
-
525
- **APIs**:
526
- - `GET /gmail/v1/users/me/settings/sendAs`
527
- - `GET /gmail/v1/users/me/settings/sendAs/{email}`
528
- - `POST /gmail/v1/users/me/settings/sendAs`
529
- - `POST /gmail/v1/users/me/settings/sendAs/{email}/verify`
530
- - `DELETE /gmail/v1/users/me/settings/sendAs/{email}`
531
- - `PUT /gmail/v1/users/me/settings/sendAs/{email}`
532
-
533
-
534
- ### gmail settings vacation get/update
535
-
536
- **APIs**:
537
- - `GET /gmail/v1/users/me/settings/vacation`
538
- - `PUT /gmail/v1/users/me/settings/vacation`
539
-
540
- Update takes: enableAutoReply, responseSubject, responseBodyHtml/PlainText, startTime, endTime, restrictToContacts, restrictToDomain.
541
-
542
-
543
- ### gmail settings watch start/status/renew/stop/serve
544
-
545
- **APIs**:
546
- - `POST /gmail/v1/users/me/watch` (start)
547
- - `POST /gmail/v1/users/me/stop` (stop)
548
- - Watch status stored locally in config file
549
-
550
- **Serve** starts a local HTTP server that receives Pub/Sub push notifications. Decodes the notification, fetches new messages via History API, and optionally forwards to a webhook URL.
551
-
552
-
553
- ## Helpers to Reimplement
554
-
555
- ### Body extraction
556
- Walk the MIME part tree recursively. Prefer `text/plain` > `text/html`. Decode `base64` / `quoted-printable`. Handle charset via `Content-Type; charset=...`.
557
-
558
- ### Attachment collection
559
- Walk MIME parts. Any part with `body.attachmentId` is an attachment.
560
-
561
- ### Date formatting
562
- Parse RFC 2822 dates, convert to target timezone, format as `YYYY-MM-DD HH:mm`.
563
-
564
- ### Email address parsing
565
- Use `mail.ParseAddressList()` (Go) / `email-addresses` npm package. Fallback: manual comma-split + `<email>` extraction.
566
-
567
- ### CSV splitting
568
- `splitCSV(s)`: split on comma, trim whitespace, filter empty strings.
569
-
570
-
571
- ## Testing Strategy
572
-
573
- The Go tests use:
574
- 1. `httptest.NewServer` - fake HTTP server returning canned JSON
575
- 2. Real Google API client pointed at fake server (`option.WithEndpoint(srv.URL)`)
576
- 3. Service constructor swapped via package-level var
577
- 4. `captureStdout(t, fn)` to capture output
578
- 5. No mock libraries
579
-
580
- **JS equivalent**:
581
- ```ts
582
- // Use nock or msw to intercept HTTP requests
583
- import nock from 'nock';
584
- nock('https://gmail.googleapis.com')
585
- .get('/gmail/v1/users/me/threads')
586
- .query({ q: 'test', maxResults: '10' })
587
- .reply(200, { threads: [...], nextPageToken: 'abc' });
588
- ```
589
-
590
-
591
- ## Key Differences for JS Implementation
592
-
593
- 1. **No goroutines**: Use `Promise.all` + `p-limit` for concurrent fetching
594
- 2. **No tabwriter**: Use a table formatting library or simple string padding
595
- 3. **RFC822 building**: Use `nodemailer` or `mailcomposer` instead of building manually
596
- 4. **OAuth**: `googleapis` handles token refresh automatically with `OAuth2Client`
597
- 5. **Retry**: Use `gaxios` retry config or wrap with custom retry logic
598
- 6. **Body parsing**: `gmail` API returns base64url in JS too; use `Buffer.from(data, 'base64url')`
599
- 7. **Keyring**: Use `keytar` or OS-specific credential storage