icloud-mcp 1.1.0 → 1.2.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 (4) hide show
  1. package/README.md +30 -16
  2. package/index.js +221 -82
  3. package/package.json +1 -1
  4. package/test.js +98 -9
package/README.md CHANGED
@@ -5,13 +5,14 @@ A Model Context Protocol (MCP) server that connects Claude Desktop to your iClou
5
5
  ## Features
6
6
 
7
7
  - šŸ“¬ Read and paginate through your inbox
8
- - šŸ” Search emails by keyword, sender, or date range
8
+ - šŸ” Search emails by keyword, sender, date range, and more
9
9
  - šŸ—‘ļø Bulk delete emails by any combination of filters
10
10
  - šŸ“ Bulk move emails between folders with flexible filtering
11
11
  - šŸ“Š Analyze top senders to identify inbox clutter
12
12
  - šŸ”¢ Count emails matching any filter before taking action
13
- - āœ… Mark emails as read/unread, flag/unflag
14
- - šŸ—‚ļø List and create mailboxes
13
+ - āœ… Mark emails as read/unread, flag/unflag in bulk or individually
14
+ - šŸ—‚ļø List, create, rename, and delete mailboxes
15
+ - šŸ”„ Dry run mode for bulk operations — preview before committing
15
16
 
16
17
  ## Prerequisites
17
18
 
@@ -77,33 +78,38 @@ Fully quit Claude Desktop (Cmd+Q) and reopen it. You should now be able to manag
77
78
 
78
79
  | Tool | Description |
79
80
  |------|-------------|
80
- | `get_inbox_summary` | Total, unread, and recent email counts |
81
- | `get_top_senders` | Top senders by volume from a sample of recent emails |
81
+ | `get_inbox_summary` | Total, unread, and recent email counts for INBOX |
82
+ | `get_mailbox_summary` | Total, unread, and recent email counts for any folder |
83
+ | `get_top_senders` | Top senders by volume from a sample of recent emails (supports `sampleSize` and `maxResults`) |
82
84
  | `get_unread_senders` | Top senders of unread emails (supports `sampleSize` and `maxResults`) |
83
85
  | `read_inbox` | Paginated inbox with sender, subject, date |
84
86
  | `get_email` | Full content of a specific email by UID |
85
87
  | `get_emails_by_sender` | All emails from a specific address |
86
88
  | `get_emails_by_date_range` | Emails between two dates |
87
- | `search_emails` | Search by keyword across subject, sender, and body |
89
+ | `search_emails` | Search by keyword with optional filters (date, unread, domain, etc.) |
88
90
  | `count_emails` | Count emails matching any combination of filters without modifying them |
89
- | `bulk_move` | Move emails matching any combination of filters between folders |
90
- | `bulk_delete` | Delete emails matching any combination of filters |
91
- | `flag_email` | Flag or unflag an email |
92
- | `mark_as_read` | Mark an email as read or unread |
93
- | `delete_email` | Move an email to Deleted Messages |
91
+ | `bulk_move` | Move emails matching any combination of filters between folders (supports `dryRun`) |
92
+ | `bulk_delete` | Delete emails matching any combination of filters (supports `dryRun`) |
93
+ | `bulk_flag` | Flag or unflag emails matching any combination of filters |
94
+ | `bulk_mark_read` | Mark all emails (or all from a sender) as read |
95
+ | `bulk_mark_unread` | Mark all emails (or all from a sender) as unread |
94
96
  | `bulk_delete_by_sender` | Delete all emails from a sender |
95
97
  | `bulk_delete_by_subject` | Delete all emails matching a subject keyword |
96
98
  | `bulk_move_by_sender` | Move all emails from a sender to a folder |
97
- | `bulk_mark_read` | Mark all emails (or all from a sender) as read |
98
- | `delete_older_than` | Delete all emails older than N days |
99
+ | `flag_email` | Flag or unflag a single email |
100
+ | `mark_as_read` | Mark a single email as read or unread |
101
+ | `delete_email` | Move an email to Deleted Messages |
99
102
  | `move_email` | Move a single email to a folder |
103
+ | `delete_older_than` | Delete all emails older than N days |
100
104
  | `list_mailboxes` | List all folders in your iCloud Mail |
101
105
  | `create_mailbox` | Create a new folder |
106
+ | `rename_mailbox` | Rename an existing folder |
107
+ | `delete_mailbox` | Delete a folder (must be empty first) |
102
108
  | `empty_trash` | Permanently delete all emails in Deleted Messages |
103
109
 
104
- ## Bulk Move & Delete Filters
110
+ ## Bulk Move, Delete & Flag Filters
105
111
 
106
- `bulk_move` and `bulk_delete` accept any combination of these filters:
112
+ `bulk_move`, `bulk_delete`, `bulk_flag`, `search_emails`, and `count_emails` all accept any combination of these filters:
107
113
 
108
114
  | Filter | Type | Description |
109
115
  |--------|------|-------------|
@@ -118,17 +124,25 @@ Fully quit Claude Desktop (Cmd+Q) and reopen it. You should now be able to manag
118
124
  | `smaller` | number | Only emails smaller than this size in KB |
119
125
  | `hasAttachment` | boolean | Only emails with attachments |
120
126
 
127
+ ### Dry Run Mode
128
+
129
+ Pass `dryRun: true` to `bulk_move` or `bulk_delete` to preview how many emails would be affected without making any changes:
130
+
131
+ > *"How many emails would be deleted if I removed everything from linkedin.com before 2022?"*
132
+
121
133
  ## Example Usage
122
134
 
123
135
  Once configured, you can ask Claude things like:
124
136
 
125
137
  - *"Show me the top senders in my iCloud inbox"*
126
138
  - *"How many unread emails do I have from substack.com?"*
139
+ - *"How many emails would be moved if I archived everything from linkedin.com before 2022?"*
127
140
  - *"Move all emails from substack.com older than 2023 to my Newsletters folder"*
128
141
  - *"Delete all unread emails from linkedin.com before 2022"*
129
142
  - *"Move everything in my old_folders/college folder to Archive"*
130
143
  - *"How many emails do I have with attachments larger than 5MB?"*
131
- - *"Delete all emails from no-reply@instagram.com"*
144
+ - *"Flag all unread emails from my bank"*
145
+ - *"Rename my Newsletters folder to Old Newsletters"*
132
146
  - *"Show me emails from the last week"*
133
147
 
134
148
  ## Security
package/index.js CHANGED
@@ -84,7 +84,7 @@ async function getInboxSummary(mailbox = 'INBOX') {
84
84
  return { mailbox, total: status.messages, unread: status.unseen, recent: status.recent };
85
85
  }
86
86
 
87
- async function getTopSenders(mailbox = 'INBOX', sampleSize = 500) {
87
+ async function getTopSenders(mailbox = 'INBOX', sampleSize = 500, maxResults = 20) {
88
88
  const client = createClient();
89
89
  await client.connect();
90
90
  const mb = await client.mailboxOpen(mailbox);
@@ -108,8 +108,8 @@ async function getTopSenders(mailbox = 'INBOX', sampleSize = 500) {
108
108
  }
109
109
 
110
110
  await client.logout();
111
- const topAddresses = Object.entries(senderCounts).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([address, count]) => ({ address, count }));
112
- const topDomains = Object.entries(senderDomains).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([domain, count]) => ({ domain, count }));
111
+ const topAddresses = Object.entries(senderCounts).sort((a, b) => b[1] - a[1]).slice(0, maxResults).map(([address, count]) => ({ address, count }));
112
+ const topDomains = Object.entries(senderDomains).sort((a, b) => b[1] - a[1]).slice(0, maxResults).map(([domain, count]) => ({ domain, count }));
113
113
  return { sampledEmails: count, topAddresses, topDomains };
114
114
  }
115
115
 
@@ -126,7 +126,6 @@ async function getUnreadSenders(mailbox = 'INBOX', sampleSize = 500, maxResults
126
126
  return [];
127
127
  }
128
128
 
129
- // Fetch all envelopes in a single batch instead of one by one
130
129
  for await (const msg of client.fetch(recentUids, { envelope: true }, { uid: true })) {
131
130
  const address = msg.envelope.from?.[0]?.address;
132
131
  if (address) senderCounts[address] = (senderCounts[address] || 0) + 1;
@@ -244,6 +243,34 @@ async function bulkMarkRead(mailbox = 'INBOX', sender = null) {
244
243
  return { marked: uids.length, sender: sender || 'all' };
245
244
  }
246
245
 
246
+ async function bulkMarkUnread(mailbox = 'INBOX', sender = null) {
247
+ const client = createClient();
248
+ await client.connect();
249
+ await client.mailboxOpen(mailbox);
250
+ const query = sender ? { from: sender, seen: true } : { seen: true };
251
+ const uids = (await client.search(query, { uid: true })) ?? [];
252
+ if (uids.length === 0) { await client.logout(); return { marked: 0 }; }
253
+ await client.messageFlagsRemove(uids, ['\\Seen'], { uid: true });
254
+ await client.logout();
255
+ return { marked: uids.length, sender: sender || 'all' };
256
+ }
257
+
258
+ async function bulkFlag(filters, flagged, mailbox = 'INBOX') {
259
+ const client = createClient();
260
+ await client.connect();
261
+ await client.mailboxOpen(mailbox);
262
+ const query = buildQuery(filters);
263
+ const uids = (await client.search(query, { uid: true })) ?? [];
264
+ if (uids.length === 0) { await client.logout(); return { flagged: 0 }; }
265
+ if (flagged) {
266
+ await client.messageFlagsAdd(uids, ['\\Flagged'], { uid: true });
267
+ } else {
268
+ await client.messageFlagsRemove(uids, ['\\Flagged'], { uid: true });
269
+ }
270
+ await client.logout();
271
+ return { [flagged ? 'flagged' : 'unflagged']: uids.length, filters };
272
+ }
273
+
247
274
  async function emptyTrash() {
248
275
  const client = createClient();
249
276
  await client.connect();
@@ -263,6 +290,30 @@ async function createMailbox(name) {
263
290
  return { created: name };
264
291
  }
265
292
 
293
+ async function renameMailbox(oldName, newName) {
294
+ const client = createClient();
295
+ await client.connect();
296
+ await client.mailboxRename(oldName, newName);
297
+ await client.logout();
298
+ return { renamed: { from: oldName, to: newName } };
299
+ }
300
+
301
+ async function deleteMailbox(name) {
302
+ const client = createClient();
303
+ await client.connect();
304
+ await client.mailboxDelete(name);
305
+ await client.logout();
306
+ return { deleted: name };
307
+ }
308
+
309
+ async function getMailboxSummary(mailbox) {
310
+ const client = createClient();
311
+ await client.connect();
312
+ const status = await client.status(mailbox, { messages: true, unseen: true, recent: true });
313
+ await client.logout();
314
+ return { mailbox, total: status.messages, unread: status.unseen, recent: status.recent };
315
+ }
316
+
266
317
  async function getEmailContent(uid, mailbox = 'INBOX') {
267
318
  const client = createClient();
268
319
  await client.connect();
@@ -344,14 +395,21 @@ async function listMailboxes() {
344
395
  return mailboxes;
345
396
  }
346
397
 
347
- async function searchEmails(query, mailbox = 'INBOX', limit = 10) {
398
+ async function searchEmails(query, mailbox = 'INBOX', limit = 10, filters = {}) {
348
399
  const client = createClient();
349
400
  await client.connect();
350
401
  await client.mailboxOpen(mailbox);
351
- const uids = (await client.search(
352
- { or: [{ subject: query }, { from: query }, { body: query }] },
353
- { uid: true }
354
- )) ?? [];
402
+
403
+ // Build base text search
404
+ const textQuery = { or: [{ subject: query }, { from: query }, { body: query }] };
405
+
406
+ // Merge with additional filters if provided
407
+ const extraQuery = buildQuery(filters);
408
+ const finalQuery = Object.keys(extraQuery).length > 0 && !extraQuery.all
409
+ ? { ...textQuery, ...extraQuery }
410
+ : textQuery;
411
+
412
+ const uids = (await client.search(finalQuery, { uid: true })) ?? [];
355
413
  const emails = [];
356
414
  const recentUids = uids.slice(-limit).reverse();
357
415
  for (const uid of recentUids) {
@@ -368,7 +426,7 @@ async function searchEmails(query, mailbox = 'INBOX', limit = 10) {
368
426
  }
369
427
  }
370
428
  await client.logout();
371
- return emails;
429
+ return { total: uids.length, showing: emails.length, emails };
372
430
  }
373
431
 
374
432
  async function moveEmail(uid, targetMailbox, sourceMailbox = 'INBOX') {
@@ -395,29 +453,36 @@ function buildQuery(filters) {
395
453
  if (filters.larger) query.larger = filters.larger * 1024;
396
454
  if (filters.smaller) query.smaller = filters.smaller * 1024;
397
455
  if (filters.hasAttachment) query.header = ['Content-Type', 'multipart/mixed'];
398
- // If no filters set, match all
399
456
  if (Object.keys(query).length === 0) query.all = true;
400
457
  return query;
401
458
  }
402
459
 
403
- async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX') {
460
+ async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX', dryRun = false) {
404
461
  const client = createClient();
405
462
  await client.connect();
406
463
  await client.mailboxOpen(sourceMailbox);
407
464
  const query = buildQuery(filters);
408
465
  const uids = (await client.search(query, { uid: true })) ?? [];
466
+ if (dryRun) {
467
+ await client.logout();
468
+ return { dryRun: true, wouldMove: uids.length, sourceMailbox, targetMailbox, filters };
469
+ }
409
470
  if (uids.length === 0) { await client.logout(); return { moved: 0, sourceMailbox, targetMailbox }; }
410
471
  await client.messageMove(uids, targetMailbox, { uid: true });
411
472
  await client.logout();
412
473
  return { moved: uids.length, sourceMailbox, targetMailbox, filters };
413
474
  }
414
475
 
415
- async function bulkDelete(filters, sourceMailbox = 'INBOX') {
476
+ async function bulkDelete(filters, sourceMailbox = 'INBOX', dryRun = false) {
416
477
  const client = createClient();
417
478
  await client.connect();
418
479
  await client.mailboxOpen(sourceMailbox);
419
480
  const query = buildQuery(filters);
420
481
  const uids = (await client.search(query, { uid: true })) ?? [];
482
+ if (dryRun) {
483
+ await client.logout();
484
+ return { dryRun: true, wouldDelete: uids.length, sourceMailbox, filters };
485
+ }
421
486
  if (uids.length === 0) { await client.logout(); return { deleted: 0, sourceMailbox }; }
422
487
  await client.messageDelete(uids, { uid: true });
423
488
  await client.logout();
@@ -436,7 +501,7 @@ async function countEmails(filters, mailbox = 'INBOX') {
436
501
 
437
502
  async function main() {
438
503
  const server = new Server(
439
- { name: 'icloud-mail', version: '1.1.0' },
504
+ { name: 'icloud-mail', version: '1.2.0' },
440
505
  { capabilities: { tools: {} } }
441
506
  );
442
507
 
@@ -460,6 +525,17 @@ async function main() {
460
525
  description: 'Get a summary of a mailbox including total, unread, and recent email counts',
461
526
  inputSchema: { type: 'object', properties: { mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' } } }
462
527
  },
528
+ {
529
+ name: 'get_mailbox_summary',
530
+ description: 'Get total, unread, and recent email counts for any specific mailbox/folder',
531
+ inputSchema: {
532
+ type: 'object',
533
+ properties: {
534
+ mailbox: { type: 'string', description: 'Mailbox path to summarize (e.g. Newsletters, Archive)' }
535
+ },
536
+ required: ['mailbox']
537
+ }
538
+ },
463
539
  {
464
540
  name: 'get_top_senders',
465
541
  description: 'Get the top senders by email count from a sample of the inbox',
@@ -467,7 +543,8 @@ async function main() {
467
543
  type: 'object',
468
544
  properties: {
469
545
  mailbox: { type: 'string', description: 'Mailbox to analyze (default INBOX)' },
470
- sampleSize: { type: 'number', description: 'Number of emails to sample (default 500)' }
546
+ sampleSize: { type: 'number', description: 'Number of emails to sample (default 500)' },
547
+ maxResults: { type: 'number', description: 'Max number of senders/domains to return (default 20)' }
471
548
  }
472
549
  }
473
550
  },
@@ -523,17 +600,68 @@ async function main() {
523
600
  },
524
601
  {
525
602
  name: 'search_emails',
526
- description: 'Search emails by keyword',
603
+ description: 'Search emails by keyword, with optional filters for date, read status, domain, and more',
527
604
  inputSchema: {
528
605
  type: 'object',
529
606
  properties: {
530
- query: { type: 'string', description: 'Search query' },
607
+ query: { type: 'string', description: 'Search keyword (matches subject, sender, body)' },
531
608
  mailbox: { type: 'string', description: 'Mailbox to search (default INBOX)' },
532
- limit: { type: 'number', description: 'Max results (default 10)' }
609
+ limit: { type: 'number', description: 'Max results (default 10)' },
610
+ ...filtersSchema
533
611
  },
534
612
  required: ['query']
535
613
  }
536
614
  },
615
+ {
616
+ name: 'count_emails',
617
+ description: 'Count how many emails match a set of filters without moving or deleting them. Use this before bulk_move or bulk_delete to preview how many emails will be affected.',
618
+ inputSchema: {
619
+ type: 'object',
620
+ properties: {
621
+ mailbox: { type: 'string', description: 'Mailbox to count in (default INBOX)' },
622
+ ...filtersSchema
623
+ }
624
+ }
625
+ },
626
+ {
627
+ name: 'bulk_move',
628
+ description: 'Move emails matching any combination of filters from one mailbox to another. Operates on ALL matching emails in a single IMAP operation. Use dryRun: true to preview without making changes.',
629
+ inputSchema: {
630
+ type: 'object',
631
+ properties: {
632
+ targetMailbox: { type: 'string', description: 'Destination mailbox path' },
633
+ sourceMailbox: { type: 'string', description: 'Source mailbox (default INBOX)' },
634
+ dryRun: { type: 'boolean', description: 'If true, preview what would be moved without actually moving' },
635
+ ...filtersSchema
636
+ },
637
+ required: ['targetMailbox']
638
+ }
639
+ },
640
+ {
641
+ name: 'bulk_delete',
642
+ description: 'Delete emails matching any combination of filters. Operates on ALL matching emails in a single IMAP operation. Use dryRun: true to preview without making changes.',
643
+ inputSchema: {
644
+ type: 'object',
645
+ properties: {
646
+ sourceMailbox: { type: 'string', description: 'Mailbox to delete from (default INBOX)' },
647
+ dryRun: { type: 'boolean', description: 'If true, preview what would be deleted without actually deleting' },
648
+ ...filtersSchema
649
+ }
650
+ }
651
+ },
652
+ {
653
+ name: 'bulk_flag',
654
+ description: 'Flag or unflag emails matching any combination of filters in bulk',
655
+ inputSchema: {
656
+ type: 'object',
657
+ properties: {
658
+ flagged: { type: 'boolean', description: 'True to flag, false to unflag' },
659
+ mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
660
+ ...filtersSchema
661
+ },
662
+ required: ['flagged']
663
+ }
664
+ },
537
665
  {
538
666
  name: 'bulk_delete_by_sender',
539
667
  description: 'Delete all emails from a specific sender',
@@ -572,61 +700,56 @@ async function main() {
572
700
  }
573
701
  },
574
702
  {
575
- name: 'delete_older_than',
576
- description: 'Delete all emails older than a certain number of days',
703
+ name: 'bulk_mark_read',
704
+ description: 'Mark all emails as read, optionally filtered by sender',
577
705
  inputSchema: {
578
706
  type: 'object',
579
707
  properties: {
580
- days: { type: 'number', description: 'Delete emails older than this many days' },
581
- mailbox: { type: 'string', description: 'Mailbox (default INBOX)' }
582
- },
583
- required: ['days']
708
+ mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
709
+ sender: { type: 'string', description: 'Optional: only mark emails from this sender as read' }
710
+ }
584
711
  }
585
712
  },
586
713
  {
587
- name: 'get_emails_by_date_range',
588
- description: 'Get emails between two dates',
714
+ name: 'bulk_mark_unread',
715
+ description: 'Mark all emails as unread, optionally filtered by sender',
589
716
  inputSchema: {
590
717
  type: 'object',
591
718
  properties: {
592
- startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
593
- endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
594
719
  mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
595
- limit: { type: 'number', description: 'Max results (default 10)' }
596
- },
597
- required: ['startDate', 'endDate']
720
+ sender: { type: 'string', description: 'Optional: only mark emails from this sender as unread' }
721
+ }
598
722
  }
599
723
  },
600
724
  {
601
- name: 'bulk_mark_read',
602
- description: 'Mark all emails as read, optionally filtered by sender',
725
+ name: 'delete_older_than',
726
+ description: 'Delete all emails older than a certain number of days',
603
727
  inputSchema: {
604
728
  type: 'object',
605
729
  properties: {
606
- mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
607
- sender: { type: 'string', description: 'Optional: only mark emails from this sender as read' }
608
- }
730
+ days: { type: 'number', description: 'Delete emails older than this many days' },
731
+ mailbox: { type: 'string', description: 'Mailbox (default INBOX)' }
732
+ },
733
+ required: ['days']
609
734
  }
610
735
  },
611
736
  {
612
- name: 'empty_trash',
613
- description: 'Permanently delete all emails in Deleted Messages',
614
- inputSchema: { type: 'object', properties: {} }
615
- },
616
- {
617
- name: 'create_mailbox',
618
- description: 'Create a new mailbox/folder',
737
+ name: 'get_emails_by_date_range',
738
+ description: 'Get emails between two dates',
619
739
  inputSchema: {
620
740
  type: 'object',
621
741
  properties: {
622
- name: { type: 'string', description: 'Name of the new mailbox' }
742
+ startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
743
+ endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
744
+ mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
745
+ limit: { type: 'number', description: 'Max results (default 10)' }
623
746
  },
624
- required: ['name']
747
+ required: ['startDate', 'endDate']
625
748
  }
626
749
  },
627
750
  {
628
751
  name: 'flag_email',
629
- description: 'Flag or unflag an email',
752
+ description: 'Flag or unflag a single email',
630
753
  inputSchema: {
631
754
  type: 'object',
632
755
  properties: {
@@ -639,7 +762,7 @@ async function main() {
639
762
  },
640
763
  {
641
764
  name: 'mark_as_read',
642
- description: 'Mark an email as read or unread',
765
+ description: 'Mark a single email as read or unread',
643
766
  inputSchema: {
644
767
  type: 'object',
645
768
  properties: {
@@ -652,7 +775,7 @@ async function main() {
652
775
  },
653
776
  {
654
777
  name: 'delete_email',
655
- description: 'Delete an email',
778
+ description: 'Delete a single email',
656
779
  inputSchema: {
657
780
  type: 'object',
658
781
  properties: {
@@ -664,7 +787,7 @@ async function main() {
664
787
  },
665
788
  {
666
789
  name: 'move_email',
667
- description: 'Move an email to a different mailbox/folder',
790
+ description: 'Move a single email to a different mailbox/folder',
668
791
  inputSchema: {
669
792
  type: 'object',
670
793
  properties: {
@@ -681,39 +804,43 @@ async function main() {
681
804
  inputSchema: { type: 'object', properties: {} }
682
805
  },
683
806
  {
684
- name: 'bulk_move',
685
- description: 'Move emails matching any combination of filters from one mailbox to another. Use this for large-scale moves — it operates on ALL matching emails in a single IMAP operation regardless of count. Prefer this over read_inbox + move_email for any bulk operation.',
807
+ name: 'create_mailbox',
808
+ description: 'Create a new mailbox/folder',
686
809
  inputSchema: {
687
810
  type: 'object',
688
811
  properties: {
689
- targetMailbox: { type: 'string', description: 'Destination mailbox path' },
690
- sourceMailbox: { type: 'string', description: 'Source mailbox (default INBOX)' },
691
- ...filtersSchema
812
+ name: { type: 'string', description: 'Name of the new mailbox' }
692
813
  },
693
- required: ['targetMailbox']
814
+ required: ['name']
694
815
  }
695
816
  },
696
817
  {
697
- name: 'bulk_delete',
698
- description: 'Delete emails matching any combination of filters. Use this for large-scale deletes — it operates on ALL matching emails in a single IMAP operation regardless of count. Prefer this over read_inbox + delete_email for any bulk operation.',
818
+ name: 'rename_mailbox',
819
+ description: 'Rename an existing mailbox/folder',
699
820
  inputSchema: {
700
821
  type: 'object',
701
822
  properties: {
702
- sourceMailbox: { type: 'string', description: 'Mailbox to delete from (default INBOX)' },
703
- ...filtersSchema
704
- }
823
+ oldName: { type: 'string', description: 'Current mailbox path' },
824
+ newName: { type: 'string', description: 'New mailbox path' }
825
+ },
826
+ required: ['oldName', 'newName']
705
827
  }
706
828
  },
707
829
  {
708
- name: 'count_emails',
709
- description: 'Count how many emails match a set of filters without moving or deleting them. Use this before bulk_move or bulk_delete to preview how many emails will be affected.',
830
+ name: 'delete_mailbox',
831
+ description: 'Delete a mailbox/folder. The folder must be empty first.',
710
832
  inputSchema: {
711
833
  type: 'object',
712
834
  properties: {
713
- mailbox: { type: 'string', description: 'Mailbox to count in (default INBOX)' },
714
- ...filtersSchema
715
- }
835
+ name: { type: 'string', description: 'Mailbox path to delete' }
836
+ },
837
+ required: ['name']
716
838
  }
839
+ },
840
+ {
841
+ name: 'empty_trash',
842
+ description: 'Permanently delete all emails in Deleted Messages',
843
+ inputSchema: { type: 'object', properties: {} }
717
844
  }
718
845
  ]
719
846
  }));
@@ -724,8 +851,10 @@ async function main() {
724
851
  let result;
725
852
  if (name === 'get_inbox_summary') {
726
853
  result = await getInboxSummary(args.mailbox || 'INBOX');
854
+ } else if (name === 'get_mailbox_summary') {
855
+ result = await getMailboxSummary(args.mailbox);
727
856
  } else if (name === 'get_top_senders') {
728
- result = await getTopSenders(args.mailbox || 'INBOX', args.sampleSize || 500);
857
+ result = await getTopSenders(args.mailbox || 'INBOX', args.sampleSize || 500, args.maxResults || 20);
729
858
  } else if (name === 'get_unread_senders') {
730
859
  result = await getUnreadSenders(args.mailbox || 'INBOX', args.sampleSize || 500, args.maxResults || 20);
731
860
  } else if (name === 'get_emails_by_sender') {
@@ -735,23 +864,34 @@ async function main() {
735
864
  } else if (name === 'get_email') {
736
865
  result = await getEmailContent(args.uid, args.mailbox || 'INBOX');
737
866
  } else if (name === 'search_emails') {
738
- result = await searchEmails(args.query, args.mailbox || 'INBOX', args.limit || 10);
867
+ const { query, mailbox, limit, ...filters } = args;
868
+ result = await searchEmails(query, mailbox || 'INBOX', limit || 10, filters);
869
+ } else if (name === 'count_emails') {
870
+ const { mailbox, ...filters } = args;
871
+ result = await countEmails(filters, mailbox || 'INBOX');
872
+ } else if (name === 'bulk_move') {
873
+ const { targetMailbox, sourceMailbox, dryRun, ...filters } = args;
874
+ result = await bulkMove(filters, targetMailbox, sourceMailbox || 'INBOX', dryRun || false);
875
+ } else if (name === 'bulk_delete') {
876
+ const { sourceMailbox, dryRun, ...filters } = args;
877
+ result = await bulkDelete(filters, sourceMailbox || 'INBOX', dryRun || false);
878
+ } else if (name === 'bulk_flag') {
879
+ const { flagged, mailbox, ...filters } = args;
880
+ result = await bulkFlag(filters, flagged, mailbox || 'INBOX');
739
881
  } else if (name === 'bulk_delete_by_sender') {
740
882
  result = await bulkDeleteBySender(args.sender, args.mailbox || 'INBOX');
741
883
  } else if (name === 'bulk_move_by_sender') {
742
884
  result = await bulkMoveBySender(args.sender, args.targetMailbox, args.sourceMailbox || 'INBOX');
743
885
  } else if (name === 'bulk_delete_by_subject') {
744
886
  result = await bulkDeleteBySubject(args.subject, args.mailbox || 'INBOX');
887
+ } else if (name === 'bulk_mark_read') {
888
+ result = await bulkMarkRead(args.mailbox || 'INBOX', args.sender || null);
889
+ } else if (name === 'bulk_mark_unread') {
890
+ result = await bulkMarkUnread(args.mailbox || 'INBOX', args.sender || null);
745
891
  } else if (name === 'delete_older_than') {
746
892
  result = await deleteOlderThan(args.days, args.mailbox || 'INBOX');
747
893
  } else if (name === 'get_emails_by_date_range') {
748
894
  result = await getEmailsByDateRange(args.startDate, args.endDate, args.mailbox || 'INBOX', args.limit || 10);
749
- } else if (name === 'bulk_mark_read') {
750
- result = await bulkMarkRead(args.mailbox || 'INBOX', args.sender || null);
751
- } else if (name === 'empty_trash') {
752
- result = await emptyTrash();
753
- } else if (name === 'create_mailbox') {
754
- result = await createMailbox(args.name);
755
895
  } else if (name === 'flag_email') {
756
896
  result = await flagEmail(args.uid, args.flagged, args.mailbox || 'INBOX');
757
897
  } else if (name === 'mark_as_read') {
@@ -762,15 +902,14 @@ async function main() {
762
902
  result = await moveEmail(args.uid, args.targetMailbox, args.sourceMailbox || 'INBOX');
763
903
  } else if (name === 'list_mailboxes') {
764
904
  result = await listMailboxes();
765
- } else if (name === 'bulk_move') {
766
- const { targetMailbox, sourceMailbox, ...filters } = args;
767
- result = await bulkMove(filters, targetMailbox, sourceMailbox || 'INBOX');
768
- } else if (name === 'bulk_delete') {
769
- const { sourceMailbox, ...filters } = args;
770
- result = await bulkDelete(filters, sourceMailbox || 'INBOX');
771
- } else if (name === 'count_emails') {
772
- const { mailbox, ...filters } = args;
773
- result = await countEmails(filters, mailbox || 'INBOX');
905
+ } else if (name === 'create_mailbox') {
906
+ result = await createMailbox(args.name);
907
+ } else if (name === 'rename_mailbox') {
908
+ result = await renameMailbox(args.oldName, args.newName);
909
+ } else if (name === 'delete_mailbox') {
910
+ result = await deleteMailbox(args.name);
911
+ } else if (name === 'empty_trash') {
912
+ result = await emptyTrash();
774
913
  } else {
775
914
  throw new Error(`Unknown tool: ${name}`);
776
915
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icloud-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A Model Context Protocol (MCP) server for iCloud Mail",
5
5
  "main": "index.js",
6
6
  "bin": {
package/test.js CHANGED
@@ -32,7 +32,7 @@ function callTool(name, args = {}) {
32
32
  {
33
33
  cwd: projectDir,
34
34
  encoding: 'utf8',
35
- timeout: 180000,
35
+ timeout: 300000,
36
36
  env: { ...process.env, IMAP_USER, IMAP_PASSWORD }
37
37
  }
38
38
  );
@@ -74,6 +74,7 @@ function assert(condition, message) {
74
74
 
75
75
  console.log('\n🧪 iCloud MCP Server Tests\n');
76
76
 
77
+ // ─── Mailbox & Summary ────────────────────────────────────────────────────────
77
78
  console.log('šŸ“¬ Mailbox & Summary');
78
79
 
79
80
  test('get_inbox_summary', () => {
@@ -84,6 +85,14 @@ test('get_inbox_summary', () => {
84
85
  console.log(`\n → ${result.total} total, ${result.unread} unread`);
85
86
  });
86
87
 
88
+ test('get_mailbox_summary', () => {
89
+ const result = callTool('get_mailbox_summary', { mailbox: 'INBOX' });
90
+ assert(typeof result.total === 'number', 'total should be a number');
91
+ assert(typeof result.unread === 'number', 'unread should be a number');
92
+ assert(result.mailbox === 'INBOX', 'mailbox should be INBOX');
93
+ console.log(`\n → ${result.total} total, ${result.unread} unread`);
94
+ });
95
+
87
96
  test('list_mailboxes', () => {
88
97
  const result = callTool('list_mailboxes');
89
98
  assert(Array.isArray(result), 'result should be an array');
@@ -92,14 +101,23 @@ test('list_mailboxes', () => {
92
101
  console.log(`\n → ${result.length} mailboxes found`);
93
102
  });
94
103
 
95
- test('get_top_senders (sample 50)', () => {
104
+ test('get_top_senders (sample 50, default maxResults)', () => {
96
105
  const result = callTool('get_top_senders', { sampleSize: 50 });
97
106
  assert(Array.isArray(result.topAddresses), 'topAddresses should be an array');
98
107
  assert(Array.isArray(result.topDomains), 'topDomains should be an array');
99
108
  assert(result.sampledEmails <= 50, 'should not exceed sample size');
109
+ assert(result.topAddresses.length <= 20, 'should not exceed default maxResults of 20');
100
110
  console.log(`\n → top sender: ${result.topAddresses[0]?.address} (${result.topAddresses[0]?.count})`);
101
111
  });
102
112
 
113
+ test('get_top_senders (sample 50, maxResults 5)', () => {
114
+ const result = callTool('get_top_senders', { sampleSize: 50, maxResults: 5 });
115
+ assert(Array.isArray(result.topAddresses), 'topAddresses should be an array');
116
+ assert(result.topAddresses.length <= 5, 'should not exceed maxResults of 5');
117
+ assert(result.topDomains.length <= 5, 'domains should not exceed maxResults of 5');
118
+ console.log(`\n → ${result.topAddresses.length} senders, ${result.topDomains.length} domains (capped at 5)`);
119
+ });
120
+
103
121
  test('get_unread_senders (sample 50, default maxResults)', () => {
104
122
  const result = callTool('get_unread_senders', { sampleSize: 50 });
105
123
  assert(Array.isArray(result), 'result should be an array');
@@ -121,6 +139,7 @@ test('get_unread_senders (sample 50, maxResults 50)', () => {
121
139
  console.log(`\n → ${result.length} unread senders found (capped at 50)`);
122
140
  });
123
141
 
142
+ // ─── Reading Emails ───────────────────────────────────────────────────────────
124
143
  console.log('\nšŸ“§ Reading Emails');
125
144
 
126
145
  test('read_inbox (page 1, limit 5)', () => {
@@ -149,10 +168,25 @@ test('read_inbox (unread only)', () => {
149
168
  console.log(`\n → ${result.emails.length} unread emails`);
150
169
  });
151
170
 
152
- test('search_emails', () => {
171
+ test('search_emails (keyword only)', () => {
153
172
  const result = callTool('search_emails', { query: 'test', limit: 5 });
154
- assert(Array.isArray(result), 'result should be an array');
155
- console.log(`\n → ${result.length} results`);
173
+ assert(typeof result.total === 'number', 'total should be a number');
174
+ assert(Array.isArray(result.emails), 'emails should be an array');
175
+ console.log(`\n → ${result.total} results`);
176
+ });
177
+
178
+ test('search_emails (keyword + unread filter)', () => {
179
+ const result = callTool('search_emails', { query: 'test', limit: 5, unread: true });
180
+ assert(typeof result.total === 'number', 'total should be a number');
181
+ assert(Array.isArray(result.emails), 'emails should be an array');
182
+ console.log(`\n → ${result.total} unread results`);
183
+ });
184
+
185
+ test('search_emails (keyword + date filter)', () => {
186
+ const result = callTool('search_emails', { query: 'test', limit: 5, since: '2024-01-01' });
187
+ assert(typeof result.total === 'number', 'total should be a number');
188
+ assert(Array.isArray(result.emails), 'emails should be an array');
189
+ console.log(`\n → ${result.total} results since 2024`);
156
190
  });
157
191
 
158
192
  test('get_emails_by_sender', () => {
@@ -186,6 +220,7 @@ test('get_email (fetch first email content)', () => {
186
220
  console.log(`\n → fetched email: "${result.subject?.slice(0, 40)}..."`);
187
221
  });
188
222
 
223
+ // ─── Count & Bulk Query ───────────────────────────────────────────────────────
189
224
  console.log('\nšŸ” Count & Bulk Query');
190
225
 
191
226
  test('count_emails (all in INBOX)', () => {
@@ -201,12 +236,17 @@ test('count_emails (unread only)', () => {
201
236
  console.log(`\n → ${result.count} unread emails`);
202
237
  });
203
238
 
239
+ test('count_emails (read only)', () => {
240
+ const result = callTool('count_emails', { unread: false });
241
+ assert(typeof result.count === 'number', 'count should be a number');
242
+ console.log(`\n → ${result.count} read emails`);
243
+ });
244
+
204
245
  test('count_emails (by domain)', () => {
205
246
  const senders = callTool('get_top_senders', { sampleSize: 20 });
206
247
  const topDomain = senders.topDomains[0]?.domain;
207
248
  assert(topDomain, 'should have at least one domain');
208
249
  const result = callTool('count_emails', { domain: topDomain });
209
- console.log('\n → raw result:', JSON.stringify(result));
210
250
  assert(typeof result.count === 'number', 'count should be a number');
211
251
  console.log(`\n → ${result.count} emails from @${topDomain}`);
212
252
  });
@@ -217,6 +257,30 @@ test('count_emails (before date)', () => {
217
257
  console.log(`\n → ${result.count} emails before 2020`);
218
258
  });
219
259
 
260
+ test('count_emails (flagged false)', () => {
261
+ const result = callTool('count_emails', { flagged: false });
262
+ assert(typeof result.count === 'number', 'count should be a number');
263
+ console.log(`\n → ${result.count} unflagged emails`);
264
+ });
265
+
266
+ test('bulk_move (dryRun)', () => {
267
+ const senders = callTool('get_top_senders', { sampleSize: 20 });
268
+ const topDomain = senders.topDomains[0]?.domain;
269
+ assert(topDomain, 'should have at least one domain');
270
+ const result = callTool('bulk_move', { domain: topDomain, targetMailbox: 'Archive', dryRun: true });
271
+ assert(result.dryRun === true, 'dryRun should be true');
272
+ assert(typeof result.wouldMove === 'number', 'wouldMove should be a number');
273
+ console.log(`\n → would move ${result.wouldMove} emails from @${topDomain}`);
274
+ });
275
+
276
+ test('bulk_delete (dryRun)', () => {
277
+ const result = callTool('bulk_delete', { before: '2015-01-01', dryRun: true });
278
+ assert(result.dryRun === true, 'dryRun should be true');
279
+ assert(typeof result.wouldDelete === 'number', 'wouldDelete should be a number');
280
+ console.log(`\n → would delete ${result.wouldDelete} emails before 2015`);
281
+ });
282
+
283
+ // ─── Write Operations ─────────────────────────────────────────────────────────
220
284
  console.log('\nāœļø Write Operations (flag/mark only — no deletions)');
221
285
 
222
286
  test('flag_email and unflag_email', () => {
@@ -241,10 +305,35 @@ test('mark_as_read and mark_as_unread', () => {
241
305
  console.log(`\n → marked read/unread uid ${uid}`);
242
306
  });
243
307
 
308
+ // ─── Mailbox Management ───────────────────────────────────────────────────────
309
+ console.log('\nšŸ—‚ļø Mailbox Management');
310
+
311
+ test('create_mailbox', () => {
312
+ const result = callTool('create_mailbox', { name: 'mcp-test-folder' });
313
+ assert(result.created === 'mcp-test-folder', 'should confirm creation');
314
+ console.log(`\n → created: ${result.created}`);
315
+ });
316
+
317
+ test('rename_mailbox', () => {
318
+ const result = callTool('rename_mailbox', { oldName: 'mcp-test-folder', newName: 'mcp-test-folder-renamed' });
319
+ assert(result.renamed.from === 'mcp-test-folder', 'from should match old name');
320
+ assert(result.renamed.to === 'mcp-test-folder-renamed', 'to should match new name');
321
+ console.log(`\n → renamed: ${result.renamed.from} → ${result.renamed.to}`);
322
+ });
323
+
324
+ test('delete_mailbox', () => {
325
+ const result = callTool('delete_mailbox', { name: 'mcp-test-folder-renamed' });
326
+ assert(result.deleted === 'mcp-test-folder-renamed', 'should confirm deletion');
327
+ console.log(`\n → deleted: ${result.deleted}`);
328
+ });
329
+
330
+ // ─── Destructive (skipped) ────────────────────────────────────────────────────
244
331
  console.log('\nāš ļø Destructive Tests (skipped by default)');
245
- console.log(' Skipping: bulk_move (sender → target folder)');
246
- console.log(' Skipping: bulk_move (folder → folder)');
247
- console.log(' Skipping: bulk_delete (by domain)');
332
+ console.log(' Skipping: bulk_move (live)');
333
+ console.log(' Skipping: bulk_delete (live)');
334
+ console.log(' Skipping: bulk_mark_read (live)');
335
+ console.log(' Skipping: bulk_mark_unread (live)');
336
+ console.log(' Skipping: bulk_flag (live)');
248
337
  console.log(' Skipping: bulk_delete_by_sender');
249
338
  console.log(' Skipping: bulk_delete_by_subject');
250
339
  console.log(' Skipping: delete_older_than');