inboxd 1.0.11 → 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 +239 -4
- package/CLAUDE.md +35 -4
- package/package.json +1 -1
- package/src/cli.js +422 -3
- package/src/gmail-monitor.js +393 -0
- package/src/sent-log.js +87 -0
- package/tests/gmail-monitor-patterns.test.js +232 -0
- package/tests/gmail-monitor.test.js +293 -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
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, 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');
|
|
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')
|
|
@@ -964,6 +1325,64 @@ async function main() {
|
|
|
964
1325
|
}
|
|
965
1326
|
});
|
|
966
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
|
+
|
|
967
1386
|
program
|
|
968
1387
|
.command('archive')
|
|
969
1388
|
.description('Archive emails (remove from inbox, keep in All Mail)')
|