icloud-mcp 1.8.0 → 1.8.1

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 (2) hide show
  1. package/index.js +47 -11
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -89,6 +89,7 @@ async function reconnect(client, mailbox) {
89
89
 
90
90
  const CHUNK_SIZE = 500;
91
91
  const CHUNK_SIZE_RETRY = 100;
92
+ const ATTACHMENT_SCAN_LIMIT = 500; // max UIDs to scan client-side for hasAttachment filter
92
93
 
93
94
  function readManifest() {
94
95
  if (!existsSync(MANIFEST_FILE)) return { current: null, history: [] };
@@ -1302,7 +1303,14 @@ async function searchEmails(query, mailbox = 'INBOX', limit = 10, filters = {})
1302
1303
  ? { ...textQuery, ...extraQuery }
1303
1304
  : textQuery;
1304
1305
 
1305
- const uids = (await client.search(finalQuery, { uid: true })) ?? [];
1306
+ let uids = (await client.search(finalQuery, { uid: true })) ?? [];
1307
+ if (filters.hasAttachment) {
1308
+ if (uids.length > ATTACHMENT_SCAN_LIMIT) {
1309
+ await client.logout();
1310
+ return { total: null, showing: 0, emails: [], error: `hasAttachment requires narrower filters first — ${uids.length} candidates exceeds scan limit of ${ATTACHMENT_SCAN_LIMIT}. Add from/since/before/subject filters to reduce the set.` };
1311
+ }
1312
+ uids = await filterUidsByAttachment(client, uids);
1313
+ }
1306
1314
  const emails = [];
1307
1315
  const recentUids = uids.slice(-limit).reverse();
1308
1316
  for (const uid of recentUids) {
@@ -1344,16 +1352,23 @@ function buildQuery(filters) {
1344
1352
  if (filters.flagged === false) query.unflagged = true;
1345
1353
  if (filters.larger) query.larger = filters.larger * 1024;
1346
1354
  if (filters.smaller) query.smaller = filters.smaller * 1024;
1347
- if (filters.hasAttachment) {
1348
- query.or = [
1349
- { header: { 'Content-Disposition': 'attachment' } },
1350
- { header: { 'Content-Type': 'multipart/mixed' } }
1351
- ];
1352
- }
1355
+ // hasAttachment is handled as a client-side post-filter (see filterUidsByAttachment)
1356
+ // iCloud does not support SEARCH HEADER or reliable size-based attachment detection
1353
1357
  if (Object.keys(query).length === 0) query.all = true;
1354
1358
  return query;
1355
1359
  }
1356
1360
 
1361
+ async function filterUidsByAttachment(client, uids) {
1362
+ if (uids.length === 0) return [];
1363
+ const result = [];
1364
+ for await (const msg of client.fetch(uids, { bodyStructure: true }, { uid: true })) {
1365
+ if (msg.bodyStructure && findAttachments(msg.bodyStructure).length > 0) {
1366
+ result.push(msg.uid);
1367
+ }
1368
+ }
1369
+ return result;
1370
+ }
1371
+
1357
1372
  async function ensureMailbox(name) {
1358
1373
  const client = createRateLimitedClient();
1359
1374
  await client.connect();
@@ -1367,6 +1382,13 @@ async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX', dryRun
1367
1382
  await client.mailboxOpen(sourceMailbox);
1368
1383
  const query = buildQuery(filters);
1369
1384
  let uids = (await client.search(query, { uid: true })) ?? [];
1385
+ if (filters.hasAttachment) {
1386
+ if (uids.length > ATTACHMENT_SCAN_LIMIT) {
1387
+ await client.logout();
1388
+ return { error: `hasAttachment requires narrower filters first — ${uids.length} candidates exceeds scan limit of ${ATTACHMENT_SCAN_LIMIT}.` };
1389
+ }
1390
+ uids = await filterUidsByAttachment(client, uids);
1391
+ }
1370
1392
  await client.logout();
1371
1393
 
1372
1394
  if (limit !== null) uids = uids.slice(0, limit);
@@ -1391,7 +1413,14 @@ async function bulkDelete(filters, sourceMailbox = 'INBOX', dryRun = false) {
1391
1413
  await client.connect();
1392
1414
  await client.mailboxOpen(sourceMailbox);
1393
1415
  const query = buildQuery(filters);
1394
- const uids = (await client.search(query, { uid: true })) ?? [];
1416
+ let uids = (await client.search(query, { uid: true })) ?? [];
1417
+ if (filters.hasAttachment) {
1418
+ if (uids.length > ATTACHMENT_SCAN_LIMIT) {
1419
+ await client.logout();
1420
+ return { error: `hasAttachment requires narrower filters first — ${uids.length} candidates exceeds scan limit of ${ATTACHMENT_SCAN_LIMIT}.` };
1421
+ }
1422
+ uids = await filterUidsByAttachment(client, uids);
1423
+ }
1395
1424
 
1396
1425
  if (dryRun) {
1397
1426
  await client.logout();
@@ -1428,7 +1457,14 @@ async function countEmails(filters, mailbox = 'INBOX') {
1428
1457
  await client.connect();
1429
1458
  await client.mailboxOpen(mailbox);
1430
1459
  const query = buildQuery(filters);
1431
- const uids = (await client.search(query, { uid: true })) ?? [];
1460
+ let uids = (await client.search(query, { uid: true })) ?? [];
1461
+ if (filters.hasAttachment) {
1462
+ if (uids.length > ATTACHMENT_SCAN_LIMIT) {
1463
+ await client.logout();
1464
+ return { count: null, mailbox, filters, error: `hasAttachment requires narrower filters first — ${uids.length} candidates exceeds scan limit of ${ATTACHMENT_SCAN_LIMIT}. Add from/since/before/subject filters to reduce the set.` };
1465
+ }
1466
+ uids = await filterUidsByAttachment(client, uids);
1467
+ }
1432
1468
  await client.logout();
1433
1469
  return { count: uids.length, mailbox, filters };
1434
1470
  }
@@ -1461,7 +1497,7 @@ function logClear() {
1461
1497
 
1462
1498
  async function main() {
1463
1499
  const server = new Server(
1464
- { name: 'icloud-mail', version: '1.8.0' },
1500
+ { name: 'icloud-mail', version: '1.8.1' },
1465
1501
  { capabilities: { tools: {} } }
1466
1502
  );
1467
1503
 
@@ -1475,7 +1511,7 @@ async function main() {
1475
1511
  flagged: { type: 'boolean', description: 'True for flagged only, false for unflagged only' },
1476
1512
  larger: { type: 'number', description: 'Only emails larger than this size in KB' },
1477
1513
  smaller: { type: 'number', description: 'Only emails smaller than this size in KB' },
1478
- hasAttachment: { type: 'boolean', description: 'Only emails with attachments' }
1514
+ hasAttachment: { type: 'boolean', description: 'Only emails with attachments (client-side BODYSTRUCTURE scan — must be combined with other filters that narrow results to under 500 emails first)' }
1479
1515
  };
1480
1516
 
1481
1517
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icloud-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "A Model Context Protocol (MCP) server for iCloud Mail",
5
5
  "main": "index.js",
6
6
  "bin": {