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.
- package/.claude/skills/inbox-assistant/SKILL.md +140 -4
- package/CLAUDE.md +20 -4
- package/package.json +1 -1
- package/src/cli.js +59 -1
- package/src/gmail-monitor.js +29 -0
- package/tests/gmail-monitor.test.js +293 -0
|
@@ -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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
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)')
|
package/src/gmail-monitor.js
CHANGED
|
@@ -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
|
});
|