mcp-icloud 2.1.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.
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Local Contacts Client
3
+ * Accesses Contacts.app via AppleScript
4
+ */
5
+
6
+ const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt } = require('../utils/applescript');
7
+ const config = require('../config');
8
+
9
+ /**
10
+ * List contacts
11
+ * @param {number} count - Number of contacts to retrieve
12
+ * @returns {Promise<Array>} - List of contacts
13
+ */
14
+ async function listContacts(count = 25) {
15
+ const script = `
16
+ const contacts = Application('Contacts');
17
+ const people = contacts.people();
18
+ let result = [];
19
+
20
+ const limit = Math.min(${asInt(count, 25)}, people.length);
21
+ for (let i = 0; i < limit; i++) {
22
+ const person = people[i];
23
+ try {
24
+ const emails = person.emails();
25
+ const phones = person.phones();
26
+
27
+ let emailList = [];
28
+ for (let e of emails) { emailList.push(e.value()); }
29
+ let phoneList = [];
30
+ for (let p of phones) { phoneList.push(p.value()); }
31
+
32
+ result.push({
33
+ id: person.id(),
34
+ name: person.name() || '',
35
+ firstName: person.firstName() || '',
36
+ lastName: person.lastName() || '',
37
+ organization: person.organization() || '',
38
+ jobTitle: person.jobTitle() || '',
39
+ emails: emailList,
40
+ phones: phoneList,
41
+ email: emailList.length > 0 ? emailList[0] : '',
42
+ phone: phoneList.length > 0 ? phoneList[0] : ''
43
+ });
44
+ } catch (e) {}
45
+ }
46
+
47
+ JSON.stringify(result);
48
+ `;
49
+
50
+ const result = await runJXA(script);
51
+ return result ? JSON.parse(result) : [];
52
+ }
53
+
54
+ /**
55
+ * Search contacts
56
+ * @param {string} query - Search query
57
+ * @param {number} count - Max results
58
+ * @returns {Promise<Array>} - Matching contacts
59
+ */
60
+ async function searchContacts(query, count = 25) {
61
+ const searchTerm = escapeJXA(query.toLowerCase());
62
+
63
+ const script = `
64
+ const contacts = Application('Contacts');
65
+ const people = contacts.people();
66
+ let result = [];
67
+
68
+ for (let person of people) {
69
+ if (result.length >= ${asInt(count, 25)}) break;
70
+
71
+ try {
72
+ const name = (person.name() || '').toLowerCase();
73
+ const org = (person.organization() || '').toLowerCase();
74
+ const emails = person.emails();
75
+ const phones = person.phones();
76
+
77
+ let emailMatch = false;
78
+ for (let e of emails) {
79
+ if (e.value().toLowerCase().includes("${searchTerm}")) {
80
+ emailMatch = true;
81
+ break;
82
+ }
83
+ }
84
+
85
+ // Normalize: strip non-digits for phone comparison
86
+ const searchDigits = "${searchTerm}".replace(/[^0-9]/g, '');
87
+ let phoneMatch = false;
88
+ for (let p of phones) {
89
+ const phoneDigits = p.value().replace(/[^0-9]/g, '');
90
+ if (p.value().includes("${searchTerm}") || (searchDigits.length >= 6 && phoneDigits.includes(searchDigits))) {
91
+ phoneMatch = true;
92
+ break;
93
+ }
94
+ }
95
+
96
+ if (name.includes("${searchTerm}") || org.includes("${searchTerm}") || emailMatch || phoneMatch) {
97
+ let emailList = [];
98
+ for (let e of emails) { emailList.push(e.value()); }
99
+ let phoneList = [];
100
+ for (let p of phones) { phoneList.push(p.value()); }
101
+
102
+ result.push({
103
+ id: person.id(),
104
+ name: person.name() || '',
105
+ firstName: person.firstName() || '',
106
+ lastName: person.lastName() || '',
107
+ organization: person.organization() || '',
108
+ jobTitle: person.jobTitle() || '',
109
+ emails: emailList,
110
+ phones: phoneList,
111
+ email: emailList.length > 0 ? emailList[0] : '',
112
+ phone: phoneList.length > 0 ? phoneList[0] : ''
113
+ });
114
+ }
115
+ } catch (e) {}
116
+ }
117
+
118
+ JSON.stringify(result);
119
+ `;
120
+
121
+ const result = await runJXA(script);
122
+ return result ? JSON.parse(result) : [];
123
+ }
124
+
125
+ /**
126
+ * Read a specific contact
127
+ * @param {string} contactId - Contact ID
128
+ * @returns {Promise<Object>} - Contact details
129
+ */
130
+ async function readContact(contactId) {
131
+ const script = `
132
+ const contacts = Application('Contacts');
133
+ const person = contacts.people.byId("${escapeJXA(contactId)}");
134
+
135
+ const emails = person.emails();
136
+ const phones = person.phones();
137
+ const addresses = person.addresses();
138
+
139
+ let emailList = [];
140
+ for (let e of emails) {
141
+ emailList.push({ label: e.label() || 'email', value: e.value() });
142
+ }
143
+
144
+ let phoneList = [];
145
+ for (let p of phones) {
146
+ phoneList.push({ label: p.label() || 'phone', value: p.value() });
147
+ }
148
+
149
+ let addressList = [];
150
+ for (let a of addresses) {
151
+ addressList.push({
152
+ label: a.label() || 'address',
153
+ street: a.street() || '',
154
+ city: a.city() || '',
155
+ state: a.state() || '',
156
+ zip: a.zip() || '',
157
+ country: a.country() || ''
158
+ });
159
+ }
160
+
161
+ JSON.stringify({
162
+ id: person.id(),
163
+ name: person.name() || '',
164
+ firstName: person.firstName() || '',
165
+ lastName: person.lastName() || '',
166
+ organization: person.organization() || '',
167
+ jobTitle: person.jobTitle() || '',
168
+ department: person.department() || '',
169
+ note: person.note() || '',
170
+ birthday: person.birthDate() ? person.birthDate().toISOString() : null,
171
+ emails: emailList,
172
+ phones: phoneList,
173
+ addresses: addressList
174
+ });
175
+ `;
176
+
177
+ const result = await runJXA(script);
178
+ return result ? JSON.parse(result) : null;
179
+ }
180
+
181
+ /**
182
+ * Create a new contact
183
+ * @param {Object} options - Contact options
184
+ * @returns {Promise<Object>} - Created contact info
185
+ */
186
+ async function createContact({ displayName, firstName, lastName, organization, jobTitle, email, phone, note }) {
187
+ let properties = [];
188
+
189
+ if (firstName) properties.push(`first name:"${escapeAppleScript(firstName)}"`);
190
+ if (lastName) properties.push(`last name:"${escapeAppleScript(lastName)}"`);
191
+ if (organization) properties.push(`organization:"${escapeAppleScript(organization)}"`);
192
+ if (jobTitle) properties.push(`job title:"${escapeAppleScript(jobTitle)}"`);
193
+ if (note) properties.push(`note:"${escapeAppleScript(note)}"`);
194
+
195
+ let script = `
196
+ tell application "Contacts"
197
+ set newPerson to make new person with properties {${properties.join(', ')}}
198
+ `;
199
+
200
+ if (email) {
201
+ script += `
202
+ tell newPerson
203
+ make new email at end of emails with properties {label:"work", value:"${escapeAppleScript(email)}"}
204
+ end tell
205
+ `;
206
+ }
207
+
208
+ if (phone) {
209
+ script += `
210
+ tell newPerson
211
+ make new phone at end of phones with properties {label:"mobile", value:"${escapeAppleScript(phone)}"}
212
+ end tell
213
+ `;
214
+ }
215
+
216
+ script += `
217
+ save
218
+ return id of newPerson
219
+ end tell
220
+ `;
221
+
222
+ const id = await runAppleScript(script);
223
+ return { success: true, id, message: 'Contact created successfully' };
224
+ }
225
+
226
+ /**
227
+ * Update a contact
228
+ * @param {string} contactId - Contact ID
229
+ * @param {Object} updates - Fields to update
230
+ * @returns {Promise<Object>} - Result
231
+ */
232
+ async function updateContact(contactId, { firstName, lastName, organization, jobTitle, email, phone, note }) {
233
+ let updateCommands = [];
234
+
235
+ if (firstName !== undefined) updateCommands.push(`set first name of thePerson to "${escapeAppleScript(firstName || '')}"`);
236
+ if (lastName !== undefined) updateCommands.push(`set last name of thePerson to "${escapeAppleScript(lastName || '')}"`);
237
+ if (organization !== undefined) updateCommands.push(`set organization of thePerson to "${escapeAppleScript(organization || '')}"`);
238
+ if (jobTitle !== undefined) updateCommands.push(`set job title of thePerson to "${escapeAppleScript(jobTitle || '')}"`);
239
+ if (note !== undefined) updateCommands.push(`set note of thePerson to "${escapeAppleScript(note || '')}"`);
240
+
241
+ const script = `
242
+ tell application "Contacts"
243
+ set thePerson to person id "${escapeAppleScript(contactId)}"
244
+ ${updateCommands.join('\n ')}
245
+ save
246
+ end tell
247
+ return "updated"
248
+ `;
249
+
250
+ await runAppleScript(script);
251
+ return { success: true, message: 'Contact updated successfully' };
252
+ }
253
+
254
+ /**
255
+ * Delete a contact
256
+ * @param {string} contactId - Contact ID
257
+ * @returns {Promise<Object>} - Result
258
+ */
259
+ async function deleteContact(contactId) {
260
+ const script = `
261
+ tell application "Contacts"
262
+ set thePerson to person id "${escapeAppleScript(contactId)}"
263
+ delete thePerson
264
+ save
265
+ end tell
266
+ return "deleted"
267
+ `;
268
+
269
+ await runAppleScript(script);
270
+ return { success: true, message: 'Contact deleted successfully' };
271
+ }
272
+
273
+ /**
274
+ * List all contact accounts (iCloud, Google, etc.)
275
+ * @returns {Promise<Array>} - List of accounts
276
+ */
277
+ async function listAccounts() {
278
+ const script = `
279
+ const contacts = Application('Contacts');
280
+ const accounts = contacts.accounts();
281
+ let result = [];
282
+
283
+ for (let account of accounts) {
284
+ try {
285
+ const groups = account.groups();
286
+ result.push({
287
+ id: account.id(),
288
+ name: account.name(),
289
+ groupCount: groups.length
290
+ });
291
+ } catch (e) {
292
+ result.push({
293
+ id: account.id(),
294
+ name: account.name(),
295
+ groupCount: 0
296
+ });
297
+ }
298
+ }
299
+
300
+ JSON.stringify(result);
301
+ `;
302
+
303
+ const result = await runJXA(script);
304
+ return result ? JSON.parse(result) : [];
305
+ }
306
+
307
+ /**
308
+ * List contact groups
309
+ * @param {string} accountId - Optional account ID to filter by
310
+ * @returns {Promise<Array>} - List of groups
311
+ */
312
+ async function listGroups(accountId = null) {
313
+ const script = accountId ? `
314
+ const contacts = Application('Contacts');
315
+ const account = contacts.accounts.byId("${escapeJXA(accountId)}");
316
+ const groups = account.groups();
317
+ let result = [];
318
+
319
+ for (let group of groups) {
320
+ try {
321
+ const people = group.people();
322
+ result.push({
323
+ id: group.id(),
324
+ name: group.name(),
325
+ accountId: "${escapeJXA(accountId)}",
326
+ accountName: account.name(),
327
+ contactCount: people.length
328
+ });
329
+ } catch (e) {}
330
+ }
331
+
332
+ JSON.stringify(result);
333
+ ` : `
334
+ const contacts = Application('Contacts');
335
+ const accounts = contacts.accounts();
336
+ let result = [];
337
+
338
+ for (let account of accounts) {
339
+ try {
340
+ const groups = account.groups();
341
+ for (let group of groups) {
342
+ try {
343
+ const people = group.people();
344
+ result.push({
345
+ id: group.id(),
346
+ name: group.name(),
347
+ accountId: account.id(),
348
+ accountName: account.name(),
349
+ contactCount: people.length
350
+ });
351
+ } catch (e) {}
352
+ }
353
+ } catch (e) {}
354
+ }
355
+
356
+ JSON.stringify(result);
357
+ `;
358
+
359
+ const result = await runJXA(script);
360
+ return result ? JSON.parse(result) : [];
361
+ }
362
+
363
+ module.exports = {
364
+ listContacts,
365
+ searchContacts,
366
+ readContact,
367
+ createContact,
368
+ updateContact,
369
+ deleteContact,
370
+ listAccounts,
371
+ listGroups
372
+ };
@@ -0,0 +1,351 @@
1
+ /**
2
+ * IMAP client wrapper for iCloud Mail
3
+ */
4
+
5
+ const Imap = require('imap');
6
+ const { simpleParser } = require('mailparser');
7
+ const config = require('../config');
8
+ const { getCredentials } = require('../auth');
9
+
10
+ /**
11
+ * Create IMAP connection
12
+ */
13
+ function createConnection() {
14
+ const creds = getCredentials();
15
+
16
+ return new Imap({
17
+ user: creds.email,
18
+ password: creds.password,
19
+ host: config.IMAP.HOST,
20
+ port: config.IMAP.PORT,
21
+ tls: config.IMAP.TLS,
22
+ authTimeout: config.IMAP.AUTH_TIMEOUT,
23
+ connTimeout: config.IMAP.CONN_TIMEOUT
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Execute IMAP operation with connection management
29
+ */
30
+ function withImap(operation) {
31
+ return new Promise((resolve, reject) => {
32
+ const imap = createConnection();
33
+
34
+ imap.once('ready', async () => {
35
+ try {
36
+ const result = await operation(imap);
37
+ imap.end();
38
+ resolve(result);
39
+ } catch (err) {
40
+ imap.end();
41
+ reject(err);
42
+ }
43
+ });
44
+
45
+ imap.once('error', (err) => {
46
+ if (err.message?.includes('AUTHENTICATIONFAILED')) {
47
+ reject(new Error('UNAUTHORIZED'));
48
+ } else {
49
+ reject(err);
50
+ }
51
+ });
52
+
53
+ imap.connect();
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Get folder name from user-friendly name
59
+ */
60
+ function getFolderName(folder) {
61
+ const lower = (folder || 'inbox').toLowerCase();
62
+ return config.EMAIL_FOLDERS[lower] || folder;
63
+ }
64
+
65
+ /**
66
+ * List emails from a folder
67
+ */
68
+ async function listEmails(folder = 'inbox', count = 25) {
69
+ return withImap(async (imap) => {
70
+ return new Promise((resolve, reject) => {
71
+ const folderName = getFolderName(folder);
72
+
73
+ imap.openBox(folderName, true, (err, box) => {
74
+ if (err) {
75
+ reject(err);
76
+ return;
77
+ }
78
+
79
+ const total = box.messages.total;
80
+ if (total === 0) {
81
+ resolve([]);
82
+ return;
83
+ }
84
+
85
+ // Fetch most recent emails
86
+ const start = Math.max(1, total - count + 1);
87
+ const range = `${start}:${total}`;
88
+
89
+ const emails = [];
90
+ const fetch = imap.seq.fetch(range, {
91
+ bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
92
+ struct: true
93
+ });
94
+
95
+ fetch.on('message', (msg, seqno) => {
96
+ const email = { seqno, uid: null };
97
+
98
+ msg.on('body', (stream, info) => {
99
+ let buffer = '';
100
+ stream.on('data', (chunk) => buffer += chunk.toString('utf8'));
101
+ stream.on('end', () => {
102
+ const headers = Imap.parseHeader(buffer);
103
+ email.from = headers.from?.[0] || '';
104
+ email.to = headers.to?.[0] || '';
105
+ email.subject = headers.subject?.[0] || '(No subject)';
106
+ email.date = headers.date?.[0] || '';
107
+ });
108
+ });
109
+
110
+ msg.once('attributes', (attrs) => {
111
+ email.uid = attrs.uid;
112
+ email.flags = attrs.flags || [];
113
+ });
114
+
115
+ msg.once('end', () => {
116
+ emails.push(email);
117
+ });
118
+ });
119
+
120
+ fetch.once('error', reject);
121
+ fetch.once('end', () => {
122
+ // Sort by date descending (newest first)
123
+ emails.sort((a, b) => new Date(b.date) - new Date(a.date));
124
+ resolve(emails);
125
+ });
126
+ });
127
+ });
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Read full email content
133
+ */
134
+ async function readEmail(uid, folder = 'inbox') {
135
+ return withImap(async (imap) => {
136
+ return new Promise((resolve, reject) => {
137
+ const folderName = getFolderName(folder);
138
+
139
+ imap.openBox(folderName, true, (err) => {
140
+ if (err) {
141
+ reject(err);
142
+ return;
143
+ }
144
+
145
+ const fetch = imap.fetch(uid, { bodies: '' });
146
+ let rawEmail = '';
147
+
148
+ fetch.on('message', (msg) => {
149
+ msg.on('body', (stream) => {
150
+ stream.on('data', (chunk) => rawEmail += chunk.toString('utf8'));
151
+ });
152
+ });
153
+
154
+ fetch.once('error', reject);
155
+ fetch.once('end', async () => {
156
+ try {
157
+ const parsed = await simpleParser(rawEmail);
158
+ resolve({
159
+ uid,
160
+ from: parsed.from?.text || '',
161
+ to: parsed.to?.text || '',
162
+ cc: parsed.cc?.text || '',
163
+ subject: parsed.subject || '(No subject)',
164
+ date: parsed.date,
165
+ text: parsed.text || '',
166
+ html: parsed.html || '',
167
+ attachments: (parsed.attachments || []).map(a => ({
168
+ filename: a.filename,
169
+ contentType: a.contentType,
170
+ size: a.size
171
+ }))
172
+ });
173
+ } catch (parseErr) {
174
+ reject(parseErr);
175
+ }
176
+ });
177
+ });
178
+ });
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Search emails
184
+ */
185
+ async function searchEmails(criteria, folder = 'inbox', count = 25) {
186
+ return withImap(async (imap) => {
187
+ return new Promise((resolve, reject) => {
188
+ const folderName = getFolderName(folder);
189
+
190
+ imap.openBox(folderName, true, (err, box) => {
191
+ if (err) {
192
+ reject(err);
193
+ return;
194
+ }
195
+
196
+ // Build IMAP search criteria
197
+ const searchCriteria = [];
198
+
199
+ if (criteria.from) {
200
+ searchCriteria.push(['FROM', criteria.from]);
201
+ }
202
+ if (criteria.subject) {
203
+ searchCriteria.push(['SUBJECT', criteria.subject]);
204
+ }
205
+ if (criteria.since) {
206
+ searchCriteria.push(['SINCE', criteria.since]);
207
+ }
208
+ if (criteria.before) {
209
+ searchCriteria.push(['BEFORE', criteria.before]);
210
+ }
211
+ if (criteria.unseen) {
212
+ searchCriteria.push('UNSEEN');
213
+ }
214
+ if (criteria.text) {
215
+ searchCriteria.push(['TEXT', criteria.text]);
216
+ }
217
+
218
+ // Default to ALL if no criteria
219
+ if (searchCriteria.length === 0) {
220
+ searchCriteria.push('ALL');
221
+ }
222
+
223
+ imap.search(searchCriteria, (err, uids) => {
224
+ if (err) {
225
+ reject(err);
226
+ return;
227
+ }
228
+
229
+ if (!uids || uids.length === 0) {
230
+ resolve([]);
231
+ return;
232
+ }
233
+
234
+ // Get most recent results
235
+ const recentUids = uids.slice(-count);
236
+
237
+ const emails = [];
238
+ const fetch = imap.fetch(recentUids, {
239
+ bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
240
+ struct: true
241
+ });
242
+
243
+ fetch.on('message', (msg, seqno) => {
244
+ const email = { seqno };
245
+
246
+ msg.on('body', (stream) => {
247
+ let buffer = '';
248
+ stream.on('data', (chunk) => buffer += chunk.toString('utf8'));
249
+ stream.on('end', () => {
250
+ const headers = Imap.parseHeader(buffer);
251
+ email.from = headers.from?.[0] || '';
252
+ email.to = headers.to?.[0] || '';
253
+ email.subject = headers.subject?.[0] || '(No subject)';
254
+ email.date = headers.date?.[0] || '';
255
+ });
256
+ });
257
+
258
+ msg.once('attributes', (attrs) => {
259
+ email.uid = attrs.uid;
260
+ email.flags = attrs.flags || [];
261
+ });
262
+
263
+ msg.once('end', () => {
264
+ emails.push(email);
265
+ });
266
+ });
267
+
268
+ fetch.once('error', reject);
269
+ fetch.once('end', () => {
270
+ emails.sort((a, b) => new Date(b.date) - new Date(a.date));
271
+ resolve(emails);
272
+ });
273
+ });
274
+ });
275
+ });
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Mark email as read/unread
281
+ */
282
+ async function markAsRead(uid, folder = 'inbox', isRead = true) {
283
+ return withImap(async (imap) => {
284
+ return new Promise((resolve, reject) => {
285
+ const folderName = getFolderName(folder);
286
+
287
+ imap.openBox(folderName, false, (err) => {
288
+ if (err) {
289
+ reject(err);
290
+ return;
291
+ }
292
+
293
+ const flags = ['\\Seen'];
294
+ const method = isRead ? 'addFlags' : 'delFlags';
295
+
296
+ imap[method](uid, flags, (err) => {
297
+ if (err) {
298
+ reject(err);
299
+ } else {
300
+ resolve(true);
301
+ }
302
+ });
303
+ });
304
+ });
305
+ });
306
+ }
307
+
308
+ /**
309
+ * List folders
310
+ */
311
+ async function listFolders() {
312
+ return withImap(async (imap) => {
313
+ return new Promise((resolve, reject) => {
314
+ imap.getBoxes((err, boxes) => {
315
+ if (err) {
316
+ reject(err);
317
+ return;
318
+ }
319
+
320
+ const folders = [];
321
+
322
+ function processBoxes(boxObj, prefix = '') {
323
+ for (const [name, box] of Object.entries(boxObj)) {
324
+ const fullPath = prefix ? `${prefix}${box.delimiter}${name}` : name;
325
+ folders.push({
326
+ name: fullPath,
327
+ delimiter: box.delimiter,
328
+ flags: box.attribs || []
329
+ });
330
+
331
+ if (box.children) {
332
+ processBoxes(box.children, fullPath);
333
+ }
334
+ }
335
+ }
336
+
337
+ processBoxes(boxes);
338
+ resolve(folders);
339
+ });
340
+ });
341
+ });
342
+ }
343
+
344
+ module.exports = {
345
+ listEmails,
346
+ readEmail,
347
+ searchEmails,
348
+ markAsRead,
349
+ listFolders,
350
+ getFolderName
351
+ };