inboxd 1.0.10 → 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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inboxd",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
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,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
- const emails = await getUnreadEmails(account, maxPerAccount, includeRead);
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')