icloud-mcp 1.8.1 → 1.9.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.
- package/index.js +77 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -90,6 +90,7 @@ async function reconnect(client, mailbox) {
|
|
|
90
90
|
const CHUNK_SIZE = 500;
|
|
91
91
|
const CHUNK_SIZE_RETRY = 100;
|
|
92
92
|
const ATTACHMENT_SCAN_LIMIT = 500; // max UIDs to scan client-side for hasAttachment filter
|
|
93
|
+
const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024; // 20 MB cap for get_attachment downloads
|
|
93
94
|
|
|
94
95
|
function readManifest() {
|
|
95
96
|
if (!existsSync(MANIFEST_FILE)) return { current: null, history: [] };
|
|
@@ -1132,6 +1133,7 @@ function findAttachments(node, parts = []) {
|
|
|
1132
1133
|
filename,
|
|
1133
1134
|
mimeType: node.type ?? 'application/octet-stream',
|
|
1134
1135
|
size: node.size ?? 0,
|
|
1136
|
+
encoding: node.encoding ?? '7bit',
|
|
1135
1137
|
disposition: node.disposition ?? 'attachment'
|
|
1136
1138
|
});
|
|
1137
1139
|
}
|
|
@@ -1241,6 +1243,65 @@ async function listAttachments(uid, mailbox = 'INBOX') {
|
|
|
1241
1243
|
};
|
|
1242
1244
|
}
|
|
1243
1245
|
|
|
1246
|
+
async function getAttachment(uid, partId, mailbox = 'INBOX') {
|
|
1247
|
+
const client = createRateLimitedClient();
|
|
1248
|
+
await client.connect();
|
|
1249
|
+
await client.mailboxOpen(mailbox);
|
|
1250
|
+
|
|
1251
|
+
// First fetch bodyStructure to find the attachment and validate size
|
|
1252
|
+
const meta = await client.fetchOne(uid, { bodyStructure: true }, { uid: true });
|
|
1253
|
+
if (!meta) throw new Error(`Email UID ${uid} not found`);
|
|
1254
|
+
|
|
1255
|
+
const attachments = meta.bodyStructure ? findAttachments(meta.bodyStructure) : [];
|
|
1256
|
+
const att = attachments.find(a => a.partId === partId);
|
|
1257
|
+
if (!att) throw new Error(`Part ID "${partId}" not found in email UID ${uid}. Use list_attachments to see available parts.`);
|
|
1258
|
+
|
|
1259
|
+
if (att.size > MAX_ATTACHMENT_BYTES) {
|
|
1260
|
+
await client.logout();
|
|
1261
|
+
return {
|
|
1262
|
+
error: `Attachment too large to download (${Math.round(att.size / 1024 / 1024 * 10) / 10} MB). Maximum allowed: ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB.`,
|
|
1263
|
+
filename: att.filename,
|
|
1264
|
+
mimeType: att.mimeType,
|
|
1265
|
+
size: att.size
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Fetch the raw body part bytes
|
|
1270
|
+
const rawChunks = [];
|
|
1271
|
+
for await (const msg of client.fetch({ uid }, { bodyParts: [partId] }, { uid: true })) {
|
|
1272
|
+
const buf = msg.bodyParts?.get(partId);
|
|
1273
|
+
if (buf) rawChunks.push(buf);
|
|
1274
|
+
}
|
|
1275
|
+
await client.logout();
|
|
1276
|
+
|
|
1277
|
+
if (rawChunks.length === 0) throw new Error(`No data returned for part "${partId}" of UID ${uid}`);
|
|
1278
|
+
|
|
1279
|
+
const raw = Buffer.concat(rawChunks);
|
|
1280
|
+
const encoding = att.encoding.toLowerCase();
|
|
1281
|
+
|
|
1282
|
+
let decoded;
|
|
1283
|
+
if (encoding === 'base64') {
|
|
1284
|
+
decoded = Buffer.from(raw.toString('ascii').replace(/\s/g, ''), 'base64');
|
|
1285
|
+
} else if (encoding === 'quoted-printable') {
|
|
1286
|
+
// decode QP: replace soft line breaks then decode =XX sequences
|
|
1287
|
+
const qp = raw.toString('binary').replace(/=\r?\n/g, '').replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
|
|
1288
|
+
decoded = Buffer.from(qp, 'binary');
|
|
1289
|
+
} else {
|
|
1290
|
+
decoded = raw; // 7bit / 8bit / binary — use as-is
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return {
|
|
1294
|
+
uid,
|
|
1295
|
+
partId,
|
|
1296
|
+
filename: att.filename,
|
|
1297
|
+
mimeType: att.mimeType,
|
|
1298
|
+
size: decoded.length,
|
|
1299
|
+
encoding: att.encoding,
|
|
1300
|
+
data: decoded.toString('base64'),
|
|
1301
|
+
dataEncoding: 'base64'
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1244
1305
|
async function flagEmail(uid, flagged, mailbox = 'INBOX') {
|
|
1245
1306
|
const client = createRateLimitedClient();
|
|
1246
1307
|
await client.connect();
|
|
@@ -1497,7 +1558,7 @@ function logClear() {
|
|
|
1497
1558
|
|
|
1498
1559
|
async function main() {
|
|
1499
1560
|
const server = new Server(
|
|
1500
|
-
{ name: 'icloud-mail', version: '1.
|
|
1561
|
+
{ name: 'icloud-mail', version: '1.9.0' },
|
|
1501
1562
|
{ capabilities: { tools: {} } }
|
|
1502
1563
|
);
|
|
1503
1564
|
|
|
@@ -1876,6 +1937,19 @@ async function main() {
|
|
|
1876
1937
|
},
|
|
1877
1938
|
required: ['uid']
|
|
1878
1939
|
}
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
name: 'get_attachment',
|
|
1943
|
+
description: 'Download a specific attachment from an email. Returns the file content as base64-encoded data. Use list_attachments first to get the partId. Maximum 20 MB per attachment.',
|
|
1944
|
+
inputSchema: {
|
|
1945
|
+
type: 'object',
|
|
1946
|
+
properties: {
|
|
1947
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
1948
|
+
partId: { type: 'string', description: 'IMAP body part ID from list_attachments (e.g. "2", "1.2")' },
|
|
1949
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }
|
|
1950
|
+
},
|
|
1951
|
+
required: ['uid', 'partId']
|
|
1952
|
+
}
|
|
1879
1953
|
}
|
|
1880
1954
|
]
|
|
1881
1955
|
}));
|
|
@@ -1907,6 +1981,8 @@ async function main() {
|
|
|
1907
1981
|
result = await withTimeout('get_email', TIMEOUT.FETCH, () => getEmailContent(args.uid, args.mailbox || 'INBOX'));
|
|
1908
1982
|
} else if (name === 'list_attachments') {
|
|
1909
1983
|
result = await withTimeout('list_attachments', TIMEOUT.FETCH, () => listAttachments(args.uid, args.mailbox || 'INBOX'));
|
|
1984
|
+
} else if (name === 'get_attachment') {
|
|
1985
|
+
result = await withTimeout('get_attachment', TIMEOUT.FETCH, () => getAttachment(args.uid, args.partId, args.mailbox || 'INBOX'));
|
|
1910
1986
|
} else if (name === 'search_emails') {
|
|
1911
1987
|
const { query, mailbox, limit, ...filters } = args;
|
|
1912
1988
|
result = await withTimeout('search_emails', TIMEOUT.FETCH, () => searchEmails(query, mailbox || 'INBOX', limit || 10, filters));
|