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
package/email/index.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email module for iCloud MCP
|
|
3
|
+
* Provides email tools via IMAP/SMTP
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
const cloudClient = require('./imap-client');
|
|
8
|
+
const smtpClient = require('./smtp-client');
|
|
9
|
+
const localClient = require('./local-client');
|
|
10
|
+
const { isLocalMode } = require('../mode');
|
|
11
|
+
const { formatSuccess, formatError, withErrorHandler } = require('../utils/error-handler');
|
|
12
|
+
const { listOutput, listResult } = require('../utils/schemas');
|
|
13
|
+
const { formatDate, formatRelative } = require('../utils/date-utils');
|
|
14
|
+
const config = require('../config');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalize a message summary to one shape regardless of mode.
|
|
18
|
+
*
|
|
19
|
+
* Cloud (IMAP) returns a uid plus a raw `flags` array; local (Mail.app)
|
|
20
|
+
* returns an id plus a boolean `read`. Downstream sees `ref` (the handle you
|
|
21
|
+
* pass back to read/mark) and `unread`.
|
|
22
|
+
*/
|
|
23
|
+
function normalizeEmail(email, local) {
|
|
24
|
+
if (local) {
|
|
25
|
+
return { ...email, ref: email.id, unread: email.read === false };
|
|
26
|
+
}
|
|
27
|
+
return { ...email, ref: email.uid, unread: !(email.flags || []).includes('\\Seen') };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Handler: List emails
|
|
32
|
+
*/
|
|
33
|
+
async function handleListEmails(args) {
|
|
34
|
+
const folder = args.folder || 'inbox';
|
|
35
|
+
const count = Math.min(args.count || 25, config.DEFAULTS.MAX_RESULTS);
|
|
36
|
+
|
|
37
|
+
const local = isLocalMode();
|
|
38
|
+
const client = local ? localClient : cloudClient;
|
|
39
|
+
|
|
40
|
+
const emails = (await client.listEmails(folder, count)).map(e => normalizeEmail(e, local));
|
|
41
|
+
|
|
42
|
+
if (emails.length === 0) {
|
|
43
|
+
return formatSuccess(`No emails found in ${folder}.`, listResult([]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines = emails.map((email, i) => {
|
|
47
|
+
const unread = email.unread ? '[UNREAD] ' : '';
|
|
48
|
+
const date = formatRelative(new Date(email.date));
|
|
49
|
+
return `${i + 1}. ${unread}${email.subject}\n From: ${email.from}\n Date: ${date}\n ${local ? 'ID' : 'UID'}: ${email.ref}`;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return formatSuccess(`Emails in ${folder} (${emails.length}):\n\n${lines.join('\n\n')}`, listResult(emails));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handler: Read email
|
|
57
|
+
*/
|
|
58
|
+
async function handleReadEmail(args) {
|
|
59
|
+
if (!args.uid) {
|
|
60
|
+
return formatError(new Error('Email UID is required'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const folder = args.folder || 'inbox';
|
|
64
|
+
const local = isLocalMode();
|
|
65
|
+
|
|
66
|
+
const email = local
|
|
67
|
+
? await localClient.readEmail(args.uid)
|
|
68
|
+
: await cloudClient.readEmail(args.uid, folder);
|
|
69
|
+
|
|
70
|
+
// Mail.app returns the body as `body`; mailparser returns `text`.
|
|
71
|
+
let body = email.text || email.body || '';
|
|
72
|
+
if (body.length > config.DEFAULTS.EMAIL_BODY_MAX_LENGTH) {
|
|
73
|
+
body = body.substring(0, config.DEFAULTS.EMAIL_BODY_MAX_LENGTH) + '\n... (truncated)';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const attachments = email.attachments || [];
|
|
77
|
+
const attachmentInfo = attachments.length > 0
|
|
78
|
+
? `\n\nAttachments (${attachments.length}):\n${attachments.map(a => `- ${a.filename}${a.size ? ` (${Math.round(a.size / 1024)}KB)` : ''}`).join('\n')}`
|
|
79
|
+
: '';
|
|
80
|
+
|
|
81
|
+
return formatSuccess(
|
|
82
|
+
`Subject: ${email.subject}
|
|
83
|
+
From: ${email.from}
|
|
84
|
+
To: ${email.to}${email.cc ? `\nCC: ${email.cc}` : ''}
|
|
85
|
+
Date: ${formatDate(email.date)}
|
|
86
|
+
${local ? 'ID' : 'UID'}: ${email.uid || email.id}
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
${body}${attachmentInfo}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handler: Send email
|
|
96
|
+
*/
|
|
97
|
+
async function handleSendEmail(args) {
|
|
98
|
+
if (!args.to) {
|
|
99
|
+
return formatError(new Error('Recipient (to) is required'));
|
|
100
|
+
}
|
|
101
|
+
if (!args.subject) {
|
|
102
|
+
return formatError(new Error('Subject is required'));
|
|
103
|
+
}
|
|
104
|
+
if (!args.body) {
|
|
105
|
+
return formatError(new Error('Body is required'));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = await (isLocalMode() ? localClient : smtpClient).sendEmail({
|
|
109
|
+
to: args.to,
|
|
110
|
+
cc: args.cc,
|
|
111
|
+
bcc: args.bcc,
|
|
112
|
+
subject: args.subject,
|
|
113
|
+
body: args.body,
|
|
114
|
+
isHtml: args.isHtml || false
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (result.success) {
|
|
118
|
+
return formatSuccess(
|
|
119
|
+
`Email sent successfully!\n\nTo: ${args.to}${args.cc ? `\nCC: ${args.cc}` : ''}\nSubject: ${args.subject}${result.messageId ? `\nMessage ID: ${result.messageId}` : ''}`
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
return formatError(new Error('Failed to send email'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Handler: Search emails
|
|
128
|
+
*/
|
|
129
|
+
async function handleSearchEmails(args) {
|
|
130
|
+
const folder = args.folder || 'inbox';
|
|
131
|
+
const count = Math.min(args.count || 25, config.DEFAULTS.MAX_RESULTS);
|
|
132
|
+
|
|
133
|
+
const criteria = {};
|
|
134
|
+
if (args.from) criteria.from = args.from;
|
|
135
|
+
if (args.subject) criteria.subject = args.subject;
|
|
136
|
+
if (args.query) criteria.text = args.query;
|
|
137
|
+
if (args.unreadOnly) criteria.unseen = true;
|
|
138
|
+
|
|
139
|
+
const local = isLocalMode();
|
|
140
|
+
const raw = local
|
|
141
|
+
? await localClient.searchEmails({ query: args.query, from: args.from, subject: args.subject, folder, count })
|
|
142
|
+
: await cloudClient.searchEmails(criteria, folder, count);
|
|
143
|
+
const emails = raw.map(e => normalizeEmail(e, local));
|
|
144
|
+
|
|
145
|
+
if (emails.length === 0) {
|
|
146
|
+
return formatSuccess(`No emails found matching your search criteria in ${folder}.`, listResult([]));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lines = emails.map((email, i) => {
|
|
150
|
+
const unread = email.unread ? '[UNREAD] ' : '';
|
|
151
|
+
const date = formatRelative(new Date(email.date));
|
|
152
|
+
return `${i + 1}. ${unread}${email.subject}\n From: ${email.from}\n Date: ${date}\n ${local ? 'ID' : 'UID'}: ${email.ref}`;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return formatSuccess(`Search results in ${folder} (${emails.length}):\n\n${lines.join('\n\n')}`, listResult(emails));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handler: Mark as read
|
|
160
|
+
*/
|
|
161
|
+
async function handleMarkAsRead(args) {
|
|
162
|
+
if (!args.uid) {
|
|
163
|
+
return formatError(new Error('Email UID is required'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const folder = args.folder || 'inbox';
|
|
167
|
+
const isRead = args.isRead !== false;
|
|
168
|
+
|
|
169
|
+
if (isLocalMode()) {
|
|
170
|
+
await localClient.markAsRead(args.uid, isRead);
|
|
171
|
+
} else {
|
|
172
|
+
await cloudClient.markAsRead(args.uid, folder, isRead);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return formatSuccess(`Email ${args.uid} marked as ${isRead ? 'read' : 'unread'}.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Handler: List folders
|
|
180
|
+
*/
|
|
181
|
+
async function handleListFolders() {
|
|
182
|
+
const folders = isLocalMode()
|
|
183
|
+
? await localClient.listFolders()
|
|
184
|
+
: await cloudClient.listFolders();
|
|
185
|
+
|
|
186
|
+
const lines = folders.map(f => `- ${f.name}`);
|
|
187
|
+
|
|
188
|
+
return formatSuccess(`Email folders:\n\n${lines.join('\n')}`, listResult(folders));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Tool definitions
|
|
192
|
+
const emailTools = [
|
|
193
|
+
{
|
|
194
|
+
name: 'list-emails',
|
|
195
|
+
outputSchema: listOutput('Emails'),
|
|
196
|
+
title: 'List Emails',
|
|
197
|
+
description: 'Lists emails from a folder (default: inbox)',
|
|
198
|
+
inputSchema: {
|
|
199
|
+
folder: z.string().optional().describe('Email folder (inbox, sent, drafts, trash, archive, junk)'),
|
|
200
|
+
count: z.number().int().min(1).max(50).optional().describe('Number of emails to retrieve (default: 25, max: 50)')
|
|
201
|
+
},
|
|
202
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":true},
|
|
203
|
+
handler: withErrorHandler(handleListEmails, 'list-emails')
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'read-email',
|
|
207
|
+
title: 'Read Email',
|
|
208
|
+
description: 'Reads the full content of an email by UID',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
uid: z.string().describe('Email handle: the UID from list-emails in cloud mode, or the message ID in local mode'),
|
|
211
|
+
folder: z.string().optional().describe('Email folder (default: inbox)')
|
|
212
|
+
},
|
|
213
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":true},
|
|
214
|
+
handler: withErrorHandler(handleReadEmail, 'read-email')
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'send-email',
|
|
218
|
+
title: 'Send Email',
|
|
219
|
+
description: 'Composes and sends an email',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
to: z.string().describe('Recipient email address(es), comma-separated'),
|
|
222
|
+
cc: z.string().optional().describe('CC recipient(s), comma-separated'),
|
|
223
|
+
bcc: z.string().optional().describe('BCC recipient(s), comma-separated'),
|
|
224
|
+
subject: z.string().describe('Email subject'),
|
|
225
|
+
body: z.string().describe('Email body content'),
|
|
226
|
+
isHtml: z.boolean().optional().describe('Whether the body is HTML (default: false)')
|
|
227
|
+
},
|
|
228
|
+
annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":true},
|
|
229
|
+
handler: withErrorHandler(handleSendEmail, 'send-email')
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'search-emails',
|
|
233
|
+
outputSchema: listOutput('Matching emails'),
|
|
234
|
+
title: 'Search Emails',
|
|
235
|
+
description: 'Search for emails by criteria',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
query: z.string().optional().describe('Text to search in email content'),
|
|
238
|
+
from: z.string().optional().describe('Filter by sender'),
|
|
239
|
+
subject: z.string().optional().describe('Filter by subject'),
|
|
240
|
+
folder: z.string().optional().describe('Email folder to search (default: inbox)'),
|
|
241
|
+
unreadOnly: z.boolean().optional().describe('Only show unread emails'),
|
|
242
|
+
count: z.number().int().min(1).max(50).optional().describe('Max results (default: 25, max: 50)')
|
|
243
|
+
},
|
|
244
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":true},
|
|
245
|
+
handler: withErrorHandler(handleSearchEmails, 'search-emails')
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'mark-as-read',
|
|
249
|
+
title: 'Mark Email Read/Unread',
|
|
250
|
+
description: 'Marks an email as read or unread',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
uid: z.string().describe('Email handle: the UID from list-emails in cloud mode, or the message ID in local mode'),
|
|
253
|
+
folder: z.string().optional().describe('Email folder (default: inbox)'),
|
|
254
|
+
isRead: z.boolean().optional().describe('Mark as read (true) or unread (false). Default: true')
|
|
255
|
+
},
|
|
256
|
+
annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},
|
|
257
|
+
handler: withErrorHandler(handleMarkAsRead, 'mark-as-read')
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'list-folders',
|
|
261
|
+
outputSchema: listOutput('Mail folders'),
|
|
262
|
+
title: 'List Mail Folders',
|
|
263
|
+
description: 'Lists all email folders',
|
|
264
|
+
inputSchema: {},
|
|
265
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":true},
|
|
266
|
+
handler: withErrorHandler(handleListFolders, 'list-folders')
|
|
267
|
+
}
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
emailTools,
|
|
272
|
+
handleListEmails,
|
|
273
|
+
handleReadEmail,
|
|
274
|
+
handleSendEmail,
|
|
275
|
+
handleSearchEmails,
|
|
276
|
+
handleMarkAsRead,
|
|
277
|
+
handleListFolders
|
|
278
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Email Client
|
|
3
|
+
* Accesses Mail.app via AppleScript
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt, asBool } = require('../utils/applescript');
|
|
7
|
+
const config = require('../config');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List emails from a mailbox
|
|
11
|
+
* @param {string} folder - Folder name (inbox, sent, drafts, etc.)
|
|
12
|
+
* @param {number} count - Number of emails to retrieve
|
|
13
|
+
* @returns {Promise<Array>} - List of emails
|
|
14
|
+
*/
|
|
15
|
+
async function listEmails(folder = 'inbox', count = 25) {
|
|
16
|
+
const mailboxName = getMailboxName(folder);
|
|
17
|
+
|
|
18
|
+
const script = `
|
|
19
|
+
ObjC.import('Foundation');
|
|
20
|
+
const mail = Application('Mail');
|
|
21
|
+
const accounts = mail.accounts();
|
|
22
|
+
let emails = [];
|
|
23
|
+
|
|
24
|
+
for (let account of accounts) {
|
|
25
|
+
try {
|
|
26
|
+
const mailbox = account.mailboxes.byName('${escapeJXA(mailboxName)}');
|
|
27
|
+
const messages = mailbox.messages();
|
|
28
|
+
const limit = Math.min(${asInt(count, 25)}, messages.length);
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < limit; i++) {
|
|
31
|
+
const msg = messages[i];
|
|
32
|
+
emails.push({
|
|
33
|
+
id: msg.id(),
|
|
34
|
+
subject: msg.subject(),
|
|
35
|
+
from: msg.sender(),
|
|
36
|
+
date: msg.dateReceived().toISOString(),
|
|
37
|
+
read: msg.readStatus(),
|
|
38
|
+
account: account.name()
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Mailbox might not exist in this account
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
JSON.stringify(emails.slice(0, ${count}));
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const result = await runJXA(script);
|
|
50
|
+
return result ? JSON.parse(result) : [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read a specific email
|
|
55
|
+
* @param {string} emailId - Email ID
|
|
56
|
+
* @returns {Promise<Object>} - Email content
|
|
57
|
+
*/
|
|
58
|
+
async function readEmail(emailId) {
|
|
59
|
+
const script = `
|
|
60
|
+
const mail = Application('Mail');
|
|
61
|
+
const msg = mail.messages.byId(${asInt(emailId)});
|
|
62
|
+
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
id: msg.id(),
|
|
65
|
+
subject: msg.subject(),
|
|
66
|
+
from: msg.sender(),
|
|
67
|
+
to: msg.toRecipients().map(r => r.address()),
|
|
68
|
+
cc: msg.ccRecipients().map(r => r.address()),
|
|
69
|
+
date: msg.dateReceived().toISOString(),
|
|
70
|
+
body: msg.content(),
|
|
71
|
+
read: msg.readStatus()
|
|
72
|
+
});
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const result = await runJXA(script);
|
|
76
|
+
return result ? JSON.parse(result) : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Send an email
|
|
81
|
+
* @param {Object} options - Email options
|
|
82
|
+
* @returns {Promise<Object>} - Send result
|
|
83
|
+
*/
|
|
84
|
+
async function sendEmail({ to, cc, bcc, subject, body }) {
|
|
85
|
+
const toRecipients = Array.isArray(to) ? to : [to];
|
|
86
|
+
const ccRecipients = cc ? (Array.isArray(cc) ? cc : [cc]) : [];
|
|
87
|
+
const bccRecipients = bcc ? (Array.isArray(bcc) ? bcc : [bcc]) : [];
|
|
88
|
+
|
|
89
|
+
let script = `
|
|
90
|
+
tell application "Mail"
|
|
91
|
+
set newMessage to make new outgoing message with properties {subject:"${escapeAppleScript(subject)}", content:"${escapeAppleScript(body)}", visible:false}
|
|
92
|
+
tell newMessage
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
// Add To recipients
|
|
96
|
+
for (const recipient of toRecipients) {
|
|
97
|
+
script += `\n make new to recipient with properties {address:"${escapeAppleScript(recipient)}"}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add CC recipients
|
|
101
|
+
for (const recipient of ccRecipients) {
|
|
102
|
+
script += `\n make new cc recipient with properties {address:"${escapeAppleScript(recipient)}"}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add BCC recipients
|
|
106
|
+
for (const recipient of bccRecipients) {
|
|
107
|
+
script += `\n make new bcc recipient with properties {address:"${escapeAppleScript(recipient)}"}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
script += `
|
|
111
|
+
send
|
|
112
|
+
end tell
|
|
113
|
+
end tell
|
|
114
|
+
return "sent"
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
await runAppleScript(script);
|
|
118
|
+
return { success: true, message: 'Email sent successfully' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Search emails
|
|
123
|
+
* @param {Object} options - Search options
|
|
124
|
+
* @returns {Promise<Array>} - Matching emails
|
|
125
|
+
*/
|
|
126
|
+
async function searchEmails({ query, from, subject, folder = 'inbox', count = 25 }) {
|
|
127
|
+
const mailboxName = getMailboxName(folder);
|
|
128
|
+
|
|
129
|
+
let conditions = [];
|
|
130
|
+
if (query) conditions.push(`(msg.subject().toLowerCase().includes("${escapeJXA(query.toLowerCase())}") || msg.content().toLowerCase().includes("${escapeJXA(query.toLowerCase())}"))`);
|
|
131
|
+
if (from) conditions.push(`msg.sender().toLowerCase().includes("${escapeJXA(from.toLowerCase())}")`);
|
|
132
|
+
if (subject) conditions.push(`msg.subject().toLowerCase().includes("${escapeJXA(subject.toLowerCase())}")`);
|
|
133
|
+
|
|
134
|
+
const filterCondition = conditions.length > 0 ? conditions.join(' && ') : 'true';
|
|
135
|
+
|
|
136
|
+
const script = `
|
|
137
|
+
const mail = Application('Mail');
|
|
138
|
+
const accounts = mail.accounts();
|
|
139
|
+
let emails = [];
|
|
140
|
+
|
|
141
|
+
for (let account of accounts) {
|
|
142
|
+
try {
|
|
143
|
+
const mailbox = account.mailboxes.byName('${escapeJXA(mailboxName)}');
|
|
144
|
+
const messages = mailbox.messages();
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < messages.length && emails.length < ${asInt(count, 25)}; i++) {
|
|
147
|
+
const msg = messages[i];
|
|
148
|
+
if (${filterCondition}) {
|
|
149
|
+
emails.push({
|
|
150
|
+
id: msg.id(),
|
|
151
|
+
subject: msg.subject(),
|
|
152
|
+
from: msg.sender(),
|
|
153
|
+
date: msg.dateReceived().toISOString(),
|
|
154
|
+
read: msg.readStatus(),
|
|
155
|
+
account: account.name()
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
JSON.stringify(emails);
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
const result = await runJXA(script);
|
|
166
|
+
return result ? JSON.parse(result) : [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Mark email as read/unread
|
|
171
|
+
* @param {string} emailId - Email ID
|
|
172
|
+
* @param {boolean} isRead - Read status
|
|
173
|
+
* @returns {Promise<Object>} - Result
|
|
174
|
+
*/
|
|
175
|
+
async function markAsRead(emailId, isRead = true) {
|
|
176
|
+
const script = `
|
|
177
|
+
tell application "Mail"
|
|
178
|
+
set theMessage to message id ${asInt(emailId)}
|
|
179
|
+
set read status of theMessage to ${asBool(isRead, true)}
|
|
180
|
+
end tell
|
|
181
|
+
return "done"
|
|
182
|
+
`;
|
|
183
|
+
|
|
184
|
+
await runAppleScript(script);
|
|
185
|
+
return { success: true, message: `Email marked as ${isRead ? 'read' : 'unread'}` };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* List mail folders/mailboxes
|
|
190
|
+
* @returns {Promise<Array>} - List of folders
|
|
191
|
+
*/
|
|
192
|
+
async function listFolders() {
|
|
193
|
+
const script = `
|
|
194
|
+
const mail = Application('Mail');
|
|
195
|
+
const accounts = mail.accounts();
|
|
196
|
+
let folders = [];
|
|
197
|
+
|
|
198
|
+
for (let account of accounts) {
|
|
199
|
+
const mailboxes = account.mailboxes();
|
|
200
|
+
for (let mb of mailboxes) {
|
|
201
|
+
folders.push({
|
|
202
|
+
name: mb.name(),
|
|
203
|
+
account: account.name(),
|
|
204
|
+
unreadCount: mb.unreadCount()
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
JSON.stringify(folders);
|
|
210
|
+
`;
|
|
211
|
+
|
|
212
|
+
const result = await runJXA(script);
|
|
213
|
+
return result ? JSON.parse(result) : [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Delete an email
|
|
218
|
+
* @param {string} emailId - Email ID
|
|
219
|
+
* @returns {Promise<Object>} - Result
|
|
220
|
+
*/
|
|
221
|
+
async function deleteEmail(emailId) {
|
|
222
|
+
const script = `
|
|
223
|
+
tell application "Mail"
|
|
224
|
+
set theMessage to message id ${asInt(emailId)}
|
|
225
|
+
delete theMessage
|
|
226
|
+
end tell
|
|
227
|
+
return "deleted"
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
await runAppleScript(script);
|
|
231
|
+
return { success: true, message: 'Email deleted' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Map folder names to Mail.app mailbox names
|
|
236
|
+
*/
|
|
237
|
+
function getMailboxName(folder) {
|
|
238
|
+
const mapping = {
|
|
239
|
+
'inbox': 'INBOX',
|
|
240
|
+
'sent': 'Sent Messages',
|
|
241
|
+
'drafts': 'Drafts',
|
|
242
|
+
'trash': 'Deleted Messages',
|
|
243
|
+
'archive': 'Archive',
|
|
244
|
+
'junk': 'Junk'
|
|
245
|
+
};
|
|
246
|
+
return mapping[folder.toLowerCase()] || folder;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
listEmails,
|
|
251
|
+
readEmail,
|
|
252
|
+
sendEmail,
|
|
253
|
+
searchEmails,
|
|
254
|
+
markAsRead,
|
|
255
|
+
listFolders,
|
|
256
|
+
deleteEmail
|
|
257
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMTP client wrapper for sending iCloud Mail
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const nodemailer = require('nodemailer');
|
|
6
|
+
const config = require('../config');
|
|
7
|
+
const { getCredentials } = require('../auth');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create SMTP transporter
|
|
11
|
+
*/
|
|
12
|
+
function createTransporter() {
|
|
13
|
+
const creds = getCredentials();
|
|
14
|
+
|
|
15
|
+
return nodemailer.createTransport({
|
|
16
|
+
host: config.SMTP.HOST,
|
|
17
|
+
port: config.SMTP.PORT,
|
|
18
|
+
secure: config.SMTP.SECURE,
|
|
19
|
+
auth: {
|
|
20
|
+
user: creds.email,
|
|
21
|
+
pass: creds.password
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Send an email
|
|
28
|
+
*/
|
|
29
|
+
async function sendEmail({ to, cc, bcc, subject, body, isHtml = false }) {
|
|
30
|
+
const creds = getCredentials();
|
|
31
|
+
const transporter = createTransporter();
|
|
32
|
+
|
|
33
|
+
const mailOptions = {
|
|
34
|
+
from: creds.email,
|
|
35
|
+
to,
|
|
36
|
+
subject,
|
|
37
|
+
[isHtml ? 'html' : 'text']: body
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (cc) mailOptions.cc = cc;
|
|
41
|
+
if (bcc) mailOptions.bcc = bcc;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const info = await transporter.sendMail(mailOptions);
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
messageId: info.messageId,
|
|
48
|
+
accepted: info.accepted,
|
|
49
|
+
rejected: info.rejected
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.message?.includes('auth') || error.code === 'EAUTH') {
|
|
53
|
+
throw new Error('UNAUTHORIZED');
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Verify SMTP connection
|
|
61
|
+
*/
|
|
62
|
+
async function verifyConnection() {
|
|
63
|
+
const transporter = createTransporter();
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await transporter.verify();
|
|
67
|
+
return true;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.message?.includes('auth') || error.code === 'EAUTH') {
|
|
70
|
+
throw new Error('UNAUTHORIZED');
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
sendEmail,
|
|
78
|
+
verifyConnection
|
|
79
|
+
};
|