proton-mail-bridge-client 1.11.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +445 -0
  3. package/dist/cli.d.ts +12 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +1627 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.d.ts +56 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +3173 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/scripts/bridge-smoke.d.ts +2 -0
  12. package/dist/scripts/bridge-smoke.d.ts.map +1 -0
  13. package/dist/scripts/bridge-smoke.js +415 -0
  14. package/dist/scripts/bridge-smoke.js.map +1 -0
  15. package/dist/scripts/check-claude-desktop.d.ts +17 -0
  16. package/dist/scripts/check-claude-desktop.d.ts.map +1 -0
  17. package/dist/scripts/check-claude-desktop.js +138 -0
  18. package/dist/scripts/check-claude-desktop.js.map +1 -0
  19. package/dist/scripts/install-claude-desktop.d.ts +39 -0
  20. package/dist/scripts/install-claude-desktop.d.ts.map +1 -0
  21. package/dist/scripts/install-claude-desktop.js +257 -0
  22. package/dist/scripts/install-claude-desktop.js.map +1 -0
  23. package/dist/scripts/setup-claude-desktop.d.ts +15 -0
  24. package/dist/scripts/setup-claude-desktop.d.ts.map +1 -0
  25. package/dist/scripts/setup-claude-desktop.js +187 -0
  26. package/dist/scripts/setup-claude-desktop.js.map +1 -0
  27. package/dist/services/analytics-service.d.ts +45 -0
  28. package/dist/services/analytics-service.d.ts.map +1 -0
  29. package/dist/services/analytics-service.js +189 -0
  30. package/dist/services/analytics-service.js.map +1 -0
  31. package/dist/services/audit-service.d.ts +13 -0
  32. package/dist/services/audit-service.d.ts.map +1 -0
  33. package/dist/services/audit-service.js +67 -0
  34. package/dist/services/audit-service.js.map +1 -0
  35. package/dist/services/background-sync-service.d.ts +28 -0
  36. package/dist/services/background-sync-service.d.ts.map +1 -0
  37. package/dist/services/background-sync-service.js +216 -0
  38. package/dist/services/background-sync-service.js.map +1 -0
  39. package/dist/services/draft-store-service.d.ts +59 -0
  40. package/dist/services/draft-store-service.d.ts.map +1 -0
  41. package/dist/services/draft-store-service.js +251 -0
  42. package/dist/services/draft-store-service.js.map +1 -0
  43. package/dist/services/local-index-service.d.ts +90 -0
  44. package/dist/services/local-index-service.d.ts.map +1 -0
  45. package/dist/services/local-index-service.js +1462 -0
  46. package/dist/services/local-index-service.js.map +1 -0
  47. package/dist/services/simple-imap-service.d.ts +209 -0
  48. package/dist/services/simple-imap-service.d.ts.map +1 -0
  49. package/dist/services/simple-imap-service.js +1100 -0
  50. package/dist/services/simple-imap-service.js.map +1 -0
  51. package/dist/services/smtp-service.d.ts +15 -0
  52. package/dist/services/smtp-service.d.ts.map +1 -0
  53. package/dist/services/smtp-service.js +100 -0
  54. package/dist/services/smtp-service.js.map +1 -0
  55. package/dist/types/index.d.ts +355 -0
  56. package/dist/types/index.d.ts.map +1 -0
  57. package/dist/types/index.js +2 -0
  58. package/dist/types/index.js.map +1 -0
  59. package/dist/utils/helpers.d.ts +45 -0
  60. package/dist/utils/helpers.d.ts.map +1 -0
  61. package/dist/utils/helpers.js +393 -0
  62. package/dist/utils/helpers.js.map +1 -0
  63. package/dist/utils/logger.d.ts +22 -0
  64. package/dist/utils/logger.d.ts.map +1 -0
  65. package/dist/utils/logger.js +91 -0
  66. package/dist/utils/logger.js.map +1 -0
  67. package/dist/utils/runtime-policy.d.ts +12 -0
  68. package/dist/utils/runtime-policy.d.ts.map +1 -0
  69. package/dist/utils/runtime-policy.js +62 -0
  70. package/dist/utils/runtime-policy.js.map +1 -0
  71. package/glama.json +6 -0
  72. package/package.json +91 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1627 @@
1
+ #!/usr/bin/env node
2
+ import { exec } from "node:child_process";
3
+ import { readFile } from "node:fs/promises";
4
+ import { promisify } from "node:util";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8
+ import { buildConfigFromEnv, createServer } from "./index.js";
9
+ import { SimpleIMAPService } from "./services/simple-imap-service.js";
10
+ import { ensureMailboxWriteAllowed, ensureSendAllowed, sanitizeRuntimeConfig } from "./utils/runtime-policy.js";
11
+ import { isValidEmail, lowerCaseAddress, parseEmails, ensureValidEmails } from "./utils/helpers.js";
12
+ import { getClaudeDesktopInstallStatus } from "./scripts/check-claude-desktop.js";
13
+ import { installClaudeDesktopConfig } from "./scripts/install-claude-desktop.js";
14
+ import { runClaudeDesktopSetupWizard } from "./scripts/setup-claude-desktop.js";
15
+ export function parseCliArgs(argv) {
16
+ const positionals = [];
17
+ const flags = {};
18
+ for (let index = 0; index < argv.length; index += 1) {
19
+ const token = argv[index];
20
+ if (!token.startsWith("--")) {
21
+ positionals.push(token);
22
+ continue;
23
+ }
24
+ const key = token.slice(2);
25
+ const next = argv[index + 1];
26
+ if (!next || next.startsWith("--")) {
27
+ flags[key] = true;
28
+ continue;
29
+ }
30
+ flags[key] = next;
31
+ index += 1;
32
+ }
33
+ const command = positionals[0] || "help";
34
+ const subcommand = command === "claude" ? positionals[1] : undefined;
35
+ const consumed = command === "claude" ? 2 : 1;
36
+ return {
37
+ command,
38
+ subcommand,
39
+ positionals: positionals.slice(consumed),
40
+ flags,
41
+ };
42
+ }
43
+ function cliEntryPath() {
44
+ return fileURLToPath(new URL("./index.js", import.meta.url));
45
+ }
46
+ function printHelp() {
47
+ process.stdout.write([
48
+ " ____ ____ ___ _____ ___ _ _ __ __ _ ___ _ ",
49
+ " | _ \\| _ \\ / _ \\_ _/ _ \\| \\ | | | \\/ | / \\ |_ _| | ",
50
+ " | |_) | |_) | | | || || | | | \\| | | |\\/| | / _ \\ | || | ",
51
+ " | __/| _ <| |_| || || |_| | |\\ | | | | |/ ___ \\ | || |___",
52
+ " |_| |_| \\_\\\\___/ |_| \\___/|_| \\_| |_| |_/_/ \\_\\___|_____|",
53
+ " Bridge Client · CLI + Claude Desktop MCP for Proton Mail",
54
+ "",
55
+ "Usage:",
56
+ " proton-mail-bridge <command> [options]",
57
+ "",
58
+ "Commands:",
59
+ " status Show local config, index, runtime, and Claude Desktop status",
60
+ " doctor Verify IMAP, SMTP, and Claude Desktop wiring",
61
+ " connection-status Show live IMAP/SMTP connectivity state",
62
+ " runtime-status Show runtime policy and background sync state",
63
+ " sync Refresh the local index from Proton Bridge",
64
+ " index-status Show local index health and freshness",
65
+ " folders List available folders from Proton Bridge",
66
+ " create-folder <path> Create a mailbox folder (e.g. Folders/Receipts)",
67
+ " rename-folder <p> <p2> Rename a folder (or use --to <newPath>)",
68
+ " delete-folder <path> Delete an empty folder",
69
+ " labels List normalized labels from the local index",
70
+ " threads [query] List normalized threads from the local index",
71
+ " digest Show inbox digest and top actionable threads",
72
+ " followups Show follow-up candidates from the local index",
73
+ " emails List emails from a folder (--folder --limit --offset)",
74
+ " attachments <emailId> List attachments for one message",
75
+ " search [query] Search indexed mail (default) or live mail with --live",
76
+ " read <emailId> Read one email by composite email id",
77
+ " move <emailId> <fldr> Move an email to another folder",
78
+ " archive <emailId> Archive an email",
79
+ " trash <emailId> Move an email to Trash",
80
+ " restore <emailId> Restore an email from Trash to Inbox",
81
+ " mark-read <emailId> Mark read (--unread to flip)",
82
+ " star <emailId> Star an email (--unstar to flip)",
83
+ " delete <emailId> Permanently delete an email",
84
+ " batch <action> <ids…> Apply action to multiple emails (or --ids)",
85
+ " send Send an email (--to --subject --body or stdin)",
86
+ " reply <emailId> Reply to an email (--body or stdin, --reply-all)",
87
+ " forward <emailId> Forward an email (--to, optional --body or stdin)",
88
+ " test-email <addr> Send a test email to verify SMTP",
89
+ " thread <id> Fetch a full thread by id",
90
+ " thread-brief <id> Summarise a thread (latest in/out, next action)",
91
+ " thread-action <id> <a> Apply action to all messages in a thread",
92
+ " actionable List actionable threads",
93
+ " document-threads Find threads with important attachments",
94
+ " meeting-context <who> Prep context for a meeting (--domain also accepted)",
95
+ " stats Mailbox counts and analytics sample",
96
+ " analytics Detailed mailbox analytics (top senders, busy hours)",
97
+ " contacts Contacts ranked by interaction volume",
98
+ " volume-trends Daily message counts (--days, default 30)",
99
+ " watch Wait for mailbox changes via IMAP IDLE (--timeout)",
100
+ " notify Daemon: watch INBOX and send a system notification on new mail (--folder --timeout)",
101
+ " drafts List local drafts",
102
+ " remote-drafts List drafts in the Proton Drafts mailbox",
103
+ " draft-create Create a draft (--to --subject --body or stdin)",
104
+ " draft-read <id> Read a saved draft",
105
+ " draft-update <id> Update a draft (--subject --body --to etc.)",
106
+ " draft-reply <emailId> Create a reply draft (--body or stdin, --reply-all)",
107
+ " draft-forward <id> Create a forward draft (--to, --body or stdin)",
108
+ " draft-sync <id> Sync a local draft to the Proton Drafts mailbox",
109
+ " draft-send <id> Send a saved draft",
110
+ " draft-delete <id> Delete a saved draft",
111
+ " draft-thread-reply <id> Create a reply draft for a thread",
112
+ " tools List every MCP tool exposed by the server",
113
+ " tool <name> Call any MCP tool with JSON arguments",
114
+ " claude setup Run the interactive Claude Desktop setup wizard",
115
+ " claude install Install or update the Claude Desktop runtime",
116
+ " claude check Check Claude Desktop integration status",
117
+ " claude update Alias for claude install",
118
+ "",
119
+ "Global flags:",
120
+ " --json Print machine-readable JSON",
121
+ "",
122
+ "Search flags:",
123
+ " --folder <name> Limit to one folder",
124
+ " --limit <n> Limit results",
125
+ " --live Use live IMAP search instead of the local index",
126
+ " --sync Refresh the local index before indexed search",
127
+ " --label <name> Filter by normalized label",
128
+ " --from <value> Filter by sender",
129
+ " --to <value> Filter by recipient",
130
+ " --subject <value> Filter by subject",
131
+ " --domain <value> Filter by sender domain",
132
+ " --read / --unread Filter by read state",
133
+ " --starred / --unstarred Filter by star state",
134
+ "",
135
+ "Examples:",
136
+ " proton-mail-bridge doctor",
137
+ " proton-mail-bridge sync --folder INBOX --limit 150",
138
+ " proton-mail-bridge folders --json",
139
+ " proton-mail-bridge digest --json",
140
+ " proton-mail-bridge attachments INBOX::25642 --json",
141
+ " proton-mail-bridge search \"label:inbox invoice\"",
142
+ " proton-mail-bridge search --live --from openai.com",
143
+ " proton-mail-bridge read INBOX::25642",
144
+ " proton-mail-bridge tools",
145
+ " proton-mail-bridge tool get_connection_status",
146
+ " proton-mail-bridge tool search_indexed_emails --args '{\"query\":\"invoice\",\"limit\":3}'",
147
+ " proton-mail-bridge claude check",
148
+ ].join("\n"));
149
+ }
150
+ function isTruthyFlag(value) {
151
+ if (value === true) {
152
+ return true;
153
+ }
154
+ if (typeof value !== "string") {
155
+ return false;
156
+ }
157
+ return ["1", "true", "yes", "y"].includes(value.trim().toLowerCase());
158
+ }
159
+ function getStringFlag(flags, key) {
160
+ const value = flags[key];
161
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
162
+ }
163
+ function getNumberFlag(flags, key, fallback) {
164
+ const value = flags[key];
165
+ if (typeof value !== "string" || !value.trim()) {
166
+ return fallback;
167
+ }
168
+ const parsed = Number.parseInt(value, 10);
169
+ if (!Number.isInteger(parsed) || parsed <= 0) {
170
+ throw new Error(`--${key} must be a positive integer.`);
171
+ }
172
+ return parsed;
173
+ }
174
+ function json(value) {
175
+ return `${JSON.stringify(value, null, 2)}\n`;
176
+ }
177
+ function tableEmails(emails) {
178
+ if (emails.length === 0) {
179
+ return "No results.\n";
180
+ }
181
+ return [
182
+ "Results:",
183
+ ...emails.map((email, index) => {
184
+ const from = Array.isArray(email.from)
185
+ ? email.from.map((entry) => entry.address || entry.name || "").filter(Boolean).join(", ")
186
+ : "";
187
+ return `${index + 1}. ${email.id} | ${email.subject || "(no subject)"} | ${from} | ${email.date || email.internalDate || ""}`;
188
+ }),
189
+ ].join("\n") + "\n";
190
+ }
191
+ function summarizeThreadList(value) {
192
+ const threads = Array.isArray(value.threads) ? value.threads : [];
193
+ if (threads.length === 0) {
194
+ return "No threads.\n";
195
+ }
196
+ return [
197
+ `Threads: ${value.total ?? value.totalThreads ?? threads.length}`,
198
+ ...threads.map((thread, index) => {
199
+ const subject = String(thread.subject || "(no subject)");
200
+ const count = thread.messageCount ?? "?";
201
+ const pending = thread.pendingOn ? ` | pending: ${thread.pendingOn}` : "";
202
+ const latest = thread.latestDate ? ` | ${thread.latestDate}` : "";
203
+ return `${index + 1}. ${thread.id} | ${subject} | messages: ${count}${pending}${latest}`;
204
+ }),
205
+ ].join("\n") + "\n";
206
+ }
207
+ function printToolCallResult(result, wantJson) {
208
+ if (wantJson) {
209
+ process.stdout.write(json(result));
210
+ return;
211
+ }
212
+ if (typeof result.structuredContent === "object" && result.structuredContent) {
213
+ process.stdout.write(`${JSON.stringify(result.structuredContent, null, 2)}\n`);
214
+ return;
215
+ }
216
+ const content = Array.isArray(result.content) ? result.content : [];
217
+ if (content.length === 0) {
218
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
219
+ return;
220
+ }
221
+ const rendered = content
222
+ .map((entry) => {
223
+ if (!entry || typeof entry !== "object") {
224
+ return String(entry);
225
+ }
226
+ if ("type" in entry && entry.type === "text" && "text" in entry) {
227
+ return String(entry.text);
228
+ }
229
+ if ("type" in entry && entry.type === "resource" && "resource" in entry) {
230
+ const resource = entry.resource;
231
+ if (resource && typeof resource === "object") {
232
+ if ("text" in resource) {
233
+ return String(resource.text);
234
+ }
235
+ if ("blob" in resource) {
236
+ return `[resource blob] ${String(resource.uri ?? "")}`;
237
+ }
238
+ }
239
+ }
240
+ if ("type" in entry && entry.type === "resource_link") {
241
+ return `${String(entry.title || entry.name || entry.uri || "resource")} -> ${String(entry.uri || "")}`;
242
+ }
243
+ return JSON.stringify(entry, null, 2);
244
+ })
245
+ .filter(Boolean)
246
+ .join("\n\n");
247
+ process.stdout.write(`${rendered}\n`);
248
+ }
249
+ async function withMcpClient(run) {
250
+ const transport = new StdioClientTransport({
251
+ command: process.execPath,
252
+ args: [cliEntryPath()],
253
+ env: Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string")),
254
+ stderr: "ignore",
255
+ });
256
+ const client = new Client({
257
+ name: "proton-mail-bridge-cli",
258
+ version: "1.6.0",
259
+ }, {
260
+ capabilities: {},
261
+ });
262
+ try {
263
+ await client.connect(transport);
264
+ return await run(client);
265
+ }
266
+ finally {
267
+ await Promise.allSettled([client.close(), transport.close()]);
268
+ }
269
+ }
270
+ async function parseToolArgs(parsed) {
271
+ const inline = getStringFlag(parsed.flags, "args");
272
+ if (inline) {
273
+ const parsedInline = JSON.parse(inline);
274
+ if (!parsedInline || typeof parsedInline !== "object" || Array.isArray(parsedInline)) {
275
+ throw new Error("--args must be a JSON object.");
276
+ }
277
+ return parsedInline;
278
+ }
279
+ const file = getStringFlag(parsed.flags, "args-file");
280
+ if (file) {
281
+ const raw = await readFile(file, "utf8");
282
+ const parsedFile = JSON.parse(raw);
283
+ if (!parsedFile || typeof parsedFile !== "object" || Array.isArray(parsedFile)) {
284
+ throw new Error("--args-file must contain a JSON object.");
285
+ }
286
+ return parsedFile;
287
+ }
288
+ return undefined;
289
+ }
290
+ function summarizeDigest(value) {
291
+ const counts = (value.counts && typeof value.counts === "object") ? value.counts : {};
292
+ const topThreads = Array.isArray(value.topThreads) ? value.topThreads : [];
293
+ const stale = Array.isArray(value.staleAwaitingYou) ? value.staleAwaitingYou : [];
294
+ return [
295
+ "Inbox digest",
296
+ `Total threads: ${counts.totalThreads ?? 0}`,
297
+ `Unread threads: ${counts.unreadThreads ?? 0}`,
298
+ `Pending on you: ${counts.pendingOnYou ?? 0}`,
299
+ `Pending on them: ${counts.pendingOnThem ?? 0}`,
300
+ `Stale awaiting you: ${counts.staleAwaitingYou ?? 0}`,
301
+ "",
302
+ "Top threads:",
303
+ ...(topThreads.length > 0
304
+ ? topThreads.map((thread, index) => `${index + 1}. ${thread.subject || "(no subject)"} | ${thread.latestDate || ""}`)
305
+ : ["None"]),
306
+ "",
307
+ "Stale awaiting you:",
308
+ ...(stale.length > 0
309
+ ? stale.map((thread, index) => `${index + 1}. ${thread.subject || "(no subject)"} | ${thread.latestDate || ""}`)
310
+ : ["None"]),
311
+ ].join("\n") + "\n";
312
+ }
313
+ async function withServices(run) {
314
+ const config = buildConfigFromEnv();
315
+ const services = createServer(config, { startBackgroundSync: false });
316
+ try {
317
+ return await run({ config, ...services });
318
+ }
319
+ finally {
320
+ services.backgroundSyncService.stop();
321
+ await Promise.allSettled([services.imapService.disconnect(), services.smtpService.close()]);
322
+ }
323
+ }
324
+ async function syncIndex(context, input) {
325
+ const snapshot = await context.imapService.collectEmailsForIndex({
326
+ ...input,
327
+ checkpoints: await context.localIndexService.getSyncCheckpointMap(),
328
+ });
329
+ const index = await context.localIndexService.recordSnapshot({
330
+ folders: snapshot.folders,
331
+ emails: snapshot.emails,
332
+ syncedAt: snapshot.syncedAt,
333
+ folderStats: snapshot.folderStats,
334
+ });
335
+ return {
336
+ syncedAt: snapshot.syncedAt,
337
+ full: Boolean(input.full),
338
+ folders: snapshot.folderStats,
339
+ cachedMessages: snapshot.emails.length,
340
+ index,
341
+ };
342
+ }
343
+ function renderStatus(status) {
344
+ const lines = [
345
+ "Proton Mail Bridge status",
346
+ `Account: ${status.account || "unknown"}`,
347
+ `IMAP: ${status.imapHost}:${status.imapPort}`,
348
+ `SMTP: ${status.smtpHost}:${status.smtpPort}`,
349
+ ];
350
+ const index = status.index;
351
+ if (index) {
352
+ lines.push(`Index path: ${index.path || "unknown"}`);
353
+ lines.push(`Indexed messages: ${index.dedupedMessageCount || 0}`);
354
+ lines.push(`Index updated: ${index.updatedAt || "never"}`);
355
+ }
356
+ const claude = status.claudeDesktop;
357
+ if (claude) {
358
+ lines.push(`Claude Desktop installed: ${claude.installed ? "yes" : "no"}`);
359
+ if (claude.runtimeDir) {
360
+ lines.push(`Claude runtime: ${String(claude.runtimeDir)}`);
361
+ }
362
+ }
363
+ return `${lines.join("\n")}\n`;
364
+ }
365
+ function buildSearchFilters(parsed) {
366
+ return {
367
+ query: parsed.positionals.join(" ") || undefined,
368
+ folder: getStringFlag(parsed.flags, "folder"),
369
+ label: getStringFlag(parsed.flags, "label"),
370
+ from: getStringFlag(parsed.flags, "from"),
371
+ to: getStringFlag(parsed.flags, "to"),
372
+ subject: getStringFlag(parsed.flags, "subject"),
373
+ senderDomain: getStringFlag(parsed.flags, "domain"),
374
+ limit: getNumberFlag(parsed.flags, "limit", 25),
375
+ isRead: isTruthyFlag(parsed.flags.read) ? true : isTruthyFlag(parsed.flags.unread) ? false : undefined,
376
+ isStarred: isTruthyFlag(parsed.flags.starred) ? true : isTruthyFlag(parsed.flags.unstarred) ? false : undefined,
377
+ };
378
+ }
379
+ async function runStatus(parsed) {
380
+ const wantJson = isTruthyFlag(parsed.flags.json);
381
+ await withServices(async ({ config, localIndexService, backgroundSyncService }) => {
382
+ const [index, claudeDesktop] = await Promise.all([
383
+ localIndexService.getStatus(),
384
+ getClaudeDesktopInstallStatus(),
385
+ ]);
386
+ const result = {
387
+ account: config.smtp.username,
388
+ imapHost: config.imap.host,
389
+ imapPort: config.imap.port,
390
+ smtpHost: config.smtp.host,
391
+ smtpPort: config.smtp.port,
392
+ runtime: sanitizeRuntimeConfig(config.runtime),
393
+ index,
394
+ backgroundSync: backgroundSyncService.getStatus(),
395
+ claudeDesktop,
396
+ };
397
+ process.stdout.write(wantJson ? json(result) : renderStatus(result));
398
+ });
399
+ }
400
+ async function runDoctor(parsed) {
401
+ const wantJson = isTruthyFlag(parsed.flags.json);
402
+ await withServices(async ({ config, smtpService, imapService, localIndexService }) => {
403
+ const [claudeDesktop, index] = await Promise.all([
404
+ getClaudeDesktopInstallStatus(),
405
+ localIndexService.getStatus(),
406
+ ]);
407
+ let imapOk = false;
408
+ let smtpOk = false;
409
+ let folderCount = 0;
410
+ let error;
411
+ try {
412
+ await imapService.ping();
413
+ imapOk = true;
414
+ folderCount = (await imapService.getFolders(true)).length;
415
+ await smtpService.verifyConnection();
416
+ smtpOk = true;
417
+ }
418
+ catch (err) {
419
+ error = err instanceof Error ? err.message : String(err);
420
+ }
421
+ const result = {
422
+ ok: imapOk && smtpOk,
423
+ account: config.smtp.username,
424
+ imapOk,
425
+ smtpOk,
426
+ folderCount,
427
+ indexUpdatedAt: index.updatedAt,
428
+ claudeDesktopInstalled: claudeDesktop.installed,
429
+ error,
430
+ };
431
+ process.stdout.write(wantJson ? json(result) : `${result.ok ? "Doctor OK" : "Doctor failed"}\n${json(result)}`);
432
+ });
433
+ }
434
+ async function runConnectionStatus(parsed) {
435
+ const wantJson = isTruthyFlag(parsed.flags.json);
436
+ await withServices(async ({ config, smtpService, imapService }) => {
437
+ let imapOk = false;
438
+ let smtpOk = false;
439
+ let error;
440
+ try {
441
+ await imapService.ping();
442
+ imapOk = true;
443
+ await smtpService.verifyConnection();
444
+ smtpOk = true;
445
+ }
446
+ catch (err) {
447
+ error = err instanceof Error ? err.message : String(err);
448
+ }
449
+ const result = {
450
+ account: config.smtp.username,
451
+ imap: {
452
+ host: config.imap.host,
453
+ port: config.imap.port,
454
+ ok: imapOk,
455
+ },
456
+ smtp: {
457
+ host: config.smtp.host,
458
+ port: config.smtp.port,
459
+ ok: smtpOk,
460
+ },
461
+ idle: imapService.getIdleStatus(),
462
+ error,
463
+ };
464
+ process.stdout.write(wantJson ? json(result) : `${JSON.stringify(result, null, 2)}\n`);
465
+ });
466
+ }
467
+ async function runRuntimeStatus(parsed) {
468
+ const wantJson = isTruthyFlag(parsed.flags.json);
469
+ await withServices(async ({ config, backgroundSyncService, draftStore, localIndexService }) => {
470
+ const result = {
471
+ account: config.smtp.username,
472
+ runtime: sanitizeRuntimeConfig(config.runtime),
473
+ backgroundSync: backgroundSyncService.getStatus(),
474
+ localIndex: await localIndexService.getStatus(),
475
+ drafts: {
476
+ total: (await draftStore.listDrafts(true)).length,
477
+ active: (await draftStore.listDrafts(false)).length,
478
+ },
479
+ };
480
+ process.stdout.write(wantJson ? json(result) : `${JSON.stringify(result, null, 2)}\n`);
481
+ });
482
+ }
483
+ async function runSync(parsed) {
484
+ const wantJson = isTruthyFlag(parsed.flags.json);
485
+ await withServices(async (context) => {
486
+ const result = await syncIndex(context, {
487
+ folder: getStringFlag(parsed.flags, "folder"),
488
+ full: isTruthyFlag(parsed.flags.full),
489
+ limitPerFolder: getNumberFlag(parsed.flags, "limit", 100),
490
+ includeAttachmentText: !isTruthyFlag(parsed.flags["no-attachment-text"]),
491
+ });
492
+ process.stdout.write(wantJson ? json(result) : `${JSON.stringify(result, null, 2)}\n`);
493
+ });
494
+ }
495
+ async function runIndexStatus(parsed) {
496
+ const wantJson = isTruthyFlag(parsed.flags.json);
497
+ await withServices(async ({ localIndexService }) => {
498
+ const result = await localIndexService.getStatus();
499
+ if (wantJson) {
500
+ process.stdout.write(json(result));
501
+ return;
502
+ }
503
+ process.stdout.write([
504
+ "Index status",
505
+ `Path: ${result.path}`,
506
+ `Updated: ${result.updatedAt || "never"}`,
507
+ `Messages: ${result.dedupedMessageCount}`,
508
+ `Threads: ${result.threadCount}`,
509
+ `Labels: ${result.labelCount}`,
510
+ `Stale: ${result.isStale ? "yes" : "no"}`,
511
+ ].join("\n") + "\n");
512
+ });
513
+ }
514
+ async function runFolders(parsed) {
515
+ const wantJson = isTruthyFlag(parsed.flags.json);
516
+ await withServices(async ({ imapService }) => {
517
+ const folders = await imapService.getFolders(true);
518
+ process.stdout.write(wantJson ? json(folders) : `${JSON.stringify(folders, null, 2)}\n`);
519
+ });
520
+ }
521
+ async function runLabels(parsed) {
522
+ const wantJson = isTruthyFlag(parsed.flags.json);
523
+ await withServices(async ({ localIndexService }) => {
524
+ const result = await localIndexService.getLabels(getNumberFlag(parsed.flags, "limit", 100));
525
+ if (wantJson) {
526
+ process.stdout.write(json(result));
527
+ return;
528
+ }
529
+ process.stdout.write(result.length === 0
530
+ ? "No labels.\n"
531
+ : result.map((label, index) => `${index + 1}. ${label.name} | ${label.type} | messages: ${label.messageCount}`).join("\n") + "\n");
532
+ });
533
+ }
534
+ async function runThreads(parsed) {
535
+ const wantJson = isTruthyFlag(parsed.flags.json);
536
+ const syncBefore = isTruthyFlag(parsed.flags.sync);
537
+ await withServices(async (context) => {
538
+ if (syncBefore) {
539
+ await syncIndex(context, {
540
+ folder: getStringFlag(parsed.flags, "folder"),
541
+ limitPerFolder: Math.max(getNumberFlag(parsed.flags, "limit", 25), 100),
542
+ includeAttachmentText: true,
543
+ });
544
+ }
545
+ const result = await context.localIndexService.getThreads({
546
+ query: parsed.positionals.join(" ") || undefined,
547
+ label: getStringFlag(parsed.flags, "label"),
548
+ limit: getNumberFlag(parsed.flags, "limit", 25),
549
+ });
550
+ process.stdout.write(wantJson ? json(result) : summarizeThreadList(result));
551
+ });
552
+ }
553
+ async function runDigest(parsed) {
554
+ const wantJson = isTruthyFlag(parsed.flags.json);
555
+ const syncBefore = isTruthyFlag(parsed.flags.sync);
556
+ await withServices(async (context) => {
557
+ if (syncBefore) {
558
+ await syncIndex(context, { folder: "INBOX", limitPerFolder: 100, includeAttachmentText: true });
559
+ }
560
+ const result = await context.localIndexService.getInboxDigest({
561
+ limit: getNumberFlag(parsed.flags, "limit", 10),
562
+ minAgeHours: getNumberFlag(parsed.flags, "age-hours", 24),
563
+ });
564
+ process.stdout.write(wantJson ? json(result) : summarizeDigest(result));
565
+ });
566
+ }
567
+ async function runFollowups(parsed) {
568
+ const wantJson = isTruthyFlag(parsed.flags.json);
569
+ const syncBefore = isTruthyFlag(parsed.flags.sync);
570
+ await withServices(async (context) => {
571
+ if (syncBefore) {
572
+ await syncIndex(context, { folder: "INBOX", limitPerFolder: 100, includeAttachmentText: true });
573
+ }
574
+ const pendingOnRaw = getStringFlag(parsed.flags, "pending");
575
+ const pendingOn = pendingOnRaw === "you" || pendingOnRaw === "them" || pendingOnRaw === "any"
576
+ ? pendingOnRaw
577
+ : "you";
578
+ const result = await context.localIndexService.getFollowUpCandidates({
579
+ limit: getNumberFlag(parsed.flags, "limit", 25),
580
+ minAgeHours: getNumberFlag(parsed.flags, "age-hours", 24),
581
+ pendingOn,
582
+ });
583
+ process.stdout.write(wantJson ? json(result) : summarizeThreadList(result));
584
+ });
585
+ }
586
+ async function runDrafts(parsed) {
587
+ const wantJson = isTruthyFlag(parsed.flags.json);
588
+ await withServices(async ({ draftStore }) => {
589
+ const result = await draftStore.listDrafts(isTruthyFlag(parsed.flags.sent));
590
+ if (wantJson) {
591
+ process.stdout.write(json(result));
592
+ return;
593
+ }
594
+ process.stdout.write(result.length === 0
595
+ ? "No drafts.\n"
596
+ : result.map((draft, index) => `${index + 1}. ${draft.id} | ${draft.mode} | ${draft.subject}`).join("\n") + "\n");
597
+ });
598
+ }
599
+ async function runAttachments(parsed) {
600
+ const emailId = parsed.positionals[0];
601
+ if (!emailId) {
602
+ throw new Error("attachments requires an emailId, for example: proton-mail-bridge attachments INBOX::123");
603
+ }
604
+ const wantJson = isTruthyFlag(parsed.flags.json);
605
+ await withServices(async ({ imapService }) => {
606
+ const result = await imapService.listAttachments(emailId);
607
+ if (wantJson) {
608
+ process.stdout.write(json(result));
609
+ return;
610
+ }
611
+ process.stdout.write(result.attachments.length === 0
612
+ ? "No attachments.\n"
613
+ : result.attachments.map((attachment, index) => `${index + 1}. ${attachment.filename || attachment.id || "(unnamed)"} | ${attachment.contentType || "unknown"} | ${attachment.kind || "other"}`).join("\n") + "\n");
614
+ });
615
+ }
616
+ async function runSearch(parsed) {
617
+ const wantJson = isTruthyFlag(parsed.flags.json);
618
+ const live = isTruthyFlag(parsed.flags.live);
619
+ const syncBefore = isTruthyFlag(parsed.flags.sync);
620
+ const filters = buildSearchFilters(parsed);
621
+ await withServices(async (context) => {
622
+ if (!live) {
623
+ const status = await context.localIndexService.getStatus();
624
+ if (syncBefore || !status.updatedAt) {
625
+ await syncIndex(context, {
626
+ folder: filters.folder,
627
+ limitPerFolder: Math.max(filters.limit ?? 25, 100),
628
+ includeAttachmentText: true,
629
+ });
630
+ }
631
+ const result = await context.localIndexService.search(filters);
632
+ process.stdout.write(wantJson ? json(result) : tableEmails(result.emails));
633
+ return;
634
+ }
635
+ const result = await context.imapService.searchEmails(filters);
636
+ process.stdout.write(wantJson ? json(result) : tableEmails(result.emails));
637
+ });
638
+ }
639
+ async function runRead(parsed) {
640
+ const emailId = parsed.positionals[0];
641
+ if (!emailId) {
642
+ throw new Error("read requires an emailId, for example: proton-mail-bridge read INBOX::123");
643
+ }
644
+ const wantJson = isTruthyFlag(parsed.flags.json);
645
+ await withServices(async ({ imapService }) => {
646
+ const detail = await imapService.getEmailById(emailId);
647
+ if (wantJson) {
648
+ process.stdout.write(json(detail));
649
+ return;
650
+ }
651
+ const lines = [
652
+ `ID: ${detail.id}`,
653
+ `Subject: ${detail.subject}`,
654
+ `From: ${detail.from.map((entry) => entry.address || entry.name || "").filter(Boolean).join(", ")}`,
655
+ `Date: ${detail.date || detail.internalDate || ""}`,
656
+ "",
657
+ detail.text || detail.preview || "(no text body available)",
658
+ ];
659
+ process.stdout.write(`${lines.join("\n")}\n`);
660
+ });
661
+ }
662
+ // ── reply/forward helpers (mirrors logic in index.ts) ──────────────────────
663
+ function uniqueAddresses(addresses) {
664
+ const seen = new Set();
665
+ const result = [];
666
+ for (const address of addresses) {
667
+ const normalized = lowerCaseAddress(address);
668
+ if (!normalized || seen.has(normalized))
669
+ continue;
670
+ seen.add(normalized);
671
+ result.push(address.trim());
672
+ }
673
+ return result;
674
+ }
675
+ function addressValues(addresses) {
676
+ return uniqueAddresses(addresses.map((a) => a.address?.trim()).filter((a) => Boolean(a)));
677
+ }
678
+ function prefixedSubject(subject, prefix) {
679
+ const trimmed = subject.trim();
680
+ return trimmed.toLowerCase().startsWith(prefix.toLowerCase()) ? trimmed : `${prefix} ${trimmed}`;
681
+ }
682
+ function formatAddressList(addresses) {
683
+ return addresses
684
+ .map((a) => (a.name && a.address ? `${a.name} <${a.address}>` : a.address || a.name || ""))
685
+ .filter(Boolean)
686
+ .join(", ");
687
+ }
688
+ function buildReplyText(detail, body) {
689
+ const originalText = detail.text || detail.preview || "";
690
+ const fromText = formatAddressList(detail.from);
691
+ const dateText = detail.date || detail.internalDate || "an unknown date";
692
+ return [
693
+ body.trim(),
694
+ "",
695
+ `On ${dateText}, ${fromText || "the sender"} wrote:`,
696
+ originalText.split(/\r?\n/).map((line) => `> ${line}`).join("\n"),
697
+ ].join("\n");
698
+ }
699
+ function buildForwardText(detail, body) {
700
+ const originalText = detail.text || detail.preview || "";
701
+ return [
702
+ body?.trim() || "",
703
+ "",
704
+ "---------- Forwarded message ---------",
705
+ `From: ${formatAddressList(detail.from)}`,
706
+ `Date: ${detail.date || detail.internalDate || ""}`,
707
+ `Subject: ${detail.subject}`,
708
+ `To: ${formatAddressList(detail.to)}`,
709
+ detail.cc.length > 0 ? `Cc: ${formatAddressList(detail.cc)}` : "",
710
+ "",
711
+ originalText,
712
+ ]
713
+ .filter((line, index, array) => line !== "" || (index > 0 && array[index - 1] !== ""))
714
+ .join("\n");
715
+ }
716
+ function getReplyRecipients(detail, ownerEmail, replyAll) {
717
+ const owner = lowerCaseAddress(ownerEmail);
718
+ const primary = addressValues(detail.replyTo).length > 0 ? detail.replyTo : detail.from;
719
+ const to = uniqueAddresses(addressValues(primary).filter((address) => lowerCaseAddress(address) !== owner));
720
+ if (!replyAll)
721
+ return { to, cc: [] };
722
+ const cc = uniqueAddresses([...addressValues(detail.to), ...addressValues(detail.cc)]).filter((address) => {
723
+ const normalized = lowerCaseAddress(address);
724
+ return normalized !== owner && !to.some((r) => lowerCaseAddress(r) === normalized);
725
+ });
726
+ return { to, cc };
727
+ }
728
+ // ── write commands ──────────────────────────────────────────────────────────
729
+ async function runMove(parsed) {
730
+ const emailId = parsed.positionals[0];
731
+ const targetFolder = parsed.positionals[1] || getStringFlag(parsed.flags, "folder");
732
+ if (!emailId)
733
+ throw new Error("move requires an emailId");
734
+ if (!targetFolder)
735
+ throw new Error("move requires a target folder as a second argument or --folder");
736
+ const wantJson = isTruthyFlag(parsed.flags.json);
737
+ await withServices(async ({ config, imapService }) => {
738
+ ensureMailboxWriteAllowed(config.runtime);
739
+ const result = await imapService.moveEmail(emailId, targetFolder);
740
+ process.stdout.write(wantJson ? json(result) : `Moved ${emailId} → ${result.targetFolder}\n`);
741
+ });
742
+ }
743
+ async function runArchive(parsed) {
744
+ const emailId = parsed.positionals[0];
745
+ if (!emailId)
746
+ throw new Error("archive requires an emailId");
747
+ const wantJson = isTruthyFlag(parsed.flags.json);
748
+ await withServices(async ({ config, imapService }) => {
749
+ ensureMailboxWriteAllowed(config.runtime);
750
+ const result = await imapService.archiveEmail(emailId);
751
+ process.stdout.write(wantJson ? json(result) : `Archived ${emailId} → ${result.targetFolder}\n`);
752
+ });
753
+ }
754
+ async function runTrash(parsed) {
755
+ const emailId = parsed.positionals[0];
756
+ if (!emailId)
757
+ throw new Error("trash requires an emailId");
758
+ const wantJson = isTruthyFlag(parsed.flags.json);
759
+ await withServices(async ({ config, imapService }) => {
760
+ ensureMailboxWriteAllowed(config.runtime);
761
+ const result = await imapService.trashEmail(emailId);
762
+ process.stdout.write(wantJson ? json(result) : `Trashed ${emailId} → ${result.targetFolder}\n`);
763
+ });
764
+ }
765
+ async function runRestore(parsed) {
766
+ const emailId = parsed.positionals[0];
767
+ if (!emailId)
768
+ throw new Error("restore requires an emailId");
769
+ const wantJson = isTruthyFlag(parsed.flags.json);
770
+ await withServices(async ({ config, imapService }) => {
771
+ ensureMailboxWriteAllowed(config.runtime);
772
+ const result = await imapService.restoreEmail(emailId, getStringFlag(parsed.flags, "folder"));
773
+ process.stdout.write(wantJson ? json(result) : `Restored ${emailId} → ${result.targetFolder}\n`);
774
+ });
775
+ }
776
+ async function runMarkRead(parsed) {
777
+ const emailId = parsed.positionals[0];
778
+ if (!emailId)
779
+ throw new Error("mark-read requires an emailId");
780
+ const isRead = !isTruthyFlag(parsed.flags.unread);
781
+ const wantJson = isTruthyFlag(parsed.flags.json);
782
+ await withServices(async ({ config, imapService }) => {
783
+ ensureMailboxWriteAllowed(config.runtime);
784
+ const result = await imapService.markEmailRead(emailId, isRead);
785
+ process.stdout.write(wantJson ? json(result) : `Marked ${emailId} as ${result.isRead ? "read" : "unread"}\n`);
786
+ });
787
+ }
788
+ async function runStar(parsed) {
789
+ const emailId = parsed.positionals[0];
790
+ if (!emailId)
791
+ throw new Error("star requires an emailId");
792
+ const isStarred = !isTruthyFlag(parsed.flags.unstar);
793
+ const wantJson = isTruthyFlag(parsed.flags.json);
794
+ await withServices(async ({ config, imapService }) => {
795
+ ensureMailboxWriteAllowed(config.runtime);
796
+ const result = await imapService.starEmail(emailId, isStarred);
797
+ process.stdout.write(wantJson ? json(result) : `${result.isStarred ? "Starred" : "Unstarred"} ${emailId}\n`);
798
+ });
799
+ }
800
+ async function runDelete(parsed) {
801
+ const emailId = parsed.positionals[0];
802
+ if (!emailId)
803
+ throw new Error("delete requires an emailId");
804
+ const wantJson = isTruthyFlag(parsed.flags.json);
805
+ await withServices(async ({ config, imapService }) => {
806
+ ensureMailboxWriteAllowed(config.runtime);
807
+ const result = await imapService.deleteEmail(emailId);
808
+ process.stdout.write(wantJson ? json(result) : `Deleted ${emailId}\n`);
809
+ });
810
+ }
811
+ async function runSend(parsed) {
812
+ const to = parseEmails(getStringFlag(parsed.flags, "to") || "");
813
+ const cc = parseEmails(getStringFlag(parsed.flags, "cc") || "");
814
+ const bcc = parseEmails(getStringFlag(parsed.flags, "bcc") || "");
815
+ const subject = getStringFlag(parsed.flags, "subject");
816
+ if (to.length === 0)
817
+ throw new Error("send requires --to");
818
+ if (!subject)
819
+ throw new Error("send requires --subject");
820
+ let body = getStringFlag(parsed.flags, "body");
821
+ if (!body) {
822
+ const chunks = [];
823
+ for await (const chunk of process.stdin)
824
+ chunks.push(Buffer.from(chunk));
825
+ body = Buffer.concat(chunks).toString("utf8").trim();
826
+ }
827
+ if (!body)
828
+ throw new Error("send requires --body or body piped via stdin");
829
+ const wantJson = isTruthyFlag(parsed.flags.json);
830
+ await withServices(async ({ config, smtpService }) => {
831
+ ensureSendAllowed(config.runtime);
832
+ ensureValidEmails(to, "to");
833
+ ensureValidEmails(cc, "cc");
834
+ ensureValidEmails(bcc, "bcc");
835
+ const result = await smtpService.sendEmail({ to, cc, bcc, subject, body: body, isHtml: isTruthyFlag(parsed.flags.html) });
836
+ process.stdout.write(wantJson ? json(result) : `Sent. messageId=${result.messageId ?? "unknown"} accepted=${result.accepted.join(",")}\n`);
837
+ });
838
+ }
839
+ async function runReply(parsed) {
840
+ const emailId = parsed.positionals[0];
841
+ if (!emailId)
842
+ throw new Error("reply requires an emailId");
843
+ const replyAll = isTruthyFlag(parsed.flags["reply-all"]) || isTruthyFlag(parsed.flags.all);
844
+ let body = getStringFlag(parsed.flags, "body");
845
+ if (!body) {
846
+ const chunks = [];
847
+ for await (const chunk of process.stdin)
848
+ chunks.push(Buffer.from(chunk));
849
+ body = Buffer.concat(chunks).toString("utf8").trim();
850
+ }
851
+ if (!body)
852
+ throw new Error("reply requires --body or body piped via stdin");
853
+ const wantJson = isTruthyFlag(parsed.flags.json);
854
+ await withServices(async ({ config, smtpService, imapService }) => {
855
+ ensureSendAllowed(config.runtime);
856
+ const detail = await imapService.getEmailById(emailId);
857
+ const recipients = getReplyRecipients(detail, config.smtp.username, replyAll);
858
+ if (recipients.to.length === 0)
859
+ throw new Error("Unable to infer reply recipient.");
860
+ const result = await smtpService.sendEmail({
861
+ to: recipients.to,
862
+ cc: recipients.cc,
863
+ subject: prefixedSubject(detail.subject, "Re:"),
864
+ body: buildReplyText(detail, body),
865
+ inReplyTo: detail.messageId,
866
+ references: detail.messageId ? [detail.messageId] : undefined,
867
+ });
868
+ process.stdout.write(wantJson ? json({ repliedTo: emailId, to: recipients.to, messageId: result.messageId }) : `Reply sent to ${recipients.to.join(", ")}\n`);
869
+ });
870
+ }
871
+ async function runForward(parsed) {
872
+ const emailId = parsed.positionals[0];
873
+ if (!emailId)
874
+ throw new Error("forward requires an emailId");
875
+ const to = parseEmails(getStringFlag(parsed.flags, "to") || "");
876
+ if (to.length === 0)
877
+ throw new Error("forward requires --to");
878
+ let body = getStringFlag(parsed.flags, "body");
879
+ if (!body) {
880
+ const stdinChunks = [];
881
+ for await (const chunk of process.stdin)
882
+ stdinChunks.push(Buffer.from(chunk));
883
+ const stdinText = Buffer.concat(stdinChunks).toString("utf8").trim();
884
+ if (stdinText)
885
+ body = stdinText;
886
+ }
887
+ const wantJson = isTruthyFlag(parsed.flags.json);
888
+ await withServices(async ({ config, smtpService, imapService }) => {
889
+ ensureSendAllowed(config.runtime);
890
+ ensureValidEmails(to, "to");
891
+ const detail = await imapService.getEmailById(emailId);
892
+ const result = await smtpService.sendEmail({
893
+ to,
894
+ subject: prefixedSubject(detail.subject, "Fwd:"),
895
+ body: buildForwardText(detail, body),
896
+ });
897
+ process.stdout.write(wantJson ? json({ forwardedMessage: emailId, to, messageId: result.messageId }) : `Forwarded to ${to.join(", ")}\n`);
898
+ });
899
+ }
900
+ async function runCreateFolder(parsed) {
901
+ const path = parsed.positionals[0] || getStringFlag(parsed.flags, "path");
902
+ if (!path)
903
+ throw new Error("create-folder requires a path argument");
904
+ const wantJson = isTruthyFlag(parsed.flags.json);
905
+ await withServices(async ({ config, imapService }) => {
906
+ ensureMailboxWriteAllowed(config.runtime);
907
+ const result = await imapService.createFolder(path);
908
+ process.stdout.write(wantJson ? json(result) : `${result.created ? "Created" : "Already existed"}: ${result.path}\n`);
909
+ });
910
+ }
911
+ async function runRenameFolder(parsed) {
912
+ const path = parsed.positionals[0] || getStringFlag(parsed.flags, "path");
913
+ const newPath = parsed.positionals[1] || getStringFlag(parsed.flags, "to") || getStringFlag(parsed.flags, "new-path");
914
+ if (!path)
915
+ throw new Error("rename-folder requires a source path argument");
916
+ if (!newPath)
917
+ throw new Error("rename-folder requires a target path as second argument or --to");
918
+ const wantJson = isTruthyFlag(parsed.flags.json);
919
+ await withServices(async ({ config, imapService }) => {
920
+ ensureMailboxWriteAllowed(config.runtime);
921
+ const result = await imapService.renameFolder(path, newPath);
922
+ process.stdout.write(wantJson ? json(result) : `Renamed: ${result.path} → ${result.newPath}\n`);
923
+ });
924
+ }
925
+ async function runDeleteFolder(parsed) {
926
+ const path = parsed.positionals[0] || getStringFlag(parsed.flags, "path");
927
+ if (!path)
928
+ throw new Error("delete-folder requires a path argument");
929
+ const wantJson = isTruthyFlag(parsed.flags.json);
930
+ await withServices(async ({ config, imapService }) => {
931
+ ensureMailboxWriteAllowed(config.runtime);
932
+ const result = await imapService.deleteFolder(path);
933
+ process.stdout.write(wantJson ? json(result) : `Deleted folder: ${result.path}\n`);
934
+ });
935
+ }
936
+ // draft commands go through withMcpClient to reuse policy/remote-sync logic in index.ts
937
+ async function runEmails(parsed) {
938
+ const wantJson = isTruthyFlag(parsed.flags.json);
939
+ await withMcpClient(async (client) => {
940
+ const result = await client.callTool({
941
+ name: "get_emails",
942
+ arguments: {
943
+ folder: getStringFlag(parsed.flags, "folder") || "INBOX",
944
+ limit: getNumberFlag(parsed.flags, "limit", 50),
945
+ offset: getNumberFlag(parsed.flags, "offset", 0),
946
+ },
947
+ });
948
+ printToolCallResult(result, wantJson);
949
+ });
950
+ }
951
+ async function runThread(parsed) {
952
+ const threadId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
953
+ if (!threadId)
954
+ throw new Error("thread requires a threadId");
955
+ const wantJson = isTruthyFlag(parsed.flags.json);
956
+ await withMcpClient(async (client) => {
957
+ const result = await client.callTool({ name: "get_thread_by_id", arguments: { threadId } });
958
+ printToolCallResult(result, wantJson);
959
+ });
960
+ }
961
+ async function runThreadBrief(parsed) {
962
+ const threadId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
963
+ if (!threadId)
964
+ throw new Error("thread-brief requires a threadId");
965
+ const wantJson = isTruthyFlag(parsed.flags.json);
966
+ await withMcpClient(async (client) => {
967
+ const result = await client.callTool({ name: "get_thread_brief", arguments: { threadId } });
968
+ printToolCallResult(result, wantJson);
969
+ });
970
+ }
971
+ async function runActionable(parsed) {
972
+ const wantJson = isTruthyFlag(parsed.flags.json);
973
+ await withMcpClient(async (client) => {
974
+ const result = await client.callTool({
975
+ name: "get_actionable_threads",
976
+ arguments: { limit: getNumberFlag(parsed.flags, "limit", 25) },
977
+ });
978
+ printToolCallResult(result, wantJson);
979
+ });
980
+ }
981
+ async function runDocumentThreads(parsed) {
982
+ const wantJson = isTruthyFlag(parsed.flags.json);
983
+ await withMcpClient(async (client) => {
984
+ const result = await client.callTool({
985
+ name: "find_document_threads",
986
+ arguments: {
987
+ category: getStringFlag(parsed.flags, "category"),
988
+ query: parsed.positionals.join(" ") || undefined,
989
+ limit: getNumberFlag(parsed.flags, "limit", 25),
990
+ sync: isTruthyFlag(parsed.flags.sync) || undefined,
991
+ },
992
+ });
993
+ printToolCallResult(result, wantJson);
994
+ });
995
+ }
996
+ async function runMeetingContext(parsed) {
997
+ const person = parsed.positionals[0] || getStringFlag(parsed.flags, "person");
998
+ const domain = getStringFlag(parsed.flags, "domain");
999
+ if (!person && !domain)
1000
+ throw new Error("meeting-context requires a person argument or --domain");
1001
+ const wantJson = isTruthyFlag(parsed.flags.json);
1002
+ await withMcpClient(async (client) => {
1003
+ const result = await client.callTool({
1004
+ name: "prepare_meeting_context",
1005
+ arguments: {
1006
+ person,
1007
+ domain,
1008
+ limit: getNumberFlag(parsed.flags, "limit", 10),
1009
+ sync: isTruthyFlag(parsed.flags.sync) || undefined,
1010
+ },
1011
+ });
1012
+ printToolCallResult(result, wantJson);
1013
+ });
1014
+ }
1015
+ async function runThreadAction(parsed) {
1016
+ const threadId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1017
+ const action = parsed.positionals[1] || getStringFlag(parsed.flags, "action");
1018
+ if (!threadId)
1019
+ throw new Error("thread-action requires a threadId");
1020
+ if (!action)
1021
+ throw new Error("thread-action requires an action (mark_read|mark_unread|star|unstar|archive|trash|restore)");
1022
+ const wantJson = isTruthyFlag(parsed.flags.json);
1023
+ await withMcpClient(async (client) => {
1024
+ const result = await client.callTool({
1025
+ name: "apply_thread_action",
1026
+ arguments: {
1027
+ threadId,
1028
+ action,
1029
+ targetFolder: getStringFlag(parsed.flags, "folder"),
1030
+ unreadOnly: isTruthyFlag(parsed.flags["unread-only"]) || undefined,
1031
+ dryRun: isTruthyFlag(parsed.flags["dry-run"]) || undefined,
1032
+ },
1033
+ });
1034
+ printToolCallResult(result, wantJson);
1035
+ });
1036
+ }
1037
+ async function runBatch(parsed) {
1038
+ const action = parsed.positionals[0] || getStringFlag(parsed.flags, "action");
1039
+ const emailIds = parsed.positionals.slice(1).join(",") || getStringFlag(parsed.flags, "ids");
1040
+ if (!action)
1041
+ throw new Error("batch requires an action (mark_read|mark_unread|star|unstar|archive|trash|restore)");
1042
+ if (!emailIds)
1043
+ throw new Error("batch requires email ids as positional args or --ids");
1044
+ const wantJson = isTruthyFlag(parsed.flags.json);
1045
+ await withMcpClient(async (client) => {
1046
+ const result = await client.callTool({
1047
+ name: "batch_email_action",
1048
+ arguments: {
1049
+ emailIds,
1050
+ action,
1051
+ targetFolder: getStringFlag(parsed.flags, "folder"),
1052
+ dryRun: isTruthyFlag(parsed.flags["dry-run"]) || undefined,
1053
+ },
1054
+ });
1055
+ printToolCallResult(result, wantJson);
1056
+ });
1057
+ }
1058
+ async function runStats(parsed) {
1059
+ const wantJson = isTruthyFlag(parsed.flags.json);
1060
+ await withMcpClient(async (client) => {
1061
+ const result = await client.callTool({ name: "get_email_stats", arguments: {} });
1062
+ printToolCallResult(result, wantJson);
1063
+ });
1064
+ }
1065
+ async function runAnalytics(parsed) {
1066
+ const wantJson = isTruthyFlag(parsed.flags.json);
1067
+ await withMcpClient(async (client) => {
1068
+ const result = await client.callTool({ name: "get_email_analytics", arguments: {} });
1069
+ printToolCallResult(result, wantJson);
1070
+ });
1071
+ }
1072
+ async function runContacts(parsed) {
1073
+ const wantJson = isTruthyFlag(parsed.flags.json);
1074
+ await withMcpClient(async (client) => {
1075
+ const result = await client.callTool({
1076
+ name: "get_contacts",
1077
+ arguments: { limit: getNumberFlag(parsed.flags, "limit", 100) },
1078
+ });
1079
+ printToolCallResult(result, wantJson);
1080
+ });
1081
+ }
1082
+ async function runVolumeTrends(parsed) {
1083
+ const wantJson = isTruthyFlag(parsed.flags.json);
1084
+ await withMcpClient(async (client) => {
1085
+ const result = await client.callTool({
1086
+ name: "get_volume_trends",
1087
+ arguments: { days: getNumberFlag(parsed.flags, "days", 30) },
1088
+ });
1089
+ printToolCallResult(result, wantJson);
1090
+ });
1091
+ }
1092
+ async function runWatch(parsed) {
1093
+ const wantJson = isTruthyFlag(parsed.flags.json);
1094
+ await withMcpClient(async (client) => {
1095
+ const result = await client.callTool({
1096
+ name: "wait_for_mailbox_changes",
1097
+ arguments: {
1098
+ folder: getStringFlag(parsed.flags, "folder") || "INBOX",
1099
+ timeoutSeconds: getNumberFlag(parsed.flags, "timeout", 15),
1100
+ },
1101
+ });
1102
+ printToolCallResult(result, wantJson);
1103
+ });
1104
+ }
1105
+ async function sendSystemNotification(title, body) {
1106
+ const execAsync = promisify(exec);
1107
+ try {
1108
+ if (process.platform === "darwin") {
1109
+ const t = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1110
+ const b = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1111
+ await execAsync(`osascript -e 'display notification "${b}" with title "${t}"'`);
1112
+ }
1113
+ else if (process.platform === "linux") {
1114
+ const { spawn } = await import("node:child_process");
1115
+ await new Promise((resolve) => {
1116
+ const proc = spawn("notify-send", [title, body], { stdio: "ignore" });
1117
+ proc.on("close", () => { resolve(); });
1118
+ proc.on("error", () => { resolve(); }); // notify-send may not be installed
1119
+ });
1120
+ }
1121
+ // Windows: no built-in toast without extra deps — stdout line is the fallback
1122
+ }
1123
+ catch {
1124
+ // Silent — stdout already carries the event
1125
+ }
1126
+ }
1127
+ async function runNotify(parsed) {
1128
+ const folder = getStringFlag(parsed.flags, "folder") || "INBOX";
1129
+ const timeoutSeconds = getNumberFlag(parsed.flags, "timeout", 30);
1130
+ const config = buildConfigFromEnv();
1131
+ const imapService = new SimpleIMAPService(config);
1132
+ let running = true;
1133
+ let previousCount;
1134
+ const shutdown = () => { running = false; };
1135
+ process.on("SIGINT", shutdown);
1136
+ process.on("SIGTERM", shutdown);
1137
+ process.stderr.write(`[notify] Watching ${folder} for new mail (IMAP IDLE). Ctrl+C to stop.\n`);
1138
+ while (running) {
1139
+ try {
1140
+ const result = await imapService.waitForMailboxChanges({
1141
+ folder,
1142
+ timeoutMs: timeoutSeconds * 1000,
1143
+ });
1144
+ if (!running)
1145
+ break;
1146
+ if (result.changed) {
1147
+ const existsEvent = result.events.find((e) => e["type"] === "exists");
1148
+ if (existsEvent) {
1149
+ const newCount = typeof existsEvent["count"] === "number" ? existsEvent["count"] : undefined;
1150
+ const delta = previousCount !== undefined && newCount !== undefined && newCount > previousCount
1151
+ ? newCount - previousCount
1152
+ : undefined;
1153
+ previousCount = newCount ?? previousCount;
1154
+ const n = delta ?? 1;
1155
+ const body = `${n} new message${n !== 1 ? "s" : ""} in ${folder}`;
1156
+ await sendSystemNotification("Proton Mail", body);
1157
+ process.stdout.write(JSON.stringify({ event: "new_mail", folder, count: n, at: result.checkedAt }) + "\n");
1158
+ }
1159
+ else {
1160
+ // flags / expunge only — track count but don't notify
1161
+ const existsOnAny = result.events.find((e) => typeof e["count"] === "number");
1162
+ if (existsOnAny)
1163
+ previousCount = existsOnAny["count"];
1164
+ }
1165
+ }
1166
+ }
1167
+ catch (error) {
1168
+ if (!running)
1169
+ break;
1170
+ const msg = error instanceof Error ? error.message : String(error);
1171
+ process.stderr.write(`[notify] Watch error: ${msg}\n`);
1172
+ // Simple backoff before retrying
1173
+ await new Promise((resolve) => { setTimeout(resolve, 15_000).unref(); });
1174
+ }
1175
+ }
1176
+ try {
1177
+ await imapService.disconnect();
1178
+ }
1179
+ catch {
1180
+ // ignore
1181
+ }
1182
+ process.stderr.write("[notify] Stopped.\n");
1183
+ }
1184
+ async function runTestEmail(parsed) {
1185
+ const to = parsed.positionals[0] || getStringFlag(parsed.flags, "to");
1186
+ if (!to)
1187
+ throw new Error("test-email requires a recipient address");
1188
+ if (!isValidEmail(to))
1189
+ throw new Error("test-email: invalid email address");
1190
+ const wantJson = isTruthyFlag(parsed.flags.json);
1191
+ await withMcpClient(async (client) => {
1192
+ const result = await client.callTool({
1193
+ name: "send_test_email",
1194
+ arguments: { to, customMessage: getStringFlag(parsed.flags, "message") },
1195
+ });
1196
+ printToolCallResult(result, wantJson);
1197
+ });
1198
+ }
1199
+ async function runDraftCreate(parsed) {
1200
+ const to = getStringFlag(parsed.flags, "to") || "";
1201
+ const subject = getStringFlag(parsed.flags, "subject");
1202
+ if (!subject)
1203
+ throw new Error("draft-create requires --subject");
1204
+ let body = getStringFlag(parsed.flags, "body");
1205
+ if (!body) {
1206
+ const chunks = [];
1207
+ for await (const chunk of process.stdin)
1208
+ chunks.push(Buffer.from(chunk));
1209
+ body = Buffer.concat(chunks).toString("utf8").trim();
1210
+ }
1211
+ if (!body)
1212
+ throw new Error("draft-create requires --body or body piped via stdin");
1213
+ const wantJson = isTruthyFlag(parsed.flags.json);
1214
+ await withMcpClient(async (client) => {
1215
+ const result = await client.callTool({
1216
+ name: "create_draft",
1217
+ arguments: { to, subject, body, cc: getStringFlag(parsed.flags, "cc") || "", bcc: getStringFlag(parsed.flags, "bcc") || "" },
1218
+ });
1219
+ printToolCallResult(result, wantJson);
1220
+ });
1221
+ }
1222
+ async function runDraftRead(parsed) {
1223
+ const draftId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1224
+ if (!draftId)
1225
+ throw new Error("draft-read requires a draft id");
1226
+ const wantJson = isTruthyFlag(parsed.flags.json);
1227
+ await withMcpClient(async (client) => {
1228
+ const result = await client.callTool({ name: "get_draft", arguments: { draftId } });
1229
+ printToolCallResult(result, wantJson);
1230
+ });
1231
+ }
1232
+ async function runDraftUpdate(parsed) {
1233
+ const draftId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1234
+ if (!draftId)
1235
+ throw new Error("draft-update requires a draft id");
1236
+ let body = getStringFlag(parsed.flags, "body");
1237
+ if (!body) {
1238
+ const chunks = [];
1239
+ for await (const chunk of process.stdin)
1240
+ chunks.push(Buffer.from(chunk));
1241
+ const text = Buffer.concat(chunks).toString("utf8").trim();
1242
+ if (text)
1243
+ body = text;
1244
+ }
1245
+ const wantJson = isTruthyFlag(parsed.flags.json);
1246
+ await withMcpClient(async (client) => {
1247
+ const result = await client.callTool({
1248
+ name: "update_draft",
1249
+ arguments: {
1250
+ draftId,
1251
+ to: getStringFlag(parsed.flags, "to"),
1252
+ cc: getStringFlag(parsed.flags, "cc"),
1253
+ bcc: getStringFlag(parsed.flags, "bcc"),
1254
+ subject: getStringFlag(parsed.flags, "subject"),
1255
+ body: body || undefined,
1256
+ notes: getStringFlag(parsed.flags, "notes"),
1257
+ },
1258
+ });
1259
+ printToolCallResult(result, wantJson);
1260
+ });
1261
+ }
1262
+ async function runDraftReply(parsed) {
1263
+ const emailId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1264
+ if (!emailId)
1265
+ throw new Error("draft-reply requires an emailId");
1266
+ let body = getStringFlag(parsed.flags, "body");
1267
+ if (!body) {
1268
+ const chunks = [];
1269
+ for await (const chunk of process.stdin)
1270
+ chunks.push(Buffer.from(chunk));
1271
+ const text = Buffer.concat(chunks).toString("utf8").trim();
1272
+ if (text)
1273
+ body = text;
1274
+ }
1275
+ if (!body)
1276
+ throw new Error("draft-reply requires --body or body piped via stdin");
1277
+ const wantJson = isTruthyFlag(parsed.flags.json);
1278
+ await withMcpClient(async (client) => {
1279
+ const result = await client.callTool({
1280
+ name: "create_reply_draft",
1281
+ arguments: {
1282
+ emailId,
1283
+ body,
1284
+ replyAll: isTruthyFlag(parsed.flags["reply-all"]) || isTruthyFlag(parsed.flags.all) || undefined,
1285
+ cc: getStringFlag(parsed.flags, "cc"),
1286
+ bcc: getStringFlag(parsed.flags, "bcc"),
1287
+ notes: getStringFlag(parsed.flags, "notes"),
1288
+ },
1289
+ });
1290
+ printToolCallResult(result, wantJson);
1291
+ });
1292
+ }
1293
+ async function runDraftForward(parsed) {
1294
+ const emailId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1295
+ const to = getStringFlag(parsed.flags, "to");
1296
+ if (!emailId)
1297
+ throw new Error("draft-forward requires an emailId");
1298
+ if (!to)
1299
+ throw new Error("draft-forward requires --to");
1300
+ let body = getStringFlag(parsed.flags, "body");
1301
+ if (!body) {
1302
+ const chunks = [];
1303
+ for await (const chunk of process.stdin)
1304
+ chunks.push(Buffer.from(chunk));
1305
+ const text = Buffer.concat(chunks).toString("utf8").trim();
1306
+ if (text)
1307
+ body = text;
1308
+ }
1309
+ const wantJson = isTruthyFlag(parsed.flags.json);
1310
+ await withMcpClient(async (client) => {
1311
+ const result = await client.callTool({
1312
+ name: "create_forward_draft",
1313
+ arguments: { emailId, to, body: body || undefined, cc: getStringFlag(parsed.flags, "cc"), bcc: getStringFlag(parsed.flags, "bcc"), notes: getStringFlag(parsed.flags, "notes") },
1314
+ });
1315
+ printToolCallResult(result, wantJson);
1316
+ });
1317
+ }
1318
+ async function runDraftSync(parsed) {
1319
+ const draftId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1320
+ if (!draftId)
1321
+ throw new Error("draft-sync requires a draft id");
1322
+ const wantJson = isTruthyFlag(parsed.flags.json);
1323
+ await withMcpClient(async (client) => {
1324
+ const result = await client.callTool({ name: "sync_draft_to_remote", arguments: { draftId } });
1325
+ printToolCallResult(result, wantJson);
1326
+ });
1327
+ }
1328
+ async function runRemoteDrafts(parsed) {
1329
+ const wantJson = isTruthyFlag(parsed.flags.json);
1330
+ await withMcpClient(async (client) => {
1331
+ const result = await client.callTool({
1332
+ name: "list_remote_drafts",
1333
+ arguments: { limit: getNumberFlag(parsed.flags, "limit", 50), offset: getNumberFlag(parsed.flags, "offset", 0) },
1334
+ });
1335
+ printToolCallResult(result, wantJson);
1336
+ });
1337
+ }
1338
+ async function runDraftThreadReply(parsed) {
1339
+ const threadId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1340
+ if (!threadId)
1341
+ throw new Error("draft-thread-reply requires a threadId");
1342
+ let body = getStringFlag(parsed.flags, "body");
1343
+ if (!body) {
1344
+ const chunks = [];
1345
+ for await (const chunk of process.stdin)
1346
+ chunks.push(Buffer.from(chunk));
1347
+ const text = Buffer.concat(chunks).toString("utf8").trim();
1348
+ if (text)
1349
+ body = text;
1350
+ }
1351
+ if (!body)
1352
+ throw new Error("draft-thread-reply requires --body or body piped via stdin");
1353
+ const wantJson = isTruthyFlag(parsed.flags.json);
1354
+ await withMcpClient(async (client) => {
1355
+ const result = await client.callTool({
1356
+ name: "create_thread_reply_draft",
1357
+ arguments: {
1358
+ threadId,
1359
+ body,
1360
+ replyAll: isTruthyFlag(parsed.flags["reply-all"]) || isTruthyFlag(parsed.flags.all) || undefined,
1361
+ cc: getStringFlag(parsed.flags, "cc"),
1362
+ bcc: getStringFlag(parsed.flags, "bcc"),
1363
+ notes: getStringFlag(parsed.flags, "notes"),
1364
+ },
1365
+ });
1366
+ printToolCallResult(result, wantJson);
1367
+ });
1368
+ }
1369
+ async function runDraftSend(parsed) {
1370
+ const draftId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1371
+ if (!draftId)
1372
+ throw new Error("draft-send requires a draft id");
1373
+ const wantJson = isTruthyFlag(parsed.flags.json);
1374
+ await withMcpClient(async (client) => {
1375
+ const result = await client.callTool({ name: "send_draft", arguments: { draftId } });
1376
+ printToolCallResult(result, wantJson);
1377
+ });
1378
+ }
1379
+ async function runDraftDelete(parsed) {
1380
+ const draftId = parsed.positionals[0] || getStringFlag(parsed.flags, "id");
1381
+ if (!draftId)
1382
+ throw new Error("draft-delete requires a draft id");
1383
+ const wantJson = isTruthyFlag(parsed.flags.json);
1384
+ await withMcpClient(async (client) => {
1385
+ const result = await client.callTool({ name: "delete_draft", arguments: { draftId } });
1386
+ printToolCallResult(result, wantJson);
1387
+ });
1388
+ }
1389
+ async function runTools(parsed) {
1390
+ const wantJson = isTruthyFlag(parsed.flags.json);
1391
+ await withMcpClient(async (client) => {
1392
+ const result = await client.listTools();
1393
+ if (wantJson) {
1394
+ process.stdout.write(json(result));
1395
+ return;
1396
+ }
1397
+ process.stdout.write(result.tools.length === 0
1398
+ ? "No MCP tools exposed.\n"
1399
+ : result.tools
1400
+ .map((tool, index) => `${index + 1}. ${tool.name}${tool.description ? ` | ${tool.description}` : ""}`)
1401
+ .join("\n") + "\n");
1402
+ });
1403
+ }
1404
+ async function runTool(parsed) {
1405
+ const toolName = parsed.positionals[0];
1406
+ if (!toolName) {
1407
+ throw new Error("tool requires a tool name, for example: proton-mail-bridge tool get_connection_status");
1408
+ }
1409
+ const wantJson = isTruthyFlag(parsed.flags.json);
1410
+ const args = await parseToolArgs(parsed);
1411
+ await withMcpClient(async (client) => {
1412
+ const result = await client.callTool({
1413
+ name: toolName,
1414
+ arguments: args,
1415
+ });
1416
+ printToolCallResult(result, wantJson);
1417
+ });
1418
+ }
1419
+ async function runClaude(parsed) {
1420
+ switch (parsed.subcommand) {
1421
+ case "setup":
1422
+ await runClaudeDesktopSetupWizard();
1423
+ return;
1424
+ case "install":
1425
+ case "update": {
1426
+ const result = await installClaudeDesktopConfig();
1427
+ process.stdout.write(json(result));
1428
+ return;
1429
+ }
1430
+ case "check":
1431
+ case "doctor": {
1432
+ const result = await getClaudeDesktopInstallStatus();
1433
+ process.stdout.write(json(result));
1434
+ return;
1435
+ }
1436
+ default:
1437
+ throw new Error("claude requires one of: setup, install, update, check, doctor");
1438
+ }
1439
+ }
1440
+ export async function main() {
1441
+ const parsed = parseCliArgs(process.argv.slice(2));
1442
+ switch (parsed.command) {
1443
+ case "help":
1444
+ case "--help":
1445
+ case "-h":
1446
+ printHelp();
1447
+ return;
1448
+ case "status":
1449
+ await runStatus(parsed);
1450
+ return;
1451
+ case "doctor":
1452
+ await runDoctor(parsed);
1453
+ return;
1454
+ case "connection-status":
1455
+ await runConnectionStatus(parsed);
1456
+ return;
1457
+ case "runtime-status":
1458
+ await runRuntimeStatus(parsed);
1459
+ return;
1460
+ case "sync":
1461
+ await runSync(parsed);
1462
+ return;
1463
+ case "index-status":
1464
+ await runIndexStatus(parsed);
1465
+ return;
1466
+ case "folders":
1467
+ await runFolders(parsed);
1468
+ return;
1469
+ case "create-folder":
1470
+ await runCreateFolder(parsed);
1471
+ return;
1472
+ case "rename-folder":
1473
+ await runRenameFolder(parsed);
1474
+ return;
1475
+ case "delete-folder":
1476
+ await runDeleteFolder(parsed);
1477
+ return;
1478
+ case "labels":
1479
+ await runLabels(parsed);
1480
+ return;
1481
+ case "threads":
1482
+ await runThreads(parsed);
1483
+ return;
1484
+ case "digest":
1485
+ await runDigest(parsed);
1486
+ return;
1487
+ case "followups":
1488
+ await runFollowups(parsed);
1489
+ return;
1490
+ case "drafts":
1491
+ await runDrafts(parsed);
1492
+ return;
1493
+ case "attachments":
1494
+ await runAttachments(parsed);
1495
+ return;
1496
+ case "search":
1497
+ await runSearch(parsed);
1498
+ return;
1499
+ case "read":
1500
+ await runRead(parsed);
1501
+ return;
1502
+ case "move":
1503
+ await runMove(parsed);
1504
+ return;
1505
+ case "archive":
1506
+ await runArchive(parsed);
1507
+ return;
1508
+ case "trash":
1509
+ await runTrash(parsed);
1510
+ return;
1511
+ case "restore":
1512
+ await runRestore(parsed);
1513
+ return;
1514
+ case "mark-read":
1515
+ await runMarkRead(parsed);
1516
+ return;
1517
+ case "star":
1518
+ await runStar(parsed);
1519
+ return;
1520
+ case "delete":
1521
+ await runDelete(parsed);
1522
+ return;
1523
+ case "send":
1524
+ await runSend(parsed);
1525
+ return;
1526
+ case "reply":
1527
+ await runReply(parsed);
1528
+ return;
1529
+ case "forward":
1530
+ await runForward(parsed);
1531
+ return;
1532
+ case "emails":
1533
+ await runEmails(parsed);
1534
+ return;
1535
+ case "thread":
1536
+ await runThread(parsed);
1537
+ return;
1538
+ case "thread-brief":
1539
+ await runThreadBrief(parsed);
1540
+ return;
1541
+ case "actionable":
1542
+ await runActionable(parsed);
1543
+ return;
1544
+ case "document-threads":
1545
+ await runDocumentThreads(parsed);
1546
+ return;
1547
+ case "meeting-context":
1548
+ await runMeetingContext(parsed);
1549
+ return;
1550
+ case "thread-action":
1551
+ await runThreadAction(parsed);
1552
+ return;
1553
+ case "batch":
1554
+ await runBatch(parsed);
1555
+ return;
1556
+ case "stats":
1557
+ await runStats(parsed);
1558
+ return;
1559
+ case "analytics":
1560
+ await runAnalytics(parsed);
1561
+ return;
1562
+ case "contacts":
1563
+ await runContacts(parsed);
1564
+ return;
1565
+ case "volume-trends":
1566
+ await runVolumeTrends(parsed);
1567
+ return;
1568
+ case "watch":
1569
+ await runWatch(parsed);
1570
+ return;
1571
+ case "notify":
1572
+ await runNotify(parsed);
1573
+ return;
1574
+ case "test-email":
1575
+ await runTestEmail(parsed);
1576
+ return;
1577
+ case "draft-create":
1578
+ await runDraftCreate(parsed);
1579
+ return;
1580
+ case "draft-read":
1581
+ await runDraftRead(parsed);
1582
+ return;
1583
+ case "draft-update":
1584
+ await runDraftUpdate(parsed);
1585
+ return;
1586
+ case "draft-reply":
1587
+ await runDraftReply(parsed);
1588
+ return;
1589
+ case "draft-forward":
1590
+ await runDraftForward(parsed);
1591
+ return;
1592
+ case "draft-sync":
1593
+ await runDraftSync(parsed);
1594
+ return;
1595
+ case "draft-send":
1596
+ await runDraftSend(parsed);
1597
+ return;
1598
+ case "draft-delete":
1599
+ await runDraftDelete(parsed);
1600
+ return;
1601
+ case "remote-drafts":
1602
+ await runRemoteDrafts(parsed);
1603
+ return;
1604
+ case "draft-thread-reply":
1605
+ await runDraftThreadReply(parsed);
1606
+ return;
1607
+ case "tools":
1608
+ await runTools(parsed);
1609
+ return;
1610
+ case "tool":
1611
+ await runTool(parsed);
1612
+ return;
1613
+ case "claude":
1614
+ await runClaude(parsed);
1615
+ return;
1616
+ default:
1617
+ throw new Error(`Unknown command: ${parsed.command}`);
1618
+ }
1619
+ }
1620
+ const isDirectExecution = Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
1621
+ if (isDirectExecution) {
1622
+ main().catch((error) => {
1623
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1624
+ process.exit(1);
1625
+ });
1626
+ }
1627
+ //# sourceMappingURL=cli.js.map