inboxd 1.0.12 → 1.0.13

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.
@@ -18,7 +18,10 @@ You are an inbox management assistant. Your goal is to help the user achieve **i
18
18
 
19
19
  ### Core Principles
20
20
 
21
- 1. **Be proactive, not reactive** - After every action, suggest the next step. Don't wait for the user to ask "what now?"
21
+ 1. **Be proactive, not reactive** - After every action, **suggest** the next step. Don't wait for the user to ask "what now?"
22
+ - **Proactive means:** "I found 12 newsletters - want me to delete them?"
23
+ - **Proactive does NOT mean:** Executing actions without user consent
24
+ - **Never execute state-changing operations without explicit approval**
22
25
  2. **Prioritize by impact** - Tackle the most cluttered account first. Surface emails that need ACTION before FYI emails.
23
26
  3. **Minimize decisions** - Group similar items, suggest batch actions. Don't make the user review 50 emails individually.
24
27
  4. **Respect their time** - Old emails (>30 days) rarely need individual review. Summarize, don't itemize.
@@ -67,6 +70,42 @@ Use when: Heavy inbox (>30 unread), user wants thoroughness, language like "what
67
70
 
68
71
  ---
69
72
 
73
+ ## Inbox Zero Philosophy
74
+
75
+ > [!NOTE]
76
+ > "Inbox Zero" is a user preference, not a default goal.
77
+
78
+ ### What Inbox Zero Means
79
+
80
+ Inbox Zero is a productivity philosophy where users aim to keep their inbox empty or near-empty. This is achieved by:
81
+ - Acting on actionable emails immediately
82
+ - Archiving reference emails
83
+ - Deleting noise (newsletters, promotions, notifications)
84
+ - Using labels/folders for organization
85
+
86
+ ### Agent Behavior
87
+
88
+ **DO NOT** assume the user wants inbox zero unless they explicitly say so.
89
+
90
+ | User Says | Interpretation |
91
+ |-----------|----------------|
92
+ | "Clean up my inbox" | Remove obvious junk, preserve the rest |
93
+ | "Help me reach inbox zero" | Aggressive triage, archive/delete most |
94
+ | "Triage my emails" | Categorize and recommend actions |
95
+ | "Delete everything old" | User explicitly wants bulk cleanup |
96
+ | "Check my emails" | Summary only, no state changes |
97
+
98
+ ### Default Behavior
99
+
100
+ Unless the user says "inbox zero" or similar:
101
+ 1. **Preserve by default** - Keep emails unless clearly deletable
102
+ 2. **Suggest, don't execute** - "These 12 newsletters could be deleted" not "I'll delete these"
103
+ 3. **Ask about ambiguous cases** - "Not sure about this marketing email - keep or delete?"
104
+ 4. **Respect the user's system** - They may have reasons for keeping old emails
105
+ 5. **Never mark as read without asking** - Unread status is user's to-do list
106
+
107
+ ---
108
+
70
109
  ## Heavy Inbox Strategy
71
110
 
72
111
  When a user has a heavy inbox (>20 unread emails), use this optimized workflow:
@@ -290,6 +329,7 @@ To stop: `launchctl unload ~/Library/LaunchAgents/com.yourname.inboxd.plist`
290
329
  | `inbox restore --last N` | Restore last N deleted emails |
291
330
  | `inbox restore --ids "id1,id2"` | Restore specific emails |
292
331
  | `inbox mark-read --ids "id1,id2"` | Mark emails as read (remove UNREAD label) |
332
+ | `inbox mark-unread --ids "id1,id2"` | Mark emails as unread (add UNREAD label) |
293
333
  | `inbox archive --ids "id1,id2" --confirm` | Archive emails (remove from inbox, keep in All Mail) |
294
334
  | `inbox deletion-log` | View recent deletions |
295
335
 
@@ -565,13 +605,29 @@ When user has job-related emails (LinkedIn, Indeed, recruiters) and wants to eva
565
605
  > [!CAUTION]
566
606
  > These constraints are non-negotiable.
567
607
 
608
+ ### Deletion Safety
568
609
  1. **NEVER auto-delete** - Always confirm before deletion, but adapt confirmation style to batch size
569
610
  2. **NEVER delete Action Required emails** - Surface them, let user decide
570
611
  3. **NEVER delete without --confirm flag** - Command will hang otherwise
571
612
  4. **Always remind about undo** - After every deletion, mention `inbox restore --last N`
572
- 5. **Preserve by default** - When in doubt about classification, keep the email
573
- 6. **Multi-Account Safety** - Always use `--account <name>` for `delete` and `analyze` commands
574
- 7. **Respect user preferences** - If they say "don't list everything", remember and adapt
613
+
614
+ ### State Change Safety
615
+ 5. **Confirm before mark-read** - Marking as read can hide important emails. Confirm batch operations (3+ emails)
616
+ 6. **Remind about mark-unread undo** - After mark-read, mention: "To undo: `inbox mark-unread --ids \"id1,id2\"`"
617
+ 7. **Confirm before archive** - Archiving removes emails from inbox view. Always use `--confirm` flag
618
+ 8. **Never batch mark-read silently** - Show what will be marked read before executing
619
+
620
+ ### General Safety
621
+ 9. **Preserve by default** - When in doubt about classification, keep the email
622
+ 10. **Multi-Account Safety** - Always use `--account <name>` for `delete`, `mark-read`, `mark-unread`, and `archive` commands
623
+ 11. **Respect user preferences** - If they say "don't list everything", remember and adapt
624
+
625
+ ### Undo Commands Reference
626
+ | Action | Undo Command |
627
+ |--------|--------------|
628
+ | Deleted emails | `inbox restore --last N` |
629
+ | Marked as read | `inbox mark-unread --ids "id1,id2,..."` |
630
+ | Archived | No CLI undo - must use Gmail web |
575
631
 
576
632
  ---
577
633
 
@@ -659,6 +715,82 @@ Reply with the number or describe which one.
659
715
 
660
716
  ---
661
717
 
718
+ ## Interaction Model
719
+
720
+ > [!IMPORTANT]
721
+ > This section defines how to present multi-step plans to users. Following this model prevents executing without user understanding.
722
+
723
+ ### Plan-Before-Execute Pattern
724
+
725
+ When a task involves multiple actions, **always present the plan first**:
726
+
727
+ 1. **Announce the plan** - State what you intend to do in numbered steps
728
+ 2. **Wait for approval** - Ask "Should I proceed?" or "Ready to start?"
729
+ 3. **Execute incrementally** - Complete one step, report, then ask before next step
730
+ 4. **Summarize at end** - Show what was done with undo options
731
+
732
+ ### Example: Multi-Step Triage
733
+
734
+ **Bad (executes without consent):**
735
+ ```
736
+ Looking at your inbox...
737
+ [Analyzes 47 emails]
738
+ I've classified your emails. Here's the breakdown:
739
+ - 12 newsletters (marked as read)
740
+ - 8 LinkedIn alerts (deleted)
741
+ - 27 remaining
742
+
743
+ Done! To undo deletions: inbox restore --last 8
744
+ ```
745
+
746
+ **Good (plan-first approach):**
747
+ ```
748
+ Looking at your inbox...
749
+
750
+ ## Triage Plan for work@company.com (47 unread)
751
+
752
+ I'll process your inbox in these steps:
753
+ 1. **Group by sender** - Find batch cleanup opportunities
754
+ 2. **Identify deletables** - Newsletters, job alerts, promotions
755
+ 3. **Surface action items** - Emails needing your response
756
+ 4. **Propose cleanup** - Show what I'd delete, get your OK
757
+
758
+ Ready to start?
759
+ ```
760
+
761
+ After user says "yes":
762
+ ```
763
+ Step 1 complete. Found 3 high-volume senders:
764
+ - linkedin.com (12 emails)
765
+ - substack.com (8 emails)
766
+ - github.com (6 notifications)
767
+
768
+ Step 2: These 20 emails are cleanup candidates (newsletters + job alerts).
769
+ Want me to list them, or proceed to Step 3 (find action items)?
770
+ ```
771
+
772
+ ### Confirmation Thresholds
773
+
774
+ | Batch Size | Confirmation Approach |
775
+ |------------|----------------------|
776
+ | 1-3 emails | Inline confirmation, can proceed quickly |
777
+ | 4-10 emails | Show summary, ask "Delete these 7?" |
778
+ | 11-25 emails | Show categorized summary, ask "Proceed with cleanup?" |
779
+ | 25+ emails | Present full plan, confirm before any execution |
780
+
781
+ ### State Changes Require Explicit Approval
782
+
783
+ **Actions that modify email state (always confirm):**
784
+ - `delete` - Always requires confirmation
785
+ - `mark-read` - Confirm if batch (3+), mention undo
786
+ - `archive` - Confirm always, warn about no CLI undo
787
+ - `send` / `reply` - Requires `--confirm` flag
788
+
789
+ **Read-only actions (no confirmation needed):**
790
+ - `summary`, `analyze`, `search`, `read`, `accounts`
791
+
792
+ ---
793
+
662
794
  ## Feedback Loop
663
795
 
664
796
  If the user encounters a bug, friction point, or suggests a feature:
@@ -679,6 +811,10 @@ If the user encounters a bug, friction point, or suggests a feature:
679
811
  | Skipping pre-flight check | Tool may not be installed | Always run `inbox --version` first |
680
812
  | Forgetting `--account` flag | Ambiguity errors with multi-account | Always specify account |
681
813
  | Being passive after actions | User has to drive every step | Proactively suggest next step |
814
+ | Executing mark-read on batch without confirmation | User loses unread status on important emails | Confirm 3+ emails, always mention undo |
815
+ | Assuming user wants inbox zero | May delete emails user wanted to keep | Ask first, preserve by default |
816
+ | Executing multi-step plan without presenting it | User doesn't know what happened or why | Use plan-before-execute pattern |
817
+ | Auto-archiving "FYI" emails | User may want them visible in inbox | Archive only on explicit request |
682
818
 
683
819
  ---
684
820
 
package/CLAUDE.md CHANGED
@@ -80,11 +80,25 @@ All user data lives in `~/.config/inboxd/`:
80
80
 
81
81
  ## Release Process
82
82
 
83
- 1. Bump version in `package.json`
84
- 2. Commit changes
85
- 3. Create a GitHub Release (e.g., `gh release create v1.0.3`)
83
+ **After merging a feature/fix PR to main, always release:**
84
+
85
+ 1. `npm version patch` (or `minor`/`major` as appropriate)
86
+ 2. Commit and push: `git add package*.json && git commit -m "chore: bump version to X.X.X" && git push`
87
+ 3. Create release with quality notes:
88
+ ```bash
89
+ gh release create vX.X.X --title "vX.X.X" --notes "$(cat <<'EOF'
90
+ ## What's New
91
+ - Feature 1: description
92
+ - Feature 2: description
93
+
94
+ ## Fixes
95
+ - Fix 1: description
96
+ EOF
97
+ )"
98
+ ```
86
99
  4. The `publish.yml` workflow will automatically test and publish to npm
87
- - Note: `src/cli.js` dynamically imports version from `package.json` to ensure consistency
100
+
101
+ Note: `src/cli.js` dynamically imports version from `package.json` to ensure consistency.
88
102
 
89
103
  ## AI Agent Integration
90
104
 
@@ -153,6 +167,8 @@ scripts/postinstall.js # npm postinstall hint about install-skill
153
167
  | `inbox search -q <query>` | Search using Gmail query syntax |
154
168
  | `inbox send -t <to> -s <subj> -b <body> --confirm` | Send email (requires --confirm) |
155
169
  | `inbox reply --id <id> -b <body> --confirm` | Reply to email (requires --confirm) |
170
+ | `inbox mark-read --ids "id1,id2"` | Mark emails as read |
171
+ | `inbox mark-unread --ids "id1,id2"` | Mark emails as unread (undo mark-read) |
156
172
  | `inbox install-skill` | Install/update the Claude Code skill |
157
173
 
158
174
  ### Smart Filtering Options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inboxd",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "CLI assistant for Gmail monitoring with multi-account support and AI-ready JSON output",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { program } = require('commander');
4
- const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails, markAsRead, archiveEmails, groupEmailsBySender, getEmailContent, searchEmails, sendEmail, replyToEmail, extractLinks } = require('./gmail-monitor');
4
+ const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails, markAsRead, markAsUnread, archiveEmails, groupEmailsBySender, getEmailContent, searchEmails, sendEmail, replyToEmail, extractLinks } = require('./gmail-monitor');
5
5
  const { getState, updateLastCheck, markEmailsSeen, getNewEmailIds, clearOldSeenEmails } = require('./state');
6
6
  const { notifyNewEmails } = require('./notifier');
7
7
  const { authorize, addAccount, getAccounts, getAccountEmail, removeAccount, removeAllAccounts, renameTokenFile, validateCredentialsFile, hasCredentials, isConfigured, installCredentials } = require('./gmail-auth');
@@ -1325,6 +1325,64 @@ async function main() {
1325
1325
  }
1326
1326
  });
1327
1327
 
1328
+ program
1329
+ .command('mark-unread')
1330
+ .description('Mark emails as unread')
1331
+ .requiredOption('--ids <ids>', 'Comma-separated message IDs to mark as unread')
1332
+ .option('-a, --account <name>', 'Account name')
1333
+ .action(async (options) => {
1334
+ try {
1335
+ const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
1336
+
1337
+ if (ids.length === 0) {
1338
+ console.log(chalk.yellow('No message IDs provided.'));
1339
+ return;
1340
+ }
1341
+
1342
+ // Get account - if not specified, try to find from configured accounts
1343
+ let account = options.account;
1344
+ if (!account) {
1345
+ const accounts = getAccounts();
1346
+ if (accounts.length === 1) {
1347
+ account = accounts[0].name;
1348
+ } else if (accounts.length > 1) {
1349
+ console.log(chalk.yellow('Multiple accounts configured. Please specify --account <name>'));
1350
+ console.log(chalk.gray('Available accounts:'));
1351
+ accounts.forEach(a => console.log(chalk.gray(` - ${a.name}`)));
1352
+ return;
1353
+ } else {
1354
+ account = 'default';
1355
+ }
1356
+ }
1357
+
1358
+ console.log(chalk.cyan(`Marking ${ids.length} email(s) as unread...`));
1359
+
1360
+ const results = await markAsUnread(account, ids);
1361
+
1362
+ const succeeded = results.filter(r => r.success).length;
1363
+ const failed = results.filter(r => !r.success).length;
1364
+
1365
+ if (succeeded > 0) {
1366
+ console.log(chalk.green(`\nMarked ${succeeded} email(s) as unread.`));
1367
+ }
1368
+ if (failed > 0) {
1369
+ console.log(chalk.red(`Failed to mark ${failed} email(s) as unread.`));
1370
+ results.filter(r => !r.success).forEach(r => {
1371
+ console.log(chalk.red(` - ${r.id}: ${r.error}`));
1372
+ });
1373
+ }
1374
+
1375
+ } catch (error) {
1376
+ if (error.message.includes('403') || error.code === 403) {
1377
+ console.error(chalk.red('Permission denied. You may need to re-authenticate with updated scopes.'));
1378
+ console.error(chalk.yellow('Run: inbox auth -a <account>'));
1379
+ } else {
1380
+ console.error(chalk.red('Error marking emails as unread:'), error.message);
1381
+ }
1382
+ process.exit(1);
1383
+ }
1384
+ });
1385
+
1328
1386
  program
1329
1387
  .command('archive')
1330
1388
  .description('Archive emails (remove from inbox, keep in All Mail)')
@@ -209,6 +209,34 @@ async function markAsRead(account, messageIds) {
209
209
  return results;
210
210
  }
211
211
 
212
+ /**
213
+ * Marks emails as unread by adding the UNREAD label
214
+ * @param {string} account - Account name
215
+ * @param {Array<string>} messageIds - Array of message IDs to mark as unread
216
+ * @returns {Array<{id: string, success: boolean, error?: string}>} Results for each message
217
+ */
218
+ async function markAsUnread(account, messageIds) {
219
+ const gmail = await getGmailClient(account);
220
+ const results = [];
221
+
222
+ for (const id of messageIds) {
223
+ try {
224
+ await withRetry(() => gmail.users.messages.modify({
225
+ userId: 'me',
226
+ id: id,
227
+ requestBody: {
228
+ addLabelIds: ['UNREAD'],
229
+ },
230
+ }));
231
+ results.push({ id, success: true });
232
+ } catch (err) {
233
+ results.push({ id, success: false, error: err.message });
234
+ }
235
+ }
236
+
237
+ return results;
238
+ }
239
+
212
240
  /**
213
241
  * Archives emails by removing the INBOX label
214
242
  * @param {string} account - Account name
@@ -646,6 +674,7 @@ module.exports = {
646
674
  getEmailById,
647
675
  untrashEmails,
648
676
  markAsRead,
677
+ markAsUnread,
649
678
  archiveEmails,
650
679
  extractSenderDomain,
651
680
  groupEmailsBySender,
@@ -132,4 +132,297 @@ describe('Gmail Monitor Logic', () => {
132
132
  }
133
133
  });
134
134
  });
135
+
136
+ describe('Mark As Read Logic', () => {
137
+ it('should mark email as read by removing UNREAD label', async () => {
138
+ const mockGmail = {
139
+ users: {
140
+ messages: {
141
+ modify: vi.fn().mockResolvedValue({ data: { id: 'msg123', labelIds: ['INBOX'] } })
142
+ }
143
+ }
144
+ };
145
+
146
+ const messageIds = ['msg123'];
147
+ const results = [];
148
+
149
+ for (const id of messageIds) {
150
+ try {
151
+ await mockGmail.users.messages.modify({
152
+ userId: 'me',
153
+ id: id,
154
+ requestBody: { removeLabelIds: ['UNREAD'] }
155
+ });
156
+ results.push({ id, success: true });
157
+ } catch (err) {
158
+ results.push({ id, success: false, error: err.message });
159
+ }
160
+ }
161
+
162
+ expect(results).toHaveLength(1);
163
+ expect(results[0].success).toBe(true);
164
+ expect(mockGmail.users.messages.modify).toHaveBeenCalledWith({
165
+ userId: 'me',
166
+ id: 'msg123',
167
+ requestBody: { removeLabelIds: ['UNREAD'] }
168
+ });
169
+ });
170
+
171
+ it('should handle errors during mark as read', async () => {
172
+ const mockGmail = {
173
+ users: {
174
+ messages: {
175
+ modify: vi.fn().mockRejectedValue(new Error('API Error'))
176
+ }
177
+ }
178
+ };
179
+
180
+ const messageIds = ['msg123'];
181
+ const results = [];
182
+
183
+ for (const id of messageIds) {
184
+ try {
185
+ await mockGmail.users.messages.modify({
186
+ userId: 'me',
187
+ id: id,
188
+ requestBody: { removeLabelIds: ['UNREAD'] }
189
+ });
190
+ results.push({ id, success: true });
191
+ } catch (err) {
192
+ results.push({ id, success: false, error: err.message });
193
+ }
194
+ }
195
+
196
+ expect(results).toHaveLength(1);
197
+ expect(results[0].success).toBe(false);
198
+ expect(results[0].error).toBe('API Error');
199
+ });
200
+
201
+ it('should handle multiple message IDs for mark as read', async () => {
202
+ const mockModify = vi.fn()
203
+ .mockResolvedValueOnce({ data: { id: 'msg1' } })
204
+ .mockResolvedValueOnce({ data: { id: 'msg2' } })
205
+ .mockRejectedValueOnce(new Error('Not found'));
206
+
207
+ const mockGmail = {
208
+ users: { messages: { modify: mockModify } }
209
+ };
210
+
211
+ const messageIds = ['msg1', 'msg2', 'msg3'];
212
+ const results = [];
213
+
214
+ for (const id of messageIds) {
215
+ try {
216
+ await mockGmail.users.messages.modify({
217
+ userId: 'me',
218
+ id: id,
219
+ requestBody: { removeLabelIds: ['UNREAD'] }
220
+ });
221
+ results.push({ id, success: true });
222
+ } catch (err) {
223
+ results.push({ id, success: false, error: err.message });
224
+ }
225
+ }
226
+
227
+ expect(results).toHaveLength(3);
228
+ expect(results[0]).toEqual({ id: 'msg1', success: true });
229
+ expect(results[1]).toEqual({ id: 'msg2', success: true });
230
+ expect(results[2]).toEqual({ id: 'msg3', success: false, error: 'Not found' });
231
+ });
232
+ });
233
+
234
+ describe('Mark As Unread Logic', () => {
235
+ it('should mark email as unread by adding UNREAD label', async () => {
236
+ const mockGmail = {
237
+ users: {
238
+ messages: {
239
+ modify: vi.fn().mockResolvedValue({ data: { id: 'msg123', labelIds: ['INBOX', 'UNREAD'] } })
240
+ }
241
+ }
242
+ };
243
+
244
+ const messageIds = ['msg123'];
245
+ const results = [];
246
+
247
+ for (const id of messageIds) {
248
+ try {
249
+ await mockGmail.users.messages.modify({
250
+ userId: 'me',
251
+ id: id,
252
+ requestBody: { addLabelIds: ['UNREAD'] }
253
+ });
254
+ results.push({ id, success: true });
255
+ } catch (err) {
256
+ results.push({ id, success: false, error: err.message });
257
+ }
258
+ }
259
+
260
+ expect(results).toHaveLength(1);
261
+ expect(results[0].success).toBe(true);
262
+ expect(mockGmail.users.messages.modify).toHaveBeenCalledWith({
263
+ userId: 'me',
264
+ id: 'msg123',
265
+ requestBody: { addLabelIds: ['UNREAD'] }
266
+ });
267
+ });
268
+
269
+ it('should handle errors during mark as unread', async () => {
270
+ const mockGmail = {
271
+ users: {
272
+ messages: {
273
+ modify: vi.fn().mockRejectedValue(new Error('Permission denied'))
274
+ }
275
+ }
276
+ };
277
+
278
+ const messageIds = ['msg123'];
279
+ const results = [];
280
+
281
+ for (const id of messageIds) {
282
+ try {
283
+ await mockGmail.users.messages.modify({
284
+ userId: 'me',
285
+ id: id,
286
+ requestBody: { addLabelIds: ['UNREAD'] }
287
+ });
288
+ results.push({ id, success: true });
289
+ } catch (err) {
290
+ results.push({ id, success: false, error: err.message });
291
+ }
292
+ }
293
+
294
+ expect(results).toHaveLength(1);
295
+ expect(results[0].success).toBe(false);
296
+ expect(results[0].error).toBe('Permission denied');
297
+ });
298
+
299
+ it('should handle multiple message IDs for mark as unread', async () => {
300
+ const mockModify = vi.fn()
301
+ .mockResolvedValueOnce({ data: { id: 'msg1' } })
302
+ .mockRejectedValueOnce(new Error('Not found'))
303
+ .mockResolvedValueOnce({ data: { id: 'msg3' } });
304
+
305
+ const mockGmail = {
306
+ users: { messages: { modify: mockModify } }
307
+ };
308
+
309
+ const messageIds = ['msg1', 'msg2', 'msg3'];
310
+ const results = [];
311
+
312
+ for (const id of messageIds) {
313
+ try {
314
+ await mockGmail.users.messages.modify({
315
+ userId: 'me',
316
+ id: id,
317
+ requestBody: { addLabelIds: ['UNREAD'] }
318
+ });
319
+ results.push({ id, success: true });
320
+ } catch (err) {
321
+ results.push({ id, success: false, error: err.message });
322
+ }
323
+ }
324
+
325
+ expect(results).toHaveLength(3);
326
+ expect(results[0]).toEqual({ id: 'msg1', success: true });
327
+ expect(results[1]).toEqual({ id: 'msg2', success: false, error: 'Not found' });
328
+ expect(results[2]).toEqual({ id: 'msg3', success: true });
329
+ });
330
+ });
331
+
332
+ describe('Archive Emails Logic', () => {
333
+ it('should archive email by removing INBOX label', async () => {
334
+ const mockGmail = {
335
+ users: {
336
+ messages: {
337
+ modify: vi.fn().mockResolvedValue({ data: { id: 'msg123', labelIds: ['CATEGORY_UPDATES'] } })
338
+ }
339
+ }
340
+ };
341
+
342
+ const messageIds = ['msg123'];
343
+ const results = [];
344
+
345
+ for (const id of messageIds) {
346
+ try {
347
+ await mockGmail.users.messages.modify({
348
+ userId: 'me',
349
+ id: id,
350
+ requestBody: { removeLabelIds: ['INBOX'] }
351
+ });
352
+ results.push({ id, success: true });
353
+ } catch (err) {
354
+ results.push({ id, success: false, error: err.message });
355
+ }
356
+ }
357
+
358
+ expect(results).toHaveLength(1);
359
+ expect(results[0].success).toBe(true);
360
+ expect(mockGmail.users.messages.modify).toHaveBeenCalledWith({
361
+ userId: 'me',
362
+ id: 'msg123',
363
+ requestBody: { removeLabelIds: ['INBOX'] }
364
+ });
365
+ });
366
+
367
+ it('should handle errors during archive', async () => {
368
+ const mockGmail = {
369
+ users: {
370
+ messages: {
371
+ modify: vi.fn().mockRejectedValue(new Error('Rate limit exceeded'))
372
+ }
373
+ }
374
+ };
375
+
376
+ const messageIds = ['msg123'];
377
+ const results = [];
378
+
379
+ for (const id of messageIds) {
380
+ try {
381
+ await mockGmail.users.messages.modify({
382
+ userId: 'me',
383
+ id: id,
384
+ requestBody: { removeLabelIds: ['INBOX'] }
385
+ });
386
+ results.push({ id, success: true });
387
+ } catch (err) {
388
+ results.push({ id, success: false, error: err.message });
389
+ }
390
+ }
391
+
392
+ expect(results).toHaveLength(1);
393
+ expect(results[0].success).toBe(false);
394
+ expect(results[0].error).toBe('Rate limit exceeded');
395
+ });
396
+
397
+ it('should handle multiple message IDs for archive', async () => {
398
+ const mockModify = vi.fn()
399
+ .mockResolvedValueOnce({ data: { id: 'msg1' } })
400
+ .mockResolvedValueOnce({ data: { id: 'msg2' } });
401
+
402
+ const mockGmail = {
403
+ users: { messages: { modify: mockModify } }
404
+ };
405
+
406
+ const messageIds = ['msg1', 'msg2'];
407
+ const results = [];
408
+
409
+ for (const id of messageIds) {
410
+ try {
411
+ await mockGmail.users.messages.modify({
412
+ userId: 'me',
413
+ id: id,
414
+ requestBody: { removeLabelIds: ['INBOX'] }
415
+ });
416
+ results.push({ id, success: true });
417
+ } catch (err) {
418
+ results.push({ id, success: false, error: err.message });
419
+ }
420
+ }
421
+
422
+ expect(results).toHaveLength(2);
423
+ expect(results[0]).toEqual({ id: 'msg1', success: true });
424
+ expect(results[1]).toEqual({ id: 'msg2', success: true });
425
+ expect(mockModify).toHaveBeenCalledTimes(2);
426
+ });
427
+ });
135
428
  });