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.
- package/DISCLOSURE +35 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/auth/index.js +175 -0
- package/calendar/caldav-client.js +320 -0
- package/calendar/index.js +252 -0
- package/calendar/local-client.js +223 -0
- package/config.js +69 -0
- package/contacts/carddav-client.js +331 -0
- package/contacts/index.js +350 -0
- package/contacts/local-client.js +372 -0
- package/email/imap-client.js +351 -0
- package/email/index.js +278 -0
- package/email/local-client.js +257 -0
- package/email/smtp-client.js +79 -0
- package/index.js +123 -0
- package/messages/index.js +157 -0
- package/messages/local-client.js +74 -0
- package/mode.js +82 -0
- package/notes/index.js +162 -0
- package/notes/local-client.js +203 -0
- package/package.json +76 -0
- package/reminders/index.js +215 -0
- package/reminders/local-client.js +269 -0
- package/safari/index.js +122 -0
- package/safari/local-client.js +156 -0
- package/utils/applescript.js +214 -0
- package/utils/date-utils.js +72 -0
- package/utils/error-handler.js +71 -0
- package/utils/schemas.js +33 -0
|
@@ -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
|
+
};
|