inboxd 1.0.12 → 1.1.0
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 +156 -9
- package/CLAUDE.md +39 -8
- package/package.json +1 -1
- package/src/archive-log.js +104 -0
- package/src/cli.js +531 -17
- package/src/deletion-log.js +101 -0
- package/src/gmail-monitor.js +58 -0
- package/src/sent-log.js +35 -0
- package/tests/archive-log.test.js +196 -0
- package/tests/cleanup-suggest.test.js +239 -0
- package/tests/gmail-monitor.test.js +293 -0
- package/tests/install-service.test.js +210 -0
- package/tests/interactive-confirm.test.js +175 -0
- package/tests/json-output.test.js +189 -0
- package/tests/stats.test.js +218 -0
- package/tests/unarchive.test.js +228 -0
package/src/cli.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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, unarchiveEmails, groupEmailsBySender, getEmailContent, searchEmails, sendEmail, replyToEmail, extractLinks } = require('./gmail-monitor');
|
|
5
|
+
const { logArchives, getRecentArchives, getArchiveLogPath, removeArchiveLogEntries } = require('./archive-log');
|
|
5
6
|
const { getState, updateLastCheck, markEmailsSeen, getNewEmailIds, clearOldSeenEmails } = require('./state');
|
|
6
7
|
const { notifyNewEmails } = require('./notifier');
|
|
7
8
|
const { authorize, addAccount, getAccounts, getAccountEmail, removeAccount, removeAllAccounts, renameTokenFile, validateCredentialsFile, hasCredentials, isConfigured, installCredentials } = require('./gmail-auth');
|
|
8
|
-
const { logDeletions, getRecentDeletions, getLogPath, readLog, removeLogEntries } = require('./deletion-log');
|
|
9
|
+
const { logDeletions, getRecentDeletions, getLogPath, readLog, removeLogEntries, getStats: getDeletionStats, analyzePatterns } = require('./deletion-log');
|
|
9
10
|
const { getSkillStatus, checkForUpdate, installSkill, SKILL_DEST_DIR, SOURCE_MARKER } = require('./skill-installer');
|
|
10
|
-
const { logSentEmail, getSentLogPath } = require('./sent-log');
|
|
11
|
+
const { logSentEmail, getSentLogPath, getSentStats } = require('./sent-log');
|
|
11
12
|
const readline = require('readline');
|
|
12
13
|
const path = require('path');
|
|
13
14
|
const os = require('os');
|
|
@@ -378,8 +379,15 @@ async function main() {
|
|
|
378
379
|
program
|
|
379
380
|
.command('accounts')
|
|
380
381
|
.description('List all configured accounts')
|
|
381
|
-
.
|
|
382
|
+
.option('--json', 'Output as JSON')
|
|
383
|
+
.action(async (options) => {
|
|
382
384
|
const accounts = getAccounts();
|
|
385
|
+
|
|
386
|
+
if (options.json) {
|
|
387
|
+
console.log(JSON.stringify({ accounts }, null, 2));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
383
391
|
if (accounts.length === 0) {
|
|
384
392
|
console.log(chalk.gray('No accounts configured. Run: inbox setup'));
|
|
385
393
|
return;
|
|
@@ -805,9 +813,18 @@ async function main() {
|
|
|
805
813
|
}
|
|
806
814
|
|
|
807
815
|
if (!options.confirm) {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
816
|
+
const rl = readline.createInterface({
|
|
817
|
+
input: process.stdin,
|
|
818
|
+
output: process.stdout,
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const answer = await prompt(rl, chalk.yellow('\nSend this email? (y/N): '));
|
|
822
|
+
rl.close();
|
|
823
|
+
|
|
824
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
825
|
+
console.log(chalk.gray('Cancelled. Email was not sent.\n'));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
811
828
|
}
|
|
812
829
|
|
|
813
830
|
console.log(chalk.cyan('\nSending...'));
|
|
@@ -888,9 +905,18 @@ async function main() {
|
|
|
888
905
|
}
|
|
889
906
|
|
|
890
907
|
if (!options.confirm) {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
908
|
+
const rl = readline.createInterface({
|
|
909
|
+
input: process.stdin,
|
|
910
|
+
output: process.stdout,
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
const answer = await prompt(rl, chalk.yellow('\nSend this reply? (y/N): '));
|
|
914
|
+
rl.close();
|
|
915
|
+
|
|
916
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
917
|
+
console.log(chalk.gray('Cancelled. Reply was not sent.\n'));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
894
920
|
}
|
|
895
921
|
|
|
896
922
|
console.log(chalk.cyan('\nSending reply...'));
|
|
@@ -933,6 +959,7 @@ async function main() {
|
|
|
933
959
|
.option('--confirm', 'Skip confirmation prompt')
|
|
934
960
|
.option('--dry-run', 'Show what would be deleted without deleting')
|
|
935
961
|
.option('--force', 'Override safety warnings (required for short patterns or large matches)')
|
|
962
|
+
.option('--json', 'Output as JSON (for --dry-run)')
|
|
936
963
|
.action(async (options) => {
|
|
937
964
|
try {
|
|
938
965
|
let emailsToDelete = [];
|
|
@@ -1075,6 +1102,20 @@ async function main() {
|
|
|
1075
1102
|
});
|
|
1076
1103
|
|
|
1077
1104
|
if (options.dryRun) {
|
|
1105
|
+
if (options.json) {
|
|
1106
|
+
console.log(JSON.stringify({
|
|
1107
|
+
dryRun: true,
|
|
1108
|
+
count: emailsToDelete.length,
|
|
1109
|
+
emails: emailsToDelete.map(e => ({
|
|
1110
|
+
id: e.id,
|
|
1111
|
+
account: e.account || 'default',
|
|
1112
|
+
from: e.from,
|
|
1113
|
+
subject: e.subject,
|
|
1114
|
+
date: e.date
|
|
1115
|
+
}))
|
|
1116
|
+
}, null, 2));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1078
1119
|
console.log(chalk.yellow(`\nDry run: ${emailsToDelete.length} email(s) would be deleted.`));
|
|
1079
1120
|
// Output IDs for programmatic use
|
|
1080
1121
|
console.log(chalk.gray(`\nIDs: ${emailsToDelete.map(e => e.id).join(',')}`));
|
|
@@ -1140,10 +1181,21 @@ async function main() {
|
|
|
1140
1181
|
.command('deletion-log')
|
|
1141
1182
|
.description('View recent email deletions')
|
|
1142
1183
|
.option('-n, --days <number>', 'Show deletions from last N days', '30')
|
|
1184
|
+
.option('--json', 'Output as JSON')
|
|
1143
1185
|
.action(async (options) => {
|
|
1144
1186
|
const days = parseInt(options.days, 10);
|
|
1145
1187
|
const deletions = getRecentDeletions(days);
|
|
1146
1188
|
|
|
1189
|
+
if (options.json) {
|
|
1190
|
+
console.log(JSON.stringify({
|
|
1191
|
+
days,
|
|
1192
|
+
count: deletions.length,
|
|
1193
|
+
logPath: getLogPath(),
|
|
1194
|
+
deletions
|
|
1195
|
+
}, null, 2));
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1147
1199
|
if (deletions.length === 0) {
|
|
1148
1200
|
console.log(chalk.gray(`No deletions in the last ${days} days.`));
|
|
1149
1201
|
console.log(chalk.gray(`Log file: ${getLogPath()}`));
|
|
@@ -1174,11 +1226,131 @@ async function main() {
|
|
|
1174
1226
|
console.log(chalk.gray(`\nLog file: ${getLogPath()}`));
|
|
1175
1227
|
});
|
|
1176
1228
|
|
|
1229
|
+
program
|
|
1230
|
+
.command('stats')
|
|
1231
|
+
.description('Show email activity statistics')
|
|
1232
|
+
.option('-n, --days <number>', 'Period in days', '30')
|
|
1233
|
+
.option('--json', 'Output as JSON')
|
|
1234
|
+
.action(async (options) => {
|
|
1235
|
+
const days = parseInt(options.days, 10);
|
|
1236
|
+
const deletionStats = getDeletionStats(days);
|
|
1237
|
+
const sentStats = getSentStats(days);
|
|
1238
|
+
|
|
1239
|
+
if (options.json) {
|
|
1240
|
+
console.log(JSON.stringify({
|
|
1241
|
+
period: days,
|
|
1242
|
+
deleted: deletionStats,
|
|
1243
|
+
sent: sentStats,
|
|
1244
|
+
}, null, 2));
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
console.log(boxen(chalk.bold(`Email Activity (Last ${days} days)`), {
|
|
1249
|
+
padding: { left: 1, right: 1, top: 0, bottom: 0 },
|
|
1250
|
+
borderStyle: 'round',
|
|
1251
|
+
borderColor: 'cyan',
|
|
1252
|
+
}));
|
|
1253
|
+
|
|
1254
|
+
// Deletion stats
|
|
1255
|
+
console.log(chalk.bold('\nDeleted: ') + chalk.white(`${deletionStats.total} emails`));
|
|
1256
|
+
|
|
1257
|
+
if (deletionStats.total > 0) {
|
|
1258
|
+
// By account
|
|
1259
|
+
const accountEntries = Object.entries(deletionStats.byAccount);
|
|
1260
|
+
if (accountEntries.length > 0) {
|
|
1261
|
+
const accountStr = accountEntries.map(([acc, cnt]) => `${acc} (${cnt})`).join(', ');
|
|
1262
|
+
console.log(chalk.gray(` By account: ${accountStr}`));
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Top senders
|
|
1266
|
+
if (deletionStats.topSenders.length > 0) {
|
|
1267
|
+
const senderStr = deletionStats.topSenders
|
|
1268
|
+
.slice(0, 5)
|
|
1269
|
+
.map(s => `${s.domain} (${s.count})`)
|
|
1270
|
+
.join(', ');
|
|
1271
|
+
console.log(chalk.gray(` Top senders: ${senderStr}`));
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Sent stats
|
|
1276
|
+
console.log(chalk.bold('\nSent: ') + chalk.white(`${sentStats.total} emails`));
|
|
1277
|
+
|
|
1278
|
+
if (sentStats.total > 0) {
|
|
1279
|
+
console.log(chalk.gray(` Replies: ${sentStats.replies}, New: ${sentStats.newEmails}`));
|
|
1280
|
+
|
|
1281
|
+
const accountEntries = Object.entries(sentStats.byAccount);
|
|
1282
|
+
if (accountEntries.length > 1) {
|
|
1283
|
+
const accountStr = accountEntries.map(([acc, cnt]) => `${acc} (${cnt})`).join(', ');
|
|
1284
|
+
console.log(chalk.gray(` By account: ${accountStr}`));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
console.log('');
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
program
|
|
1292
|
+
.command('cleanup-suggest')
|
|
1293
|
+
.description('Get smart cleanup suggestions based on deletion patterns')
|
|
1294
|
+
.option('-n, --days <number>', 'Period to analyze', '30')
|
|
1295
|
+
.option('--json', 'Output as JSON')
|
|
1296
|
+
.action(async (options) => {
|
|
1297
|
+
const days = parseInt(options.days, 10);
|
|
1298
|
+
const analysis = analyzePatterns(days);
|
|
1299
|
+
|
|
1300
|
+
if (options.json) {
|
|
1301
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
console.log(boxen(chalk.bold('Cleanup Suggestions'), {
|
|
1306
|
+
padding: { left: 1, right: 1, top: 0, bottom: 0 },
|
|
1307
|
+
borderStyle: 'round',
|
|
1308
|
+
borderColor: 'cyan',
|
|
1309
|
+
}));
|
|
1310
|
+
|
|
1311
|
+
if (analysis.totalDeleted === 0) {
|
|
1312
|
+
console.log(chalk.gray(`\nNo deletions in the last ${days} days to analyze.\n`));
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
console.log(chalk.gray(`\nBased on ${analysis.totalDeleted} deletions in the last ${days} days:\n`));
|
|
1317
|
+
|
|
1318
|
+
// Frequent deleters
|
|
1319
|
+
if (analysis.frequentDeleters.length > 0) {
|
|
1320
|
+
console.log(chalk.bold('You frequently delete emails from:'));
|
|
1321
|
+
analysis.frequentDeleters.slice(0, 5).forEach(sender => {
|
|
1322
|
+
console.log(chalk.yellow(` ${sender.domain}`) + chalk.gray(` (${sender.deletedCount} deleted this month)`));
|
|
1323
|
+
console.log(chalk.cyan(` → ${sender.suggestion}`));
|
|
1324
|
+
});
|
|
1325
|
+
console.log('');
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Never-read senders
|
|
1329
|
+
if (analysis.neverReadSenders.length > 0) {
|
|
1330
|
+
console.log(chalk.bold('Never-read senders (deleted unread):'));
|
|
1331
|
+
analysis.neverReadSenders.slice(0, 5).forEach(sender => {
|
|
1332
|
+
console.log(chalk.yellow(` ${sender.domain}`) + chalk.gray(` (${sender.deletedCount} emails)`));
|
|
1333
|
+
console.log(chalk.cyan(` → ${sender.suggestion}`));
|
|
1334
|
+
});
|
|
1335
|
+
console.log('');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (analysis.frequentDeleters.length === 0 && analysis.neverReadSenders.length === 0) {
|
|
1339
|
+
console.log(chalk.gray('No strong patterns detected. Your inbox management looks good!\n'));
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Helpful tip
|
|
1343
|
+
console.log(chalk.gray('Tip: Use these commands to act on suggestions:'));
|
|
1344
|
+
console.log(chalk.gray(' inbox delete --sender "domain.com" --dry-run'));
|
|
1345
|
+
console.log(chalk.gray(' inbox search -q "from:sender@domain.com"\n'));
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1177
1348
|
program
|
|
1178
1349
|
.command('restore')
|
|
1179
1350
|
.description('Restore deleted emails from trash')
|
|
1180
1351
|
.option('--ids <ids>', 'Comma-separated message IDs to restore')
|
|
1181
1352
|
.option('--last <number>', 'Restore the N most recent deletions', parseInt)
|
|
1353
|
+
.option('--json', 'Output as JSON')
|
|
1182
1354
|
.action(async (options) => {
|
|
1183
1355
|
try {
|
|
1184
1356
|
let emailsToRestore = [];
|
|
@@ -1258,11 +1430,40 @@ async function main() {
|
|
|
1258
1430
|
// Clean up log
|
|
1259
1431
|
if (successfulIds.length > 0) {
|
|
1260
1432
|
removeLogEntries(successfulIds);
|
|
1261
|
-
|
|
1433
|
+
if (!options.json) {
|
|
1434
|
+
console.log(chalk.gray(`\nRemoved ${successfulIds.length} entries from deletion log.`));
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// JSON output at the end
|
|
1439
|
+
if (options.json) {
|
|
1440
|
+
const allResults = [];
|
|
1441
|
+
for (const [account, emails] of Object.entries(byAccount)) {
|
|
1442
|
+
const ids = emails.map(e => e.id);
|
|
1443
|
+
emails.forEach(e => {
|
|
1444
|
+
const wasSuccessful = successfulIds.includes(e.id);
|
|
1445
|
+
allResults.push({
|
|
1446
|
+
id: e.id,
|
|
1447
|
+
account,
|
|
1448
|
+
from: e.from,
|
|
1449
|
+
subject: e.subject,
|
|
1450
|
+
success: wasSuccessful
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
console.log(JSON.stringify({
|
|
1455
|
+
restored: successfulIds.length,
|
|
1456
|
+
failed: emailsToRestore.length - successfulIds.length,
|
|
1457
|
+
results: allResults
|
|
1458
|
+
}, null, 2));
|
|
1262
1459
|
}
|
|
1263
1460
|
|
|
1264
1461
|
} catch (error) {
|
|
1265
|
-
|
|
1462
|
+
if (options.json) {
|
|
1463
|
+
console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
1464
|
+
} else {
|
|
1465
|
+
console.error(chalk.red('Error restoring emails:'), error.message);
|
|
1466
|
+
}
|
|
1266
1467
|
process.exit(1);
|
|
1267
1468
|
}
|
|
1268
1469
|
});
|
|
@@ -1325,6 +1526,64 @@ async function main() {
|
|
|
1325
1526
|
}
|
|
1326
1527
|
});
|
|
1327
1528
|
|
|
1529
|
+
program
|
|
1530
|
+
.command('mark-unread')
|
|
1531
|
+
.description('Mark emails as unread')
|
|
1532
|
+
.requiredOption('--ids <ids>', 'Comma-separated message IDs to mark as unread')
|
|
1533
|
+
.option('-a, --account <name>', 'Account name')
|
|
1534
|
+
.action(async (options) => {
|
|
1535
|
+
try {
|
|
1536
|
+
const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
|
|
1537
|
+
|
|
1538
|
+
if (ids.length === 0) {
|
|
1539
|
+
console.log(chalk.yellow('No message IDs provided.'));
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Get account - if not specified, try to find from configured accounts
|
|
1544
|
+
let account = options.account;
|
|
1545
|
+
if (!account) {
|
|
1546
|
+
const accounts = getAccounts();
|
|
1547
|
+
if (accounts.length === 1) {
|
|
1548
|
+
account = accounts[0].name;
|
|
1549
|
+
} else if (accounts.length > 1) {
|
|
1550
|
+
console.log(chalk.yellow('Multiple accounts configured. Please specify --account <name>'));
|
|
1551
|
+
console.log(chalk.gray('Available accounts:'));
|
|
1552
|
+
accounts.forEach(a => console.log(chalk.gray(` - ${a.name}`)));
|
|
1553
|
+
return;
|
|
1554
|
+
} else {
|
|
1555
|
+
account = 'default';
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
console.log(chalk.cyan(`Marking ${ids.length} email(s) as unread...`));
|
|
1560
|
+
|
|
1561
|
+
const results = await markAsUnread(account, ids);
|
|
1562
|
+
|
|
1563
|
+
const succeeded = results.filter(r => r.success).length;
|
|
1564
|
+
const failed = results.filter(r => !r.success).length;
|
|
1565
|
+
|
|
1566
|
+
if (succeeded > 0) {
|
|
1567
|
+
console.log(chalk.green(`\nMarked ${succeeded} email(s) as unread.`));
|
|
1568
|
+
}
|
|
1569
|
+
if (failed > 0) {
|
|
1570
|
+
console.log(chalk.red(`Failed to mark ${failed} email(s) as unread.`));
|
|
1571
|
+
results.filter(r => !r.success).forEach(r => {
|
|
1572
|
+
console.log(chalk.red(` - ${r.id}: ${r.error}`));
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
if (error.message.includes('403') || error.code === 403) {
|
|
1578
|
+
console.error(chalk.red('Permission denied. You may need to re-authenticate with updated scopes.'));
|
|
1579
|
+
console.error(chalk.yellow('Run: inbox auth -a <account>'));
|
|
1580
|
+
} else {
|
|
1581
|
+
console.error(chalk.red('Error marking emails as unread:'), error.message);
|
|
1582
|
+
}
|
|
1583
|
+
process.exit(1);
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1328
1587
|
program
|
|
1329
1588
|
.command('archive')
|
|
1330
1589
|
.description('Archive emails (remove from inbox, keep in All Mail)')
|
|
@@ -1388,6 +1647,11 @@ async function main() {
|
|
|
1388
1647
|
console.log(chalk.gray('Use --confirm to skip this prompt.\n'));
|
|
1389
1648
|
}
|
|
1390
1649
|
|
|
1650
|
+
// Log archives BEFORE actually archiving (for undo)
|
|
1651
|
+
const emailsWithAccount = emailsToArchive.map(e => ({ ...e, account }));
|
|
1652
|
+
logArchives(emailsWithAccount);
|
|
1653
|
+
console.log(chalk.gray(`Logged to: ${getArchiveLogPath()}`));
|
|
1654
|
+
|
|
1391
1655
|
const results = await archiveEmails(account, emailsToArchive.map(e => e.id));
|
|
1392
1656
|
|
|
1393
1657
|
const succeeded = results.filter(r => r.success).length;
|
|
@@ -1395,6 +1659,7 @@ async function main() {
|
|
|
1395
1659
|
|
|
1396
1660
|
if (succeeded > 0) {
|
|
1397
1661
|
console.log(chalk.green(`\nArchived ${succeeded} email(s).`));
|
|
1662
|
+
console.log(chalk.gray(`Tip: Use 'inbox unarchive --last ${succeeded}' to undo.`));
|
|
1398
1663
|
}
|
|
1399
1664
|
if (failed > 0) {
|
|
1400
1665
|
console.log(chalk.red(`Failed to archive ${failed} email(s).`));
|
|
@@ -1414,22 +1679,271 @@ async function main() {
|
|
|
1414
1679
|
}
|
|
1415
1680
|
});
|
|
1416
1681
|
|
|
1682
|
+
program
|
|
1683
|
+
.command('unarchive')
|
|
1684
|
+
.description('Restore archived emails back to inbox')
|
|
1685
|
+
.option('--ids <ids>', 'Comma-separated message IDs to unarchive')
|
|
1686
|
+
.option('--last <number>', 'Unarchive the N most recent archives', parseInt)
|
|
1687
|
+
.option('--json', 'Output as JSON')
|
|
1688
|
+
.action(async (options) => {
|
|
1689
|
+
try {
|
|
1690
|
+
let emailsToUnarchive = [];
|
|
1691
|
+
|
|
1692
|
+
// Scenario 1: Unarchive by explicit IDs
|
|
1693
|
+
if (options.ids) {
|
|
1694
|
+
const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
|
|
1695
|
+
const log = getRecentArchives(30);
|
|
1696
|
+
|
|
1697
|
+
for (const id of ids) {
|
|
1698
|
+
// Find in log first to get the account
|
|
1699
|
+
const entry = log.find(e => e.id === id);
|
|
1700
|
+
if (entry) {
|
|
1701
|
+
emailsToUnarchive.push(entry);
|
|
1702
|
+
} else {
|
|
1703
|
+
if (!options.json) {
|
|
1704
|
+
console.log(chalk.yellow(`Warning: ID ${id} not found in local archive log.`));
|
|
1705
|
+
console.log(chalk.gray(`Cannot determine account automatically. Please unarchive manually via Gmail.`));
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
// Scenario 2: Unarchive last N items
|
|
1711
|
+
else if (options.last) {
|
|
1712
|
+
const count = options.last;
|
|
1713
|
+
const archives = getRecentArchives(30);
|
|
1714
|
+
// Sort by archivedAt desc
|
|
1715
|
+
archives.sort((a, b) => new Date(b.archivedAt) - new Date(a.archivedAt));
|
|
1716
|
+
|
|
1717
|
+
emailsToUnarchive = archives.slice(0, count);
|
|
1718
|
+
} else {
|
|
1719
|
+
if (options.json) {
|
|
1720
|
+
console.log(JSON.stringify({ error: 'Must specify either --ids or --last' }, null, 2));
|
|
1721
|
+
} else {
|
|
1722
|
+
console.log(chalk.red('Error: Must specify either --ids or --last'));
|
|
1723
|
+
console.log(chalk.gray('Examples:'));
|
|
1724
|
+
console.log(chalk.gray(' inbox unarchive --last 1'));
|
|
1725
|
+
console.log(chalk.gray(' inbox unarchive --ids 12345,67890'));
|
|
1726
|
+
}
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (emailsToUnarchive.length === 0) {
|
|
1731
|
+
if (options.json) {
|
|
1732
|
+
console.log(JSON.stringify({ unarchived: 0, failed: 0, results: [] }, null, 2));
|
|
1733
|
+
} else {
|
|
1734
|
+
console.log(chalk.yellow('No emails found to unarchive.'));
|
|
1735
|
+
}
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
if (!options.json) {
|
|
1740
|
+
console.log(chalk.cyan(`Unarchiving ${emailsToUnarchive.length} email(s)...`));
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Group by account to batch API calls
|
|
1744
|
+
const byAccount = {};
|
|
1745
|
+
for (const email of emailsToUnarchive) {
|
|
1746
|
+
if (!byAccount[email.account]) {
|
|
1747
|
+
byAccount[email.account] = [];
|
|
1748
|
+
}
|
|
1749
|
+
byAccount[email.account].push(email);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
const successfulIds = [];
|
|
1753
|
+
|
|
1754
|
+
for (const [account, emails] of Object.entries(byAccount)) {
|
|
1755
|
+
const ids = emails.map(e => e.id);
|
|
1756
|
+
if (!options.json) {
|
|
1757
|
+
console.log(chalk.gray(`Unarchiving ${ids.length} email(s) for account "${account}"...`));
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
const results = await unarchiveEmails(account, ids);
|
|
1761
|
+
|
|
1762
|
+
const succeeded = results.filter(r => r.success);
|
|
1763
|
+
const failed = results.filter(r => !r.success);
|
|
1764
|
+
|
|
1765
|
+
if (!options.json) {
|
|
1766
|
+
if (succeeded.length > 0) {
|
|
1767
|
+
console.log(chalk.green(` ✓ Unarchived ${succeeded.length} email(s)`));
|
|
1768
|
+
}
|
|
1769
|
+
if (failed.length > 0) {
|
|
1770
|
+
console.log(chalk.red(` ✗ Failed to unarchive ${failed.length} email(s)`));
|
|
1771
|
+
failed.forEach(r => {
|
|
1772
|
+
console.log(chalk.gray(` - ID ${r.id}: ${r.error}`));
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
successfulIds.push(...succeeded.map(r => r.id));
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Clean up log
|
|
1781
|
+
if (successfulIds.length > 0) {
|
|
1782
|
+
removeArchiveLogEntries(successfulIds);
|
|
1783
|
+
if (!options.json) {
|
|
1784
|
+
console.log(chalk.gray(`\nRemoved ${successfulIds.length} entries from archive log.`));
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// JSON output
|
|
1789
|
+
if (options.json) {
|
|
1790
|
+
const allResults = [];
|
|
1791
|
+
for (const [account, emails] of Object.entries(byAccount)) {
|
|
1792
|
+
emails.forEach(e => {
|
|
1793
|
+
const wasSuccessful = successfulIds.includes(e.id);
|
|
1794
|
+
allResults.push({
|
|
1795
|
+
id: e.id,
|
|
1796
|
+
account,
|
|
1797
|
+
from: e.from,
|
|
1798
|
+
subject: e.subject,
|
|
1799
|
+
success: wasSuccessful
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
console.log(JSON.stringify({
|
|
1804
|
+
unarchived: successfulIds.length,
|
|
1805
|
+
failed: emailsToUnarchive.length - successfulIds.length,
|
|
1806
|
+
results: allResults
|
|
1807
|
+
}, null, 2));
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
if (options.json) {
|
|
1812
|
+
console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
1813
|
+
} else {
|
|
1814
|
+
console.error(chalk.red('Error unarchiving emails:'), error.message);
|
|
1815
|
+
}
|
|
1816
|
+
process.exit(1);
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1417
1820
|
program
|
|
1418
1821
|
.command('install-service')
|
|
1419
|
-
.description('Install background service (launchd
|
|
1822
|
+
.description('Install background service (launchd for macOS, systemd for Linux)')
|
|
1420
1823
|
.option('-i, --interval <minutes>', 'Check interval in minutes', '5')
|
|
1824
|
+
.option('--uninstall', 'Remove the service instead of installing')
|
|
1421
1825
|
.action(async (options) => {
|
|
1422
|
-
|
|
1826
|
+
const { execSync } = require('child_process');
|
|
1827
|
+
const interval = parseInt(options.interval, 10);
|
|
1828
|
+
const homeDir = os.homedir();
|
|
1829
|
+
|
|
1830
|
+
// Linux systemd support
|
|
1831
|
+
if (process.platform === 'linux') {
|
|
1832
|
+
const systemdUserDir = path.join(homeDir, '.config/systemd/user');
|
|
1833
|
+
const servicePath = path.join(systemdUserDir, 'inboxd.service');
|
|
1834
|
+
const timerPath = path.join(systemdUserDir, 'inboxd.timer');
|
|
1835
|
+
|
|
1836
|
+
// Uninstall
|
|
1837
|
+
if (options.uninstall) {
|
|
1838
|
+
try {
|
|
1839
|
+
execSync('systemctl --user stop inboxd.timer 2>/dev/null', { stdio: 'ignore' });
|
|
1840
|
+
execSync('systemctl --user disable inboxd.timer 2>/dev/null', { stdio: 'ignore' });
|
|
1841
|
+
} catch {
|
|
1842
|
+
// Ignore - may not be running
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
let removed = false;
|
|
1846
|
+
if (fs.existsSync(servicePath)) {
|
|
1847
|
+
fs.unlinkSync(servicePath);
|
|
1848
|
+
removed = true;
|
|
1849
|
+
}
|
|
1850
|
+
if (fs.existsSync(timerPath)) {
|
|
1851
|
+
fs.unlinkSync(timerPath);
|
|
1852
|
+
removed = true;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
if (removed) {
|
|
1856
|
+
try {
|
|
1857
|
+
execSync('systemctl --user daemon-reload', { stdio: 'ignore' });
|
|
1858
|
+
} catch {
|
|
1859
|
+
// Ignore
|
|
1860
|
+
}
|
|
1861
|
+
console.log(chalk.green('\n✓ Service uninstalled.'));
|
|
1862
|
+
console.log(chalk.gray(` Removed: ${servicePath}`));
|
|
1863
|
+
console.log(chalk.gray(` Removed: ${timerPath}\n`));
|
|
1864
|
+
} else {
|
|
1865
|
+
console.log(chalk.gray('\nService was not installed.\n'));
|
|
1866
|
+
}
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Install
|
|
1871
|
+
const nodePath = process.execPath;
|
|
1872
|
+
const scriptPath = path.resolve(__dirname, 'cli.js');
|
|
1873
|
+
const workingDir = path.resolve(__dirname, '..');
|
|
1874
|
+
|
|
1875
|
+
const serviceContent = `[Unit]
|
|
1876
|
+
Description=inboxd - Gmail monitoring and notifications
|
|
1877
|
+
After=network-online.target
|
|
1878
|
+
Wants=network-online.target
|
|
1879
|
+
|
|
1880
|
+
[Service]
|
|
1881
|
+
Type=oneshot
|
|
1882
|
+
ExecStart=${nodePath} ${scriptPath} check --quiet
|
|
1883
|
+
WorkingDirectory=${workingDir}
|
|
1884
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
1885
|
+
|
|
1886
|
+
[Install]
|
|
1887
|
+
WantedBy=default.target
|
|
1888
|
+
`;
|
|
1889
|
+
|
|
1890
|
+
const timerContent = `[Unit]
|
|
1891
|
+
Description=Run inboxd every ${interval} minutes
|
|
1892
|
+
|
|
1893
|
+
[Timer]
|
|
1894
|
+
OnBootSec=1min
|
|
1895
|
+
OnUnitActiveSec=${interval}min
|
|
1896
|
+
Persistent=true
|
|
1897
|
+
|
|
1898
|
+
[Install]
|
|
1899
|
+
WantedBy=timers.target
|
|
1900
|
+
`;
|
|
1901
|
+
|
|
1902
|
+
try {
|
|
1903
|
+
if (!fs.existsSync(systemdUserDir)) {
|
|
1904
|
+
fs.mkdirSync(systemdUserDir, { recursive: true });
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
fs.writeFileSync(servicePath, serviceContent);
|
|
1908
|
+
fs.writeFileSync(timerPath, timerContent);
|
|
1909
|
+
|
|
1910
|
+
// Reload and enable
|
|
1911
|
+
execSync('systemctl --user daemon-reload');
|
|
1912
|
+
execSync('systemctl --user enable inboxd.timer');
|
|
1913
|
+
execSync('systemctl --user start inboxd.timer');
|
|
1914
|
+
|
|
1915
|
+
console.log(chalk.green('\n✓ Background service installed and running!'));
|
|
1916
|
+
console.log(chalk.gray(` Service: ${servicePath}`));
|
|
1917
|
+
console.log(chalk.gray(` Timer: ${timerPath}`));
|
|
1918
|
+
console.log(chalk.gray(` Interval: every ${interval} minutes\n`));
|
|
1919
|
+
console.log(chalk.white('The service will:'));
|
|
1920
|
+
console.log(chalk.gray(' • Check your inbox automatically'));
|
|
1921
|
+
console.log(chalk.gray(' • Send notifications for new emails'));
|
|
1922
|
+
console.log(chalk.gray(' • Start on login\n'));
|
|
1923
|
+
console.log(chalk.white('Useful commands:'));
|
|
1924
|
+
console.log(chalk.cyan(' systemctl --user status inboxd.timer') + chalk.gray(' # Check status'));
|
|
1925
|
+
console.log(chalk.cyan(' journalctl --user -u inboxd') + chalk.gray(' # View logs'));
|
|
1926
|
+
console.log(chalk.cyan(' inbox install-service --uninstall') + chalk.gray(' # Remove service\n'));
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
console.error(chalk.red('Error installing service:'), error.message);
|
|
1929
|
+
console.log(chalk.yellow('\nThe config files may have been created but could not be enabled.'));
|
|
1930
|
+
console.log(chalk.white('Try running manually:'));
|
|
1931
|
+
console.log(chalk.cyan(' systemctl --user daemon-reload'));
|
|
1932
|
+
console.log(chalk.cyan(' systemctl --user enable --now inboxd.timer\n'));
|
|
1933
|
+
}
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Platform check for unsupported platforms
|
|
1423
1938
|
if (process.platform !== 'darwin') {
|
|
1424
|
-
console.log(chalk.red('\nError: install-service is only supported on macOS.'));
|
|
1425
|
-
console.log(chalk.gray('This command uses launchd
|
|
1939
|
+
console.log(chalk.red('\nError: install-service is only supported on macOS and Linux.'));
|
|
1940
|
+
console.log(chalk.gray('This command uses launchd (macOS) or systemd (Linux).\n'));
|
|
1426
1941
|
console.log(chalk.white('For other platforms, you can set up a cron job or scheduled task:'));
|
|
1427
1942
|
console.log(chalk.cyan(` */5 * * * * ${process.execPath} ${path.resolve(__dirname, 'cli.js')} check --quiet`));
|
|
1428
1943
|
console.log('');
|
|
1429
1944
|
return;
|
|
1430
1945
|
}
|
|
1431
1946
|
|
|
1432
|
-
const interval = parseInt(options.interval, 10);
|
|
1433
1947
|
const seconds = interval * 60;
|
|
1434
1948
|
|
|
1435
1949
|
// Determine paths
|