inboxd 1.0.11 → 1.0.12
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 +99 -0
- package/CLAUDE.md +15 -0
- package/package.json +1 -1
- package/src/cli.js +364 -3
- package/src/gmail-monitor.js +364 -0
- package/src/sent-log.js +87 -0
- package/tests/gmail-monitor-patterns.test.js +232 -0
- package/tests/link-extraction.test.js +249 -0
- package/tests/older-than.test.js +127 -0
- package/tests/sent-log.test.js +142 -0
|
@@ -67,6 +67,94 @@ Use when: Heavy inbox (>30 unread), user wants thoroughness, language like "what
|
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
70
|
+
## Heavy Inbox Strategy
|
|
71
|
+
|
|
72
|
+
When a user has a heavy inbox (>20 unread emails), use this optimized workflow:
|
|
73
|
+
|
|
74
|
+
### 1. Quick Assessment
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
inbox summary --json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Identify which account(s) have the bulk of unread emails.
|
|
81
|
+
|
|
82
|
+
### 2. Group Analysis First
|
|
83
|
+
|
|
84
|
+
For heavy inboxes, **always start with grouped analysis**:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
inbox analyze --count 100 --account <name> --group-by sender
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This reveals:
|
|
91
|
+
- Which senders are flooding the inbox
|
|
92
|
+
- Batch cleanup opportunities (all from same sender)
|
|
93
|
+
- High-volume vs. low-volume senders
|
|
94
|
+
|
|
95
|
+
### 3. Batch Cleanup by Sender
|
|
96
|
+
|
|
97
|
+
When grouped analysis shows high-volume senders (5+ emails):
|
|
98
|
+
|
|
99
|
+
| Count | Sender Pattern | Likely Action |
|
|
100
|
+
|-------|----------------|---------------|
|
|
101
|
+
| 10+ | linkedin.com | Job alerts - offer batch delete |
|
|
102
|
+
| 5+ | newsletter@ | Newsletters - offer unsubscribe + delete |
|
|
103
|
+
| 5+ | noreply@ | Notifications - review, likely safe to batch |
|
|
104
|
+
| 3+ | same domain | Check if promotional or transactional |
|
|
105
|
+
|
|
106
|
+
**Example workflow:**
|
|
107
|
+
```
|
|
108
|
+
## Inbox Analysis: work@company.com (47 unread)
|
|
109
|
+
|
|
110
|
+
### High-Volume Senders:
|
|
111
|
+
| Sender | Count | Likely Type |
|
|
112
|
+
|--------|-------|-------------|
|
|
113
|
+
| linkedin.com | 12 | Job alerts |
|
|
114
|
+
| github.com | 8 | Notifications |
|
|
115
|
+
| substack.com | 6 | Newsletters |
|
|
116
|
+
|
|
117
|
+
### Recommendation:
|
|
118
|
+
These 26 emails (55% of inbox) are recurring notifications.
|
|
119
|
+
Delete all LinkedIn job alerts and old newsletters?
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 4. Find Stale Emails
|
|
123
|
+
|
|
124
|
+
For cleanup of old emails, use server-side filtering:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
inbox analyze --older-than 30d --group-by sender
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Old emails (>30 days) are usually safe to batch delete:
|
|
131
|
+
- Expired promotions
|
|
132
|
+
- Delivered order notifications
|
|
133
|
+
- Old newsletters
|
|
134
|
+
|
|
135
|
+
### 5. Then Individual Review
|
|
136
|
+
|
|
137
|
+
After batch cleanup, remaining emails are typically:
|
|
138
|
+
- Direct messages from humans
|
|
139
|
+
- Action items (PRs, meeting requests)
|
|
140
|
+
- Transactional (receipts, confirmations)
|
|
141
|
+
|
|
142
|
+
These deserve individual attention.
|
|
143
|
+
|
|
144
|
+
### Decision Tree
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Unread count?
|
|
148
|
+
├── ≤5: Quick summary, list all
|
|
149
|
+
├── 6-20: Analyze, offer batch actions for obvious noise
|
|
150
|
+
└── >20:
|
|
151
|
+
├── Group by sender FIRST
|
|
152
|
+
├── Batch delete obvious noise (LinkedIn, newsletters, promos)
|
|
153
|
+
└── Then individual review of remaining
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
70
158
|
## Quick Start
|
|
71
159
|
|
|
72
160
|
| Task | Command |
|
|
@@ -74,6 +162,8 @@ Use when: Heavy inbox (>30 unread), user wants thoroughness, language like "what
|
|
|
74
162
|
| Check status | `inbox summary --json` |
|
|
75
163
|
| Full triage | `inbox analyze --count 50` → classify → present |
|
|
76
164
|
| Analyze by sender | `inbox analyze --count 50 --group-by sender` |
|
|
165
|
+
| Find old emails | `inbox analyze --older-than 30d` |
|
|
166
|
+
| Extract links from email | `inbox read --id <id> --links` |
|
|
77
167
|
| Delete by ID | `inbox delete --ids "id1,id2" --confirm` |
|
|
78
168
|
| Delete by sender | `inbox delete --sender "linkedin" --dry-run` → confirm → delete |
|
|
79
169
|
| Delete by subject | `inbox delete --match "weekly digest" --dry-run` |
|
|
@@ -179,7 +269,12 @@ To stop: `launchctl unload ~/Library/LaunchAgents/com.yourname.inboxd.plist`
|
|
|
179
269
|
| `inbox analyze --count 50` | Get email data for analysis | JSON array of email objects |
|
|
180
270
|
| `inbox analyze --count 50 --all` | Include read emails | JSON array (read + unread) |
|
|
181
271
|
| `inbox analyze --since 7d` | Only emails from last 7 days | JSON array (filtered by date) |
|
|
272
|
+
| `inbox analyze --older-than 30d` | Only emails older than 30 days | JSON array (server-side filtered) |
|
|
182
273
|
| `inbox analyze --group-by sender` | Group emails by sender domain | `{groups: [{sender, count, emails}], totalCount}` |
|
|
274
|
+
| `inbox read --id <id>` | Read full email content | Email headers + body |
|
|
275
|
+
| `inbox read --id <id> --links` | Extract links from email | List of URLs with optional link text |
|
|
276
|
+
| `inbox read --id <id> --links --json` | Extract links as JSON | `{id, subject, from, linkCount, links}` |
|
|
277
|
+
| `inbox search -q "query"` | Search using Gmail query syntax | JSON array of matching emails |
|
|
183
278
|
| `inbox accounts` | List configured accounts | Account names and emails |
|
|
184
279
|
|
|
185
280
|
### Actions
|
|
@@ -276,8 +371,10 @@ Based on the summary stats, immediately suggest ONE clear next action:
|
|
|
276
371
|
| One account has >50% of unread | "[account] has X of your Y unread—let me triage that first." |
|
|
277
372
|
| Total unread ≤ 5 | "Only X unread—here's a quick summary:" (show inline) |
|
|
278
373
|
| All accounts have 1-2 unread | "Light inbox day. Quick summary of all emails:" |
|
|
374
|
+
| Total unread > 20 | "Heavy inbox. Let me group by sender to find batch cleanup opportunities." → `--group-by sender` |
|
|
279
375
|
| Total unread > 30 | "Heavy inbox. I'll process by account, starting with [highest]." |
|
|
280
376
|
| Single account with 0 unread | "Inbox zero on [account]! Want me to check the others?" |
|
|
377
|
+
| Grouped analysis shows sender with 5+ emails | "[sender] has X emails. Delete them all?" |
|
|
281
378
|
|
|
282
379
|
**Example good response:**
|
|
283
380
|
```
|
|
@@ -453,6 +550,8 @@ When user has job-related emails (LinkedIn, Indeed, recruiters) and wants to eva
|
|
|
453
550
|
| "Delete [sender]'s emails" | Bulk sender cleanup | Two-step pattern with `--sender` filter |
|
|
454
551
|
| "Delete the security emails" | Subject-based cleanup | `--match "security" --dry-run` → confirm → `--ids` |
|
|
455
552
|
| "What senders have the most emails?" | Inbox analysis | `inbox analyze --group-by sender` |
|
|
553
|
+
| "What links are in this email?" | Extract URLs | `inbox read --id <id> --links` |
|
|
554
|
+
| "Find my old emails" / "Clean up old stuff" | Stale email review | `inbox analyze --older-than 30d` |
|
|
456
555
|
| "I keep getting these" | Recurring annoyance | Suggest unsubscribe/filter, then delete batch |
|
|
457
556
|
| "Check [specific account]" | Single-account focus | Skip other accounts entirely |
|
|
458
557
|
| "Undo" / "Restore" | Recover deleted emails | `inbox restore --last N` |
|
package/CLAUDE.md
CHANGED
|
@@ -23,6 +23,7 @@ src/
|
|
|
23
23
|
├── gmail-monitor.js # Gmail API: fetch, count, trash, restore
|
|
24
24
|
├── state.js # Tracks seen emails per account
|
|
25
25
|
├── deletion-log.js # Logs deleted emails for restore capability
|
|
26
|
+
├── sent-log.js # Logs sent emails for audit trail
|
|
26
27
|
├── notifier.js # macOS notifications (node-notifier)
|
|
27
28
|
└── skill-installer.js # Copies skill to ~/.claude/skills/
|
|
28
29
|
|
|
@@ -53,6 +54,7 @@ All user data lives in `~/.config/inboxd/`:
|
|
|
53
54
|
| `token-<name>.json` | OAuth refresh/access tokens |
|
|
54
55
|
| `state-<name>.json` | `{ seenEmailIds, lastCheck, lastNotifiedAt }` |
|
|
55
56
|
| `deletion-log.json` | Audit log for deleted emails |
|
|
57
|
+
| `sent-log.json` | Audit log for sent emails |
|
|
56
58
|
|
|
57
59
|
## Code Patterns
|
|
58
60
|
|
|
@@ -141,10 +143,16 @@ scripts/postinstall.js # npm postinstall hint about install-skill
|
|
|
141
143
|
| `inbox summary --json` | Quick status check (unread counts) |
|
|
142
144
|
| `inbox analyze --count 50` | Get email data as JSON for classification |
|
|
143
145
|
| `inbox analyze --group-by sender` | Group emails by sender domain |
|
|
146
|
+
| `inbox analyze --older-than 30d` | Find emails older than 30 days (server-side filtering) |
|
|
144
147
|
| `inbox delete --ids "id1,id2" --confirm` | Delete specific emails by ID |
|
|
145
148
|
| `inbox delete --sender "pattern" --dry-run` | Preview deletion by sender filter |
|
|
146
149
|
| `inbox delete --match "pattern" --dry-run` | Preview deletion by subject filter |
|
|
147
150
|
| `inbox restore --last N` | Undo last N deletions |
|
|
151
|
+
| `inbox read --id <id>` | Read full email content |
|
|
152
|
+
| `inbox read --id <id> --links` | Extract links from email |
|
|
153
|
+
| `inbox search -q <query>` | Search using Gmail query syntax |
|
|
154
|
+
| `inbox send -t <to> -s <subj> -b <body> --confirm` | Send email (requires --confirm) |
|
|
155
|
+
| `inbox reply --id <id> -b <body> --confirm` | Reply to email (requires --confirm) |
|
|
148
156
|
| `inbox install-skill` | Install/update the Claude Code skill |
|
|
149
157
|
|
|
150
158
|
### Smart Filtering Options
|
|
@@ -156,6 +164,13 @@ scripts/postinstall.js # npm postinstall hint about install-skill
|
|
|
156
164
|
| `--force` | Override safety warnings (short patterns, large batches) |
|
|
157
165
|
| `--dry-run` | Preview what would be deleted without deleting |
|
|
158
166
|
|
|
167
|
+
### Send/Reply Safety
|
|
168
|
+
The `send` and `reply` commands have built-in safety features:
|
|
169
|
+
- **`--dry-run`**: Preview the email without sending
|
|
170
|
+
- **`--confirm`**: Required flag to actually send (prevents accidental sends)
|
|
171
|
+
- **Audit logging**: All sent emails are logged to `~/.config/inboxd/sent-log.json`
|
|
172
|
+
- **Account resolution**: Prompts for account selection when multiple accounts exist
|
|
173
|
+
|
|
159
174
|
### Email Object Shape (from `analyze`)
|
|
160
175
|
```json
|
|
161
176
|
{
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
|
-
const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails, markAsRead, archiveEmails, groupEmailsBySender } = require('./gmail-monitor');
|
|
4
|
+
const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails, markAsRead, 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');
|
|
8
8
|
const { logDeletions, getRecentDeletions, getLogPath, readLog, removeLogEntries } = require('./deletion-log');
|
|
9
9
|
const { getSkillStatus, checkForUpdate, installSkill, SKILL_DEST_DIR, SOURCE_MARKER } = require('./skill-installer');
|
|
10
|
+
const { logSentEmail, getSentLogPath } = require('./sent-log');
|
|
10
11
|
const readline = require('readline');
|
|
11
12
|
const path = require('path');
|
|
12
13
|
const os = require('os');
|
|
@@ -62,6 +63,61 @@ function parseSinceDuration(duration) {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Parses a duration string for Gmail's older_than query
|
|
68
|
+
* Gmail only supports days (d) for older_than, so we convert weeks/months to days
|
|
69
|
+
* @param {string} duration - Duration string (e.g., "30d", "2w", "1m")
|
|
70
|
+
* @returns {string|null} Gmail query component (e.g., "30d") or null if invalid
|
|
71
|
+
*/
|
|
72
|
+
function parseOlderThanDuration(duration) {
|
|
73
|
+
const match = duration.match(/^(\d+)([dwm])$/i);
|
|
74
|
+
if (!match) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const value = parseInt(match[1], 10);
|
|
79
|
+
const unit = match[2].toLowerCase();
|
|
80
|
+
|
|
81
|
+
switch (unit) {
|
|
82
|
+
case 'd': // days
|
|
83
|
+
return `${value}d`;
|
|
84
|
+
case 'w': // weeks -> days
|
|
85
|
+
return `${value * 7}d`;
|
|
86
|
+
case 'm': // months (approximate as 30 days)
|
|
87
|
+
return `${value * 30}d`;
|
|
88
|
+
default:
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolves the account to use, prompting user if ambiguous
|
|
95
|
+
* @param {string|undefined} specifiedAccount - Account specified via option
|
|
96
|
+
* @param {Object} chalk - Chalk instance for coloring
|
|
97
|
+
* @returns {{account: string|null, error: string|null}} Account name or error
|
|
98
|
+
*/
|
|
99
|
+
function resolveAccount(specifiedAccount, chalk) {
|
|
100
|
+
if (specifiedAccount) {
|
|
101
|
+
return { account: specifiedAccount, error: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const accounts = getAccounts();
|
|
105
|
+
if (accounts.length === 0) {
|
|
106
|
+
return { account: 'default', error: null };
|
|
107
|
+
}
|
|
108
|
+
if (accounts.length === 1) {
|
|
109
|
+
return { account: accounts[0].name, error: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Multiple accounts, must specify
|
|
113
|
+
let errorMsg = chalk.yellow('Multiple accounts configured. Please specify --account <name>\n');
|
|
114
|
+
errorMsg += chalk.gray('Available accounts:\n');
|
|
115
|
+
accounts.forEach(a => {
|
|
116
|
+
errorMsg += chalk.gray(` - ${a.name} (${a.email || 'unknown'})\n`);
|
|
117
|
+
});
|
|
118
|
+
return { account: null, error: errorMsg };
|
|
119
|
+
}
|
|
120
|
+
|
|
65
121
|
async function main() {
|
|
66
122
|
const chalk = (await import('chalk')).default;
|
|
67
123
|
const boxen = (await import('boxen')).default;
|
|
@@ -512,6 +568,7 @@ async function main() {
|
|
|
512
568
|
.option('-n, --count <number>', 'Number of emails to analyze per account', '20')
|
|
513
569
|
.option('--all', 'Include read and unread emails (default: unread only)')
|
|
514
570
|
.option('--since <duration>', 'Only include emails from last N days/hours (e.g., "7d", "24h", "3d")')
|
|
571
|
+
.option('--older-than <duration>', 'Only include emails older than N days/weeks (e.g., "30d", "2w", "1m")')
|
|
515
572
|
.option('--group-by <field>', 'Group emails by field (sender)')
|
|
516
573
|
.action(async (options) => {
|
|
517
574
|
try {
|
|
@@ -527,12 +584,34 @@ async function main() {
|
|
|
527
584
|
const includeRead = !!options.all;
|
|
528
585
|
let allEmails = [];
|
|
529
586
|
|
|
587
|
+
// Build Gmail query for --older-than (server-side filtering)
|
|
588
|
+
let olderThanQuery = null;
|
|
589
|
+
if (options.olderThan) {
|
|
590
|
+
const olderThanDays = parseOlderThanDuration(options.olderThan);
|
|
591
|
+
if (!olderThanDays) {
|
|
592
|
+
console.error(JSON.stringify({
|
|
593
|
+
error: `Invalid --older-than format: "${options.olderThan}". Use format like "30d", "2w", "1m"`
|
|
594
|
+
}));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
olderThanQuery = `older_than:${olderThanDays}`;
|
|
598
|
+
}
|
|
599
|
+
|
|
530
600
|
for (const account of accounts) {
|
|
531
|
-
|
|
601
|
+
let emails;
|
|
602
|
+
if (olderThanQuery) {
|
|
603
|
+
// Use searchEmails for server-side filtering when --older-than is specified
|
|
604
|
+
const query = includeRead
|
|
605
|
+
? olderThanQuery
|
|
606
|
+
: `is:unread ${olderThanQuery}`;
|
|
607
|
+
emails = await searchEmails(account, query, maxPerAccount);
|
|
608
|
+
} else {
|
|
609
|
+
emails = await getUnreadEmails(account, maxPerAccount, includeRead);
|
|
610
|
+
}
|
|
532
611
|
allEmails.push(...emails);
|
|
533
612
|
}
|
|
534
613
|
|
|
535
|
-
// Filter by --since if provided
|
|
614
|
+
// Filter by --since if provided (client-side, for newer emails)
|
|
536
615
|
if (options.since) {
|
|
537
616
|
const sinceDate = parseSinceDuration(options.since);
|
|
538
617
|
if (sinceDate) {
|
|
@@ -561,6 +640,288 @@ async function main() {
|
|
|
561
640
|
}
|
|
562
641
|
});
|
|
563
642
|
|
|
643
|
+
program
|
|
644
|
+
.command('read')
|
|
645
|
+
.description('Read full content of an email')
|
|
646
|
+
.requiredOption('--id <id>', 'Message ID to read')
|
|
647
|
+
.option('-a, --account <name>', 'Account name')
|
|
648
|
+
.option('--json', 'Output as JSON')
|
|
649
|
+
.option('--links', 'Extract and display links from email')
|
|
650
|
+
.action(async (options) => {
|
|
651
|
+
try {
|
|
652
|
+
const id = options.id.trim();
|
|
653
|
+
if (!id) {
|
|
654
|
+
console.log(chalk.yellow('No message ID provided.'));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const { account, error } = resolveAccount(options.account, chalk);
|
|
659
|
+
if (error) {
|
|
660
|
+
console.log(error);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// When --links is used, prefer HTML for better link extraction
|
|
665
|
+
const emailOptions = options.links ? { preferHtml: true } : {};
|
|
666
|
+
const email = await getEmailContent(account, id, emailOptions);
|
|
667
|
+
|
|
668
|
+
if (!email) {
|
|
669
|
+
console.log(chalk.red(`Email ${id} not found in account "${account}".`));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// If --links flag is used, extract and display links
|
|
674
|
+
if (options.links) {
|
|
675
|
+
const links = extractLinks(email.body, email.mimeType);
|
|
676
|
+
|
|
677
|
+
if (options.json) {
|
|
678
|
+
console.log(JSON.stringify({
|
|
679
|
+
id: email.id,
|
|
680
|
+
subject: email.subject,
|
|
681
|
+
from: email.from,
|
|
682
|
+
linkCount: links.length,
|
|
683
|
+
links
|
|
684
|
+
}, null, 2));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
console.log(chalk.cyan('From: ') + chalk.white(email.from));
|
|
689
|
+
console.log(chalk.cyan('Subject: ') + chalk.white(email.subject));
|
|
690
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
691
|
+
|
|
692
|
+
if (links.length === 0) {
|
|
693
|
+
console.log(chalk.gray('No links found in this email.'));
|
|
694
|
+
} else {
|
|
695
|
+
console.log(chalk.bold(`\nLinks (${links.length}):\n`));
|
|
696
|
+
links.forEach((link, i) => {
|
|
697
|
+
if (link.text) {
|
|
698
|
+
console.log(chalk.white(`${i + 1}. ${link.text}`));
|
|
699
|
+
console.log(chalk.cyan(` ${link.url}`));
|
|
700
|
+
} else {
|
|
701
|
+
console.log(chalk.cyan(`${i + 1}. ${link.url}`));
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (options.json) {
|
|
709
|
+
console.log(JSON.stringify(email, null, 2));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
console.log(chalk.cyan('From: ') + chalk.white(email.from));
|
|
714
|
+
if (email.to) {
|
|
715
|
+
console.log(chalk.cyan('To: ') + chalk.white(email.to));
|
|
716
|
+
}
|
|
717
|
+
console.log(chalk.cyan('Date: ') + chalk.white(email.date));
|
|
718
|
+
console.log(chalk.cyan('Subject: ') + chalk.white(email.subject));
|
|
719
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
720
|
+
console.log(email.body || chalk.gray('(No content)'));
|
|
721
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
722
|
+
|
|
723
|
+
} catch (error) {
|
|
724
|
+
console.error(chalk.red('Error reading email:'), error.message);
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
program
|
|
730
|
+
.command('search')
|
|
731
|
+
.description('Search emails using Gmail query syntax')
|
|
732
|
+
.requiredOption('-q, --query <query>', 'Search query (e.g. "from:boss is:unread")')
|
|
733
|
+
.option('-a, --account <name>', 'Account to search')
|
|
734
|
+
.option('-n, --limit <number>', 'Max results', '20')
|
|
735
|
+
.option('--json', 'Output as JSON')
|
|
736
|
+
.action(async (options) => {
|
|
737
|
+
try {
|
|
738
|
+
const { account, error } = resolveAccount(options.account, chalk);
|
|
739
|
+
if (error) {
|
|
740
|
+
console.log(error);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const limit = parseInt(options.limit, 10);
|
|
745
|
+
const emails = await searchEmails(account, options.query, limit);
|
|
746
|
+
|
|
747
|
+
if (options.json) {
|
|
748
|
+
console.log(JSON.stringify(emails, null, 2));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (emails.length === 0) {
|
|
753
|
+
console.log(chalk.gray('No emails found matching query.'));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
console.log(chalk.bold(`Found ${emails.length} emails matching "${options.query}":\n`));
|
|
758
|
+
|
|
759
|
+
emails.forEach(e => {
|
|
760
|
+
const from = e.from.length > 35 ? e.from.substring(0, 32) + '...' : e.from;
|
|
761
|
+
const subject = e.subject.length > 50 ? e.subject.substring(0, 47) + '...' : e.subject;
|
|
762
|
+
console.log(chalk.cyan(e.id) + ' ' + chalk.white(from));
|
|
763
|
+
console.log(chalk.gray(` ${subject}\n`));
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
} catch (error) {
|
|
767
|
+
console.error(chalk.red('Error searching emails:'), error.message);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
program
|
|
773
|
+
.command('send')
|
|
774
|
+
.description('Send an email')
|
|
775
|
+
.requiredOption('-t, --to <email>', 'Recipient email')
|
|
776
|
+
.requiredOption('-s, --subject <subject>', 'Email subject')
|
|
777
|
+
.requiredOption('-b, --body <body>', 'Email body text')
|
|
778
|
+
.option('-a, --account <name>', 'Account to send from')
|
|
779
|
+
.option('--dry-run', 'Preview the email without sending')
|
|
780
|
+
.option('--confirm', 'Skip confirmation prompt')
|
|
781
|
+
.action(async (options) => {
|
|
782
|
+
try {
|
|
783
|
+
const { account, error } = resolveAccount(options.account, chalk);
|
|
784
|
+
if (error) {
|
|
785
|
+
console.log(error);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Get account email for display
|
|
790
|
+
const accountInfo = getAccounts().find(a => a.name === account);
|
|
791
|
+
const fromEmail = accountInfo?.email || account;
|
|
792
|
+
|
|
793
|
+
// Always show preview
|
|
794
|
+
console.log(chalk.bold('\nEmail to send:\n'));
|
|
795
|
+
console.log(chalk.cyan('From: ') + chalk.white(fromEmail));
|
|
796
|
+
console.log(chalk.cyan('To: ') + chalk.white(options.to));
|
|
797
|
+
console.log(chalk.cyan('Subject: ') + chalk.white(options.subject));
|
|
798
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
799
|
+
console.log(options.body);
|
|
800
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
801
|
+
|
|
802
|
+
if (options.dryRun) {
|
|
803
|
+
console.log(chalk.yellow('\nDry run: Email was not sent.'));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!options.confirm) {
|
|
808
|
+
console.log(chalk.yellow('\nThis will send the email above.'));
|
|
809
|
+
console.log(chalk.gray('Use --confirm to skip this prompt, or --dry-run to preview without sending.\n'));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
console.log(chalk.cyan('\nSending...'));
|
|
814
|
+
|
|
815
|
+
const result = await sendEmail(account, {
|
|
816
|
+
to: options.to,
|
|
817
|
+
subject: options.subject,
|
|
818
|
+
body: options.body
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
if (result.success) {
|
|
822
|
+
// Log the sent email
|
|
823
|
+
logSentEmail({
|
|
824
|
+
account,
|
|
825
|
+
to: options.to,
|
|
826
|
+
subject: options.subject,
|
|
827
|
+
body: options.body,
|
|
828
|
+
id: result.id,
|
|
829
|
+
threadId: result.threadId
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
console.log(chalk.green(`\n✓ Email sent successfully!`));
|
|
833
|
+
console.log(chalk.gray(` ID: ${result.id}`));
|
|
834
|
+
console.log(chalk.gray(` Logged to: ${getSentLogPath()}`));
|
|
835
|
+
} else {
|
|
836
|
+
console.log(chalk.red(`\n✗ Failed to send email: ${result.error}`));
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
839
|
+
} catch (error) {
|
|
840
|
+
console.error(chalk.red('Error sending email:'), error.message);
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
program
|
|
846
|
+
.command('reply')
|
|
847
|
+
.description('Reply to an email')
|
|
848
|
+
.requiredOption('--id <id>', 'Message ID to reply to')
|
|
849
|
+
.requiredOption('-b, --body <body>', 'Reply body text')
|
|
850
|
+
.option('-a, --account <name>', 'Account to reply from')
|
|
851
|
+
.option('--dry-run', 'Preview the reply without sending')
|
|
852
|
+
.option('--confirm', 'Skip confirmation prompt')
|
|
853
|
+
.action(async (options) => {
|
|
854
|
+
try {
|
|
855
|
+
const { account, error } = resolveAccount(options.account, chalk);
|
|
856
|
+
if (error) {
|
|
857
|
+
console.log(error);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Fetch original email to show context
|
|
862
|
+
const original = await getEmailContent(account, options.id);
|
|
863
|
+
if (!original) {
|
|
864
|
+
console.log(chalk.red(`Email ${options.id} not found in account "${account}".`));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Build the subject we'll use
|
|
869
|
+
const replySubject = original.subject.toLowerCase().startsWith('re:')
|
|
870
|
+
? original.subject
|
|
871
|
+
: `Re: ${original.subject}`;
|
|
872
|
+
|
|
873
|
+
// Show preview
|
|
874
|
+
console.log(chalk.bold('\nReply to:\n'));
|
|
875
|
+
console.log(chalk.gray('Original from: ') + chalk.white(original.from));
|
|
876
|
+
console.log(chalk.gray('Original subject: ') + chalk.white(original.subject));
|
|
877
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
878
|
+
console.log(chalk.bold('\nYour reply:\n'));
|
|
879
|
+
console.log(chalk.cyan('To: ') + chalk.white(original.from));
|
|
880
|
+
console.log(chalk.cyan('Subject: ') + chalk.white(replySubject));
|
|
881
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
882
|
+
console.log(options.body);
|
|
883
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
884
|
+
|
|
885
|
+
if (options.dryRun) {
|
|
886
|
+
console.log(chalk.yellow('\nDry run: Reply was not sent.'));
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (!options.confirm) {
|
|
891
|
+
console.log(chalk.yellow('\nThis will send the reply above.'));
|
|
892
|
+
console.log(chalk.gray('Use --confirm to skip this prompt, or --dry-run to preview without sending.\n'));
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
console.log(chalk.cyan('\nSending reply...'));
|
|
897
|
+
|
|
898
|
+
const result = await replyToEmail(account, options.id, options.body);
|
|
899
|
+
|
|
900
|
+
if (result.success) {
|
|
901
|
+
// Log the sent reply
|
|
902
|
+
logSentEmail({
|
|
903
|
+
account,
|
|
904
|
+
to: original.from,
|
|
905
|
+
subject: replySubject,
|
|
906
|
+
body: options.body,
|
|
907
|
+
id: result.id,
|
|
908
|
+
threadId: result.threadId,
|
|
909
|
+
replyToId: options.id
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
console.log(chalk.green(`\n✓ Reply sent successfully!`));
|
|
913
|
+
console.log(chalk.gray(` ID: ${result.id}`));
|
|
914
|
+
console.log(chalk.gray(` Logged to: ${getSentLogPath()}`));
|
|
915
|
+
} else {
|
|
916
|
+
console.log(chalk.red(`\n✗ Failed to send reply: ${result.error}`));
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
} catch (error) {
|
|
920
|
+
console.error(chalk.red('Error replying:'), error.message);
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
|
|
564
925
|
program
|
|
565
926
|
.command('delete')
|
|
566
927
|
.description('Move emails to trash')
|