icloud-mcp 1.7.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.
- package/index.js +96 -6
- 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: [] };
|
|
@@ -1119,6 +1120,25 @@ function findTextPart(node) {
|
|
|
1119
1120
|
return null;
|
|
1120
1121
|
}
|
|
1121
1122
|
|
|
1123
|
+
function findAttachments(node, parts = []) {
|
|
1124
|
+
if (node.childNodes) {
|
|
1125
|
+
for (const child of node.childNodes) findAttachments(child, parts);
|
|
1126
|
+
} else {
|
|
1127
|
+
const filename = node.dispositionParameters?.filename ?? node.parameters?.name ?? null;
|
|
1128
|
+
const isTextBody = (node.type === 'text/plain' || node.type === 'text/html') && node.disposition !== 'attachment';
|
|
1129
|
+
if (node.disposition === 'attachment' || node.disposition === 'inline' || (filename && !isTextBody)) {
|
|
1130
|
+
parts.push({
|
|
1131
|
+
partId: node.part ?? 'TEXT',
|
|
1132
|
+
filename,
|
|
1133
|
+
mimeType: node.type ?? 'application/octet-stream',
|
|
1134
|
+
size: node.size ?? 0,
|
|
1135
|
+
disposition: node.disposition ?? 'attachment'
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return parts;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1122
1142
|
// ─── Email content fetcher (MIME-aware) ───────────────────────────────────────
|
|
1123
1143
|
|
|
1124
1144
|
async function getEmailContent(uid, mailbox = 'INBOX') {
|
|
@@ -1205,6 +1225,22 @@ async function getEmailContent(uid, mailbox = 'INBOX') {
|
|
|
1205
1225
|
};
|
|
1206
1226
|
}
|
|
1207
1227
|
|
|
1228
|
+
async function listAttachments(uid, mailbox = 'INBOX') {
|
|
1229
|
+
const client = createRateLimitedClient();
|
|
1230
|
+
await client.connect();
|
|
1231
|
+
await client.mailboxOpen(mailbox);
|
|
1232
|
+
const meta = await client.fetchOne(uid, { envelope: true, bodyStructure: true }, { uid: true });
|
|
1233
|
+
await client.logout();
|
|
1234
|
+
if (!meta) return { uid, subject: null, attachmentCount: 0, attachments: [] };
|
|
1235
|
+
const attachments = meta.bodyStructure ? findAttachments(meta.bodyStructure) : [];
|
|
1236
|
+
return {
|
|
1237
|
+
uid: meta.uid,
|
|
1238
|
+
subject: meta.envelope.subject,
|
|
1239
|
+
attachmentCount: attachments.length,
|
|
1240
|
+
attachments
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1208
1244
|
async function flagEmail(uid, flagged, mailbox = 'INBOX') {
|
|
1209
1245
|
const client = createRateLimitedClient();
|
|
1210
1246
|
await client.connect();
|
|
@@ -1267,7 +1303,14 @@ async function searchEmails(query, mailbox = 'INBOX', limit = 10, filters = {})
|
|
|
1267
1303
|
? { ...textQuery, ...extraQuery }
|
|
1268
1304
|
: textQuery;
|
|
1269
1305
|
|
|
1270
|
-
|
|
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
|
+
}
|
|
1271
1314
|
const emails = [];
|
|
1272
1315
|
const recentUids = uids.slice(-limit).reverse();
|
|
1273
1316
|
for (const uid of recentUids) {
|
|
@@ -1309,11 +1352,23 @@ function buildQuery(filters) {
|
|
|
1309
1352
|
if (filters.flagged === false) query.unflagged = true;
|
|
1310
1353
|
if (filters.larger) query.larger = filters.larger * 1024;
|
|
1311
1354
|
if (filters.smaller) query.smaller = filters.smaller * 1024;
|
|
1312
|
-
|
|
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
|
|
1313
1357
|
if (Object.keys(query).length === 0) query.all = true;
|
|
1314
1358
|
return query;
|
|
1315
1359
|
}
|
|
1316
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
|
+
|
|
1317
1372
|
async function ensureMailbox(name) {
|
|
1318
1373
|
const client = createRateLimitedClient();
|
|
1319
1374
|
await client.connect();
|
|
@@ -1327,6 +1382,13 @@ async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX', dryRun
|
|
|
1327
1382
|
await client.mailboxOpen(sourceMailbox);
|
|
1328
1383
|
const query = buildQuery(filters);
|
|
1329
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
|
+
}
|
|
1330
1392
|
await client.logout();
|
|
1331
1393
|
|
|
1332
1394
|
if (limit !== null) uids = uids.slice(0, limit);
|
|
@@ -1351,7 +1413,14 @@ async function bulkDelete(filters, sourceMailbox = 'INBOX', dryRun = false) {
|
|
|
1351
1413
|
await client.connect();
|
|
1352
1414
|
await client.mailboxOpen(sourceMailbox);
|
|
1353
1415
|
const query = buildQuery(filters);
|
|
1354
|
-
|
|
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
|
+
}
|
|
1355
1424
|
|
|
1356
1425
|
if (dryRun) {
|
|
1357
1426
|
await client.logout();
|
|
@@ -1388,7 +1457,14 @@ async function countEmails(filters, mailbox = 'INBOX') {
|
|
|
1388
1457
|
await client.connect();
|
|
1389
1458
|
await client.mailboxOpen(mailbox);
|
|
1390
1459
|
const query = buildQuery(filters);
|
|
1391
|
-
|
|
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
|
+
}
|
|
1392
1468
|
await client.logout();
|
|
1393
1469
|
return { count: uids.length, mailbox, filters };
|
|
1394
1470
|
}
|
|
@@ -1421,7 +1497,7 @@ function logClear() {
|
|
|
1421
1497
|
|
|
1422
1498
|
async function main() {
|
|
1423
1499
|
const server = new Server(
|
|
1424
|
-
{ name: 'icloud-mail', version: '1.
|
|
1500
|
+
{ name: 'icloud-mail', version: '1.8.1' },
|
|
1425
1501
|
{ capabilities: { tools: {} } }
|
|
1426
1502
|
);
|
|
1427
1503
|
|
|
@@ -1435,7 +1511,7 @@ async function main() {
|
|
|
1435
1511
|
flagged: { type: 'boolean', description: 'True for flagged only, false for unflagged only' },
|
|
1436
1512
|
larger: { type: 'number', description: 'Only emails larger than this size in KB' },
|
|
1437
1513
|
smaller: { type: 'number', description: 'Only emails smaller than this size in KB' },
|
|
1438
|
-
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)' }
|
|
1439
1515
|
};
|
|
1440
1516
|
|
|
1441
1517
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -1788,6 +1864,18 @@ async function main() {
|
|
|
1788
1864
|
name: 'log_clear',
|
|
1789
1865
|
description: 'Clear the session log and start fresh. Use this at the start of a new task.',
|
|
1790
1866
|
inputSchema: { type: 'object', properties: {} }
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
name: 'list_attachments',
|
|
1870
|
+
description: 'List all attachments in an email without downloading them. Returns filename, MIME type, size, and IMAP part ID for each attachment.',
|
|
1871
|
+
inputSchema: {
|
|
1872
|
+
type: 'object',
|
|
1873
|
+
properties: {
|
|
1874
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
1875
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }
|
|
1876
|
+
},
|
|
1877
|
+
required: ['uid']
|
|
1878
|
+
}
|
|
1791
1879
|
}
|
|
1792
1880
|
]
|
|
1793
1881
|
}));
|
|
@@ -1817,6 +1905,8 @@ async function main() {
|
|
|
1817
1905
|
result = await withTimeout('read_inbox', TIMEOUT.FETCH, () => fetchEmails(args.mailbox || 'INBOX', args.limit || 10, args.onlyUnread || false, args.page || 1));
|
|
1818
1906
|
} else if (name === 'get_email') {
|
|
1819
1907
|
result = await withTimeout('get_email', TIMEOUT.FETCH, () => getEmailContent(args.uid, args.mailbox || 'INBOX'));
|
|
1908
|
+
} else if (name === 'list_attachments') {
|
|
1909
|
+
result = await withTimeout('list_attachments', TIMEOUT.FETCH, () => listAttachments(args.uid, args.mailbox || 'INBOX'));
|
|
1820
1910
|
} else if (name === 'search_emails') {
|
|
1821
1911
|
const { query, mailbox, limit, ...filters } = args;
|
|
1822
1912
|
result = await withTimeout('search_emails', TIMEOUT.FETCH, () => searchEmails(query, mailbox || 'INBOX', limit || 10, filters));
|