inboxd 1.0.13 → 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 +17 -6
- package/CLAUDE.md +19 -4
- package/package.json +1 -1
- package/src/archive-log.js +104 -0
- package/src/cli.js +473 -17
- package/src/deletion-log.js +101 -0
- package/src/gmail-monitor.js +29 -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/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, markAsUnread, 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
|
});
|
|
@@ -1446,6 +1647,11 @@ async function main() {
|
|
|
1446
1647
|
console.log(chalk.gray('Use --confirm to skip this prompt.\n'));
|
|
1447
1648
|
}
|
|
1448
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
|
+
|
|
1449
1655
|
const results = await archiveEmails(account, emailsToArchive.map(e => e.id));
|
|
1450
1656
|
|
|
1451
1657
|
const succeeded = results.filter(r => r.success).length;
|
|
@@ -1453,6 +1659,7 @@ async function main() {
|
|
|
1453
1659
|
|
|
1454
1660
|
if (succeeded > 0) {
|
|
1455
1661
|
console.log(chalk.green(`\nArchived ${succeeded} email(s).`));
|
|
1662
|
+
console.log(chalk.gray(`Tip: Use 'inbox unarchive --last ${succeeded}' to undo.`));
|
|
1456
1663
|
}
|
|
1457
1664
|
if (failed > 0) {
|
|
1458
1665
|
console.log(chalk.red(`Failed to archive ${failed} email(s).`));
|
|
@@ -1472,22 +1679,271 @@ async function main() {
|
|
|
1472
1679
|
}
|
|
1473
1680
|
});
|
|
1474
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
|
+
|
|
1475
1820
|
program
|
|
1476
1821
|
.command('install-service')
|
|
1477
|
-
.description('Install background service (launchd
|
|
1822
|
+
.description('Install background service (launchd for macOS, systemd for Linux)')
|
|
1478
1823
|
.option('-i, --interval <minutes>', 'Check interval in minutes', '5')
|
|
1824
|
+
.option('--uninstall', 'Remove the service instead of installing')
|
|
1479
1825
|
.action(async (options) => {
|
|
1480
|
-
|
|
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
|
|
1481
1938
|
if (process.platform !== 'darwin') {
|
|
1482
|
-
console.log(chalk.red('\nError: install-service is only supported on macOS.'));
|
|
1483
|
-
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'));
|
|
1484
1941
|
console.log(chalk.white('For other platforms, you can set up a cron job or scheduled task:'));
|
|
1485
1942
|
console.log(chalk.cyan(` */5 * * * * ${process.execPath} ${path.resolve(__dirname, 'cli.js')} check --quiet`));
|
|
1486
1943
|
console.log('');
|
|
1487
1944
|
return;
|
|
1488
1945
|
}
|
|
1489
1946
|
|
|
1490
|
-
const interval = parseInt(options.interval, 10);
|
|
1491
1947
|
const seconds = interval * 60;
|
|
1492
1948
|
|
|
1493
1949
|
// Determine paths
|