icloud-mcp 1.2.0 → 1.2.2

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 (3) hide show
  1. package/index.js +60 -22
  2. package/package.json +1 -1
  3. package/test.js +32 -2
package/index.js CHANGED
@@ -166,9 +166,15 @@ async function bulkDeleteBySender(sender, mailbox = 'INBOX') {
166
166
  await client.mailboxOpen(mailbox);
167
167
  const uids = (await client.search({ from: sender }, { uid: true })) ?? [];
168
168
  if (uids.length === 0) { await client.logout(); return { deleted: 0 }; }
169
- await client.messageDelete(uids, { uid: true });
169
+ const chunkSize = 250;
170
+ let deleted = 0;
171
+ for (let i = 0; i < uids.length; i += chunkSize) {
172
+ const chunk = uids.slice(i, i + chunkSize);
173
+ await client.messageDelete(chunk, { uid: true });
174
+ deleted += chunk.length;
175
+ }
170
176
  await client.logout();
171
- return { deleted: uids.length, sender };
177
+ return { deleted, sender };
172
178
  }
173
179
 
174
180
  async function bulkMoveBySender(sender, targetMailbox, sourceMailbox = 'INBOX') {
@@ -177,9 +183,15 @@ async function bulkMoveBySender(sender, targetMailbox, sourceMailbox = 'INBOX')
177
183
  await client.mailboxOpen(sourceMailbox);
178
184
  const uids = (await client.search({ from: sender }, { uid: true })) ?? [];
179
185
  if (uids.length === 0) { await client.logout(); return { moved: 0 }; }
180
- await client.messageMove(uids, targetMailbox, { uid: true });
186
+ const chunkSize = 250;
187
+ let moved = 0;
188
+ for (let i = 0; i < uids.length; i += chunkSize) {
189
+ const chunk = uids.slice(i, i + chunkSize);
190
+ await client.messageMove(chunk, targetMailbox, { uid: true });
191
+ moved += chunk.length;
192
+ }
181
193
  await client.logout();
182
- return { moved: uids.length, sender, targetMailbox };
194
+ return { moved, sender, targetMailbox };
183
195
  }
184
196
 
185
197
  async function bulkDeleteBySubject(subject, mailbox = 'INBOX') {
@@ -188,9 +200,15 @@ async function bulkDeleteBySubject(subject, mailbox = 'INBOX') {
188
200
  await client.mailboxOpen(mailbox);
189
201
  const uids = (await client.search({ subject }, { uid: true })) ?? [];
190
202
  if (uids.length === 0) { await client.logout(); return { deleted: 0 }; }
191
- await client.messageDelete(uids, { uid: true });
203
+ const chunkSize = 250;
204
+ let deleted = 0;
205
+ for (let i = 0; i < uids.length; i += chunkSize) {
206
+ const chunk = uids.slice(i, i + chunkSize);
207
+ await client.messageDelete(chunk, { uid: true });
208
+ deleted += chunk.length;
209
+ }
192
210
  await client.logout();
193
- return { deleted: uids.length, subject };
211
+ return { deleted, subject };
194
212
  }
195
213
 
196
214
  async function deleteOlderThan(days, mailbox = 'INBOX') {
@@ -201,9 +219,15 @@ async function deleteOlderThan(days, mailbox = 'INBOX') {
201
219
  date.setDate(date.getDate() - days);
202
220
  const uids = (await client.search({ before: date }, { uid: true })) ?? [];
203
221
  if (uids.length === 0) { await client.logout(); return { deleted: 0 }; }
204
- await client.messageDelete(uids, { uid: true });
222
+ const chunkSize = 250;
223
+ let deleted = 0;
224
+ for (let i = 0; i < uids.length; i += chunkSize) {
225
+ const chunk = uids.slice(i, i + chunkSize);
226
+ await client.messageDelete(chunk, { uid: true });
227
+ deleted += chunk.length;
228
+ }
205
229
  await client.logout();
206
- return { deleted: uids.length, olderThan: date.toISOString() };
230
+ return { deleted, olderThan: date.toISOString() };
207
231
  }
208
232
 
209
233
  async function getEmailsByDateRange(startDate, endDate, mailbox = 'INBOX', limit = 10) {
@@ -277,9 +301,15 @@ async function emptyTrash() {
277
301
  await client.mailboxOpen('Deleted Messages');
278
302
  const uids = (await client.search({ all: true }, { uid: true })) ?? [];
279
303
  if (uids.length === 0) { await client.logout(); return { deleted: 0 }; }
280
- await client.messageDelete(uids, { uid: true });
304
+ const chunkSize = 250;
305
+ let deleted = 0;
306
+ for (let i = 0; i < uids.length; i += chunkSize) {
307
+ const chunk = uids.slice(i, i + chunkSize);
308
+ await client.messageDelete(chunk, { uid: true });
309
+ deleted += chunk.length;
310
+ }
281
311
  await client.logout();
282
- return { deleted: uids.length };
312
+ return { deleted };
283
313
  }
284
314
 
285
315
  async function createMailbox(name) {
@@ -400,10 +430,7 @@ async function searchEmails(query, mailbox = 'INBOX', limit = 10, filters = {})
400
430
  await client.connect();
401
431
  await client.mailboxOpen(mailbox);
402
432
 
403
- // Build base text search
404
433
  const textQuery = { or: [{ subject: query }, { from: query }, { body: query }] };
405
-
406
- // Merge with additional filters if provided
407
434
  const extraQuery = buildQuery(filters);
408
435
  const finalQuery = Object.keys(extraQuery).length > 0 && !extraQuery.all
409
436
  ? { ...textQuery, ...extraQuery }
@@ -438,7 +465,6 @@ async function moveEmail(uid, targetMailbox, sourceMailbox = 'INBOX') {
438
465
  return true;
439
466
  }
440
467
 
441
- // Build an IMAP search query from a filters object
442
468
  function buildQuery(filters) {
443
469
  const query = {};
444
470
  if (filters.sender) query.from = filters.sender;
@@ -468,9 +494,15 @@ async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX', dryRun
468
494
  return { dryRun: true, wouldMove: uids.length, sourceMailbox, targetMailbox, filters };
469
495
  }
470
496
  if (uids.length === 0) { await client.logout(); return { moved: 0, sourceMailbox, targetMailbox }; }
471
- await client.messageMove(uids, targetMailbox, { uid: true });
497
+ const chunkSize = 250;
498
+ let moved = 0;
499
+ for (let i = 0; i < uids.length; i += chunkSize) {
500
+ const chunk = uids.slice(i, i + chunkSize);
501
+ await client.messageMove(chunk, targetMailbox, { uid: true });
502
+ moved += chunk.length;
503
+ }
472
504
  await client.logout();
473
- return { moved: uids.length, sourceMailbox, targetMailbox, filters };
505
+ return { moved, sourceMailbox, targetMailbox, filters };
474
506
  }
475
507
 
476
508
  async function bulkDelete(filters, sourceMailbox = 'INBOX', dryRun = false) {
@@ -484,9 +516,15 @@ async function bulkDelete(filters, sourceMailbox = 'INBOX', dryRun = false) {
484
516
  return { dryRun: true, wouldDelete: uids.length, sourceMailbox, filters };
485
517
  }
486
518
  if (uids.length === 0) { await client.logout(); return { deleted: 0, sourceMailbox }; }
487
- await client.messageDelete(uids, { uid: true });
519
+ const chunkSize = 250;
520
+ let deleted = 0;
521
+ for (let i = 0; i < uids.length; i += chunkSize) {
522
+ const chunk = uids.slice(i, i + chunkSize);
523
+ await client.messageDelete(chunk, { uid: true });
524
+ deleted += chunk.length;
525
+ }
488
526
  await client.logout();
489
- return { deleted: uids.length, sourceMailbox, filters };
527
+ return { deleted, sourceMailbox, filters };
490
528
  }
491
529
 
492
530
  async function countEmails(filters, mailbox = 'INBOX') {
@@ -501,7 +539,7 @@ async function countEmails(filters, mailbox = 'INBOX') {
501
539
 
502
540
  async function main() {
503
541
  const server = new Server(
504
- { name: 'icloud-mail', version: '1.2.0' },
542
+ { name: 'icloud-mail', version: '1.3.0' },
505
543
  { capabilities: { tools: {} } }
506
544
  );
507
545
 
@@ -625,7 +663,7 @@ async function main() {
625
663
  },
626
664
  {
627
665
  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.',
666
+ description: 'Move emails matching any combination of filters from one mailbox to another. Processes in chunks of 250 for reliability. Use dryRun: true to preview without making changes.',
629
667
  inputSchema: {
630
668
  type: 'object',
631
669
  properties: {
@@ -639,7 +677,7 @@ async function main() {
639
677
  },
640
678
  {
641
679
  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.',
680
+ description: 'Delete emails matching any combination of filters. Processes in chunks of 250 for reliability. Use dryRun: true to preview without making changes.',
643
681
  inputSchema: {
644
682
  type: 'object',
645
683
  properties: {
@@ -937,4 +975,4 @@ process.on('unhandledRejection', (reason) => {
937
975
  main().catch((err) => {
938
976
  process.stderr.write(`Fatal error: ${err.message}\n${err.stack}\n`);
939
977
  process.exit(1);
940
- });
978
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icloud-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "A Model Context Protocol (MCP) server for iCloud Mail",
5
5
  "main": "index.js",
6
6
  "bin": {
package/test.js CHANGED
@@ -327,9 +327,39 @@ test('delete_mailbox', () => {
327
327
  console.log(`\n → deleted: ${result.deleted}`);
328
328
  });
329
329
 
330
+ // ─── Chunk Move Test (live) ───────────────────────────────────────────────────
331
+ console.log('\n📦 Chunk Move Test (live — newsletters ↔ test)');
332
+
333
+ test('bulk_move newsletters → test (chunked)', () => {
334
+ const beforeSource = callTool('count_emails', { mailbox: 'newsletters' });
335
+ assert(beforeSource.count > 0, 'newsletters should have emails');
336
+ console.log(`\n → newsletters before: ${beforeSource.count}`);
337
+
338
+ const moveResult = callTool('bulk_move', { sourceMailbox: 'newsletters', targetMailbox: 'test' });
339
+ console.log(`\n → moved: ${moveResult.moved}`);
340
+ assert(moveResult.moved === beforeSource.count, `moved count should match source (expected ${beforeSource.count}, got ${moveResult.moved})`);
341
+
342
+ const afterTarget = callTool('count_emails', { mailbox: 'test' });
343
+ console.log(`\n → test folder after: ${afterTarget.count}`);
344
+ assert(afterTarget.count === beforeSource.count, `test folder should have all ${beforeSource.count} emails`);
345
+ });
346
+
347
+ test('bulk_move test → newsletters (restore)', () => {
348
+ const beforeSource = callTool('count_emails', { mailbox: 'test' });
349
+ assert(beforeSource.count > 0, 'test folder should have emails to restore');
350
+ console.log(`\n → test folder before restore: ${beforeSource.count}`);
351
+
352
+ const moveBack = callTool('bulk_move', { sourceMailbox: 'test', targetMailbox: 'newsletters' });
353
+ console.log(`\n → moved back: ${moveBack.moved}`);
354
+ assert(moveBack.moved === beforeSource.count, `should move all ${beforeSource.count} emails back`);
355
+
356
+ const afterRestore = callTool('count_emails', { mailbox: 'newsletters' });
357
+ console.log(`\n → newsletters restored: ${afterRestore.count}`);
358
+ assert(afterRestore.count === beforeSource.count, 'newsletters should be fully restored');
359
+ });
360
+
330
361
  // ─── Destructive (skipped) ────────────────────────────────────────────────────
331
362
  console.log('\n⚠️ Destructive Tests (skipped by default)');
332
- console.log(' Skipping: bulk_move (live)');
333
363
  console.log(' Skipping: bulk_delete (live)');
334
364
  console.log(' Skipping: bulk_mark_read (live)');
335
365
  console.log(' Skipping: bulk_mark_unread (live)');
@@ -346,4 +376,4 @@ console.log(`\n✅ Passed: ${passed}`);
346
376
  console.log(`❌ Failed: ${failed}`);
347
377
  console.log(`📊 Total: ${passed + failed}\n`);
348
378
 
349
- if (failed > 0) process.exit(1);
379
+ if (failed > 0) process.exit(1);