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.
Files changed (2) hide show
  1. package/index.js +77 -1
  2. 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.8.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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icloud-mcp",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "A Model Context Protocol (MCP) server for iCloud Mail",
5
5
  "main": "index.js",
6
6
  "bin": {