icloud-mcp 2.5.0 → 2.7.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/LICENSE +21 -0
- package/README.md +235 -341
- package/index.js +30 -1138
- package/lib/caldav.js +628 -3
- package/lib/carddav.js +28 -2
- package/lib/data-paths.js +13 -0
- package/lib/digest.js +23 -8
- package/lib/imap.js +298 -85
- package/lib/reminders.js +316 -0
- package/lib/session.js +9 -7
- package/lib/smtp.js +53 -21
- package/lib/tools/calendar.js +333 -0
- package/lib/tools/contacts.js +135 -0
- package/lib/tools/mail.js +903 -0
- package/lib/tools/reminders.js +165 -0
- package/package.json +26 -7
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/settings.local.json +0 -35
- package/.mcp.json.example +0 -12
- package/run-digest.js +0 -57
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
// Mail, rules, SMTP, digest, and session-log tools.
|
|
2
|
+
import {
|
|
3
|
+
TIMEOUT, withTimeout,
|
|
4
|
+
fetchEmails, getInboxSummary, getMailboxSummary, getTopSenders, getUnreadSenders,
|
|
5
|
+
getEmailsBySender, getEmailsByDateRange, searchEmails,
|
|
6
|
+
getEmailContent, getEmailRaw, listAttachments, getAttachment, getUnsubscribeInfo,
|
|
7
|
+
getThread, getStorageReport,
|
|
8
|
+
flagEmail, markAsRead, deleteEmail, moveEmail, listMailboxes,
|
|
9
|
+
bulkMove, bulkMoveBySender, bulkMoveByDomain, archiveOlderThan,
|
|
10
|
+
bulkDelete, bulkDeleteBySender, bulkDeleteBySubject, deleteOlderThan,
|
|
11
|
+
bulkMarkRead, bulkMarkUnread, markOlderThanRead,
|
|
12
|
+
bulkFlag, bulkFlagBySender, emptyTrash,
|
|
13
|
+
createMailbox, renameMailbox, deleteMailbox,
|
|
14
|
+
getMoveStatus, abandonMove, countEmails,
|
|
15
|
+
createRule, listRules, runRule, deleteRule, runAllRules,
|
|
16
|
+
} from '../imap.js';
|
|
17
|
+
import { logRead, logWrite, logClear } from '../session.js';
|
|
18
|
+
import { composeEmail, replyToEmail, forwardEmail, saveDraft } from '../smtp.js';
|
|
19
|
+
import { getDigestState, updateDigestState } from '../digest.js';
|
|
20
|
+
|
|
21
|
+
const filtersSchema = {
|
|
22
|
+
sender: { type: 'string', description: 'Match exact sender email address' },
|
|
23
|
+
domain: { type: 'string', description: 'Match any sender from this domain (e.g. substack.com)' },
|
|
24
|
+
subject: { type: 'string', description: 'Keyword to match in subject' },
|
|
25
|
+
before: { type: 'string', description: 'Only emails before this date (YYYY-MM-DD)' },
|
|
26
|
+
since: { type: 'string', description: 'Only emails since this date (YYYY-MM-DD)' },
|
|
27
|
+
unread: { type: 'boolean', description: 'True for unread only, false for read only' },
|
|
28
|
+
flagged: { type: 'boolean', description: 'True for flagged only, false for unflagged only' },
|
|
29
|
+
larger: { type: 'number', description: 'Only emails larger than this size in KB' },
|
|
30
|
+
smaller: { type: 'number', description: 'Only emails smaller than this size in KB' },
|
|
31
|
+
hasAttachment: { type: 'boolean', description: 'Only emails with attachments (client-side BODYSTRUCTURE scan — must be combined with other filters that narrow results to under 500 emails first)' },
|
|
32
|
+
account: { type: 'string', description: "Account name to use (e.g. 'icloud', 'gmail'). Defaults to first configured account. Use list_accounts to see available accounts." }
|
|
33
|
+
};
|
|
34
|
+
const accountSchema = filtersSchema.account;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export const mailTools = [
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
name: 'list_accounts',
|
|
41
|
+
description: 'List all configured email accounts (names and IMAP hosts). Use the account name in any mail tool\'s account parameter.',
|
|
42
|
+
inputSchema: { type: 'object', properties: {} }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'get_inbox_summary',
|
|
46
|
+
description: 'Get a summary of a mailbox including total, unread, and recent email counts',
|
|
47
|
+
inputSchema: { type: 'object', properties: { mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }, account: accountSchema } }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'get_mailbox_summary',
|
|
51
|
+
description: 'Get total, unread, and recent email counts for any specific mailbox/folder',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: { mailbox: { type: 'string', description: 'Mailbox path to summarize (e.g. Newsletters, Archive)' } },
|
|
55
|
+
required: ['mailbox']
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'get_top_senders',
|
|
60
|
+
description: 'Get the top senders by email count from a sample of the inbox',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
mailbox: { type: 'string', description: 'Mailbox to analyze (default INBOX)' },
|
|
65
|
+
sampleSize: { type: 'number', description: 'Number of emails to sample (default 500)' },
|
|
66
|
+
maxResults: { type: 'number', description: 'Max number of senders/domains to return (default 20)' }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'get_unread_senders',
|
|
72
|
+
description: 'Get top senders of unread emails',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
mailbox: { type: 'string', description: 'Mailbox to analyze (default INBOX)' },
|
|
77
|
+
sampleSize: { type: 'number', description: 'Number of emails to sample (default 500)' },
|
|
78
|
+
maxResults: { type: 'number', description: 'Max number of senders to return (default 20)' }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'get_emails_by_sender',
|
|
84
|
+
description: 'Get all emails from a specific sender',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
sender: { type: 'string', description: 'Sender email address or domain' },
|
|
89
|
+
mailbox: { type: 'string', description: 'Mailbox to search (default INBOX)' },
|
|
90
|
+
limit: { type: 'number', description: 'Max results to show (default 10)' }
|
|
91
|
+
},
|
|
92
|
+
required: ['sender']
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'read_inbox',
|
|
97
|
+
description: 'Read emails from an inbox with pagination. Supports multiple accounts.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
limit: { type: 'number', description: 'Number of emails per page (default 10)' },
|
|
102
|
+
page: { type: 'number', description: 'Page number (default 1)' },
|
|
103
|
+
onlyUnread: { type: 'boolean', description: 'Only fetch unread emails' },
|
|
104
|
+
mailbox: { type: 'string', description: 'Mailbox to read (default INBOX)' },
|
|
105
|
+
account: { type: 'string', description: 'Account name to read from (e.g. "work", "personal", "school", "icloud"). Omit for default iCloud account.' }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'get_email',
|
|
111
|
+
description: 'Get full content of a specific email by UID',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
116
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' },
|
|
117
|
+
maxChars: { type: 'number', description: 'Max body characters to return (default 8000, max 50000)' },
|
|
118
|
+
includeHeaders: { type: 'boolean', description: 'If true, include a headers object with to/cc/replyTo/messageId/inReplyTo/references/listUnsubscribe' }
|
|
119
|
+
},
|
|
120
|
+
required: ['uid']
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'search_emails',
|
|
125
|
+
description: 'Search emails by keyword or targeted field queries, with optional filters for date, read status, domain, and more',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
query: { type: 'string', description: 'Search keyword (matches subject, sender, body — use OR across all fields)' },
|
|
130
|
+
subjectQuery: { type: 'string', description: 'Match only in subject field' },
|
|
131
|
+
bodyQuery: { type: 'string', description: 'Match only in body field' },
|
|
132
|
+
fromQuery: { type: 'string', description: 'Match only in from/sender field' },
|
|
133
|
+
queryMode: { type: 'string', enum: ['or', 'and'], description: 'How to combine subjectQuery/bodyQuery/fromQuery: or (default) or and' },
|
|
134
|
+
mailbox: { type: 'string', description: 'Mailbox to search (default INBOX)' },
|
|
135
|
+
limit: { type: 'number', description: 'Max results (default 10)' },
|
|
136
|
+
includeSnippet: { type: 'boolean', description: 'If true, include a 200-char body preview snippet for each result (max 10 emails)' },
|
|
137
|
+
...filtersSchema
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'count_emails',
|
|
143
|
+
description: 'Count how many emails match a set of filters without moving or deleting them. Use this before bulk_move or bulk_delete to preview how many emails will be affected.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
mailbox: { type: 'string', description: 'Mailbox to count in (default INBOX)' },
|
|
148
|
+
...filtersSchema
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'bulk_move',
|
|
154
|
+
description: 'Move emails matching any combination of filters from one mailbox to another. Uses safe copy-verify-delete with fingerprint verification and a persistent manifest. Use dryRun: true to preview without making changes.',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
targetMailbox: { type: 'string', description: 'Destination mailbox path' },
|
|
159
|
+
sourceMailbox: { type: 'string', description: 'Source mailbox (default INBOX)' },
|
|
160
|
+
dryRun: { type: 'boolean', description: 'If true, preview what would be moved without actually moving' },
|
|
161
|
+
limit: { type: 'number', description: 'Maximum number of emails to move (default: all matching)' },
|
|
162
|
+
...filtersSchema
|
|
163
|
+
},
|
|
164
|
+
required: ['targetMailbox']
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'bulk_delete',
|
|
169
|
+
description: 'Delete emails matching any combination of filters. Processes in chunks of 500 with per-chunk timeouts for reliability. Use dryRun: true to preview without making changes.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
sourceMailbox: { type: 'string', description: 'Mailbox to delete from (default INBOX)' },
|
|
174
|
+
dryRun: { type: 'boolean', description: 'If true, preview what would be deleted without actually deleting' },
|
|
175
|
+
...filtersSchema
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'bulk_flag',
|
|
181
|
+
description: 'Flag or unflag emails matching any combination of filters in bulk',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
flagged: { type: 'boolean', description: 'True to flag, false to unflag' },
|
|
186
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
187
|
+
...filtersSchema,
|
|
188
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
189
|
+
},
|
|
190
|
+
required: ['flagged']
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'bulk_delete_by_sender',
|
|
195
|
+
description: 'Delete all emails from a specific sender',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
sender: { type: 'string', description: 'Sender email address' },
|
|
200
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
201
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
202
|
+
},
|
|
203
|
+
required: ['sender']
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'bulk_move_by_sender',
|
|
208
|
+
description: 'Move all emails from a specific sender to a folder',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
sender: { type: 'string', description: 'Sender email address' },
|
|
213
|
+
targetMailbox: { type: 'string', description: 'Destination folder' },
|
|
214
|
+
sourceMailbox: { type: 'string', description: 'Source mailbox (default INBOX)' },
|
|
215
|
+
dryRun: { type: 'boolean', description: 'Preview only — return count without moving' }
|
|
216
|
+
},
|
|
217
|
+
required: ['sender', 'targetMailbox']
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'bulk_delete_by_subject',
|
|
222
|
+
description: 'Delete all emails matching a subject pattern',
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
properties: {
|
|
226
|
+
subject: { type: 'string', description: 'Subject keyword to match' },
|
|
227
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
228
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
229
|
+
},
|
|
230
|
+
required: ['subject']
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'bulk_mark_read',
|
|
235
|
+
description: 'Mark all emails as read, optionally filtered by sender',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
240
|
+
sender: { type: 'string', description: 'Optional: only mark emails from this sender as read' },
|
|
241
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'bulk_mark_unread',
|
|
247
|
+
description: 'Mark all emails as unread, optionally filtered by sender',
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
252
|
+
sender: { type: 'string', description: 'Optional: only mark emails from this sender as unread' },
|
|
253
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'delete_older_than',
|
|
259
|
+
description: 'Delete all emails older than a certain number of days',
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
days: { type: 'number', description: 'Delete emails older than this many days' },
|
|
264
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
265
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
266
|
+
},
|
|
267
|
+
required: ['days']
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'get_emails_by_date_range',
|
|
272
|
+
description: 'Get emails between two dates',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
277
|
+
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
278
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
279
|
+
limit: { type: 'number', description: 'Max results (default 10)' }
|
|
280
|
+
},
|
|
281
|
+
required: ['startDate', 'endDate']
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'flag_email',
|
|
286
|
+
description: 'Flag or unflag a single email',
|
|
287
|
+
inputSchema: {
|
|
288
|
+
type: 'object',
|
|
289
|
+
properties: {
|
|
290
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
291
|
+
flagged: { type: 'boolean', description: 'True to flag, false to unflag' },
|
|
292
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }
|
|
293
|
+
},
|
|
294
|
+
required: ['uid', 'flagged']
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'mark_as_read',
|
|
299
|
+
description: 'Mark a single email as read or unread',
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
properties: {
|
|
303
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
304
|
+
seen: { type: 'boolean', description: 'True to mark as read, false for unread' },
|
|
305
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }
|
|
306
|
+
},
|
|
307
|
+
required: ['uid', 'seen']
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'delete_email',
|
|
312
|
+
description: 'Delete a single email',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
317
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' },
|
|
318
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
319
|
+
},
|
|
320
|
+
required: ['uid']
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'move_email',
|
|
325
|
+
description: 'Move a single email to a different mailbox/folder',
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
properties: {
|
|
329
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
330
|
+
targetMailbox: { type: 'string', description: 'Destination mailbox path' },
|
|
331
|
+
sourceMailbox: { type: 'string', description: 'Source mailbox (default INBOX)' },
|
|
332
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
333
|
+
},
|
|
334
|
+
required: ['uid', 'targetMailbox']
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'list_mailboxes',
|
|
339
|
+
description: 'List all mailboxes/folders in iCloud Mail',
|
|
340
|
+
inputSchema: { type: 'object', properties: {} }
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'create_mailbox',
|
|
344
|
+
description: 'Create a new mailbox/folder',
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: 'object',
|
|
347
|
+
properties: { name: { type: 'string', description: 'Name of the new mailbox' } },
|
|
348
|
+
required: ['name']
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'rename_mailbox',
|
|
353
|
+
description: 'Rename an existing mailbox/folder',
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: 'object',
|
|
356
|
+
properties: {
|
|
357
|
+
oldName: { type: 'string', description: 'Current mailbox path' },
|
|
358
|
+
newName: { type: 'string', description: 'New mailbox path' }
|
|
359
|
+
},
|
|
360
|
+
required: ['oldName', 'newName']
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'delete_mailbox',
|
|
365
|
+
description: 'Delete a mailbox/folder. The folder must be empty first.',
|
|
366
|
+
inputSchema: {
|
|
367
|
+
type: 'object',
|
|
368
|
+
properties: {
|
|
369
|
+
name: { type: 'string', description: 'Mailbox path to delete' },
|
|
370
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
371
|
+
},
|
|
372
|
+
required: ['name']
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: 'empty_trash',
|
|
377
|
+
description: 'Permanently delete all emails in the trash (Deleted Messages or Trash folder). Use dryRun: true to preview first.',
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: 'object',
|
|
380
|
+
properties: {
|
|
381
|
+
dryRun: { type: 'boolean', description: 'If true, preview how many emails would be deleted without deleting' }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: 'get_move_status',
|
|
387
|
+
description: 'Check the status of the current or most recent bulk move operation. Shows progress, chunk statuses, and any failures. Call this to monitor a long-running move or inspect a failed one.',
|
|
388
|
+
inputSchema: { type: 'object', properties: {} }
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'abandon_move',
|
|
392
|
+
description: 'Abandon an in-progress move operation so a new one can start. Only use if you are certain the operation should not be resumed. Emails already moved will not be returned to source.',
|
|
393
|
+
inputSchema: { type: 'object', properties: {} }
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: 'log_write',
|
|
397
|
+
description: 'Write a step to the session log. Use this to record your plan before starting, and after each completed step. Helps maintain progress across long operations.',
|
|
398
|
+
inputSchema: {
|
|
399
|
+
type: 'object',
|
|
400
|
+
properties: {
|
|
401
|
+
step: { type: 'string', description: 'Description of what you are doing or just completed' }
|
|
402
|
+
},
|
|
403
|
+
required: ['step']
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: 'log_read',
|
|
408
|
+
description: 'Read the current session log to see what has been done so far.',
|
|
409
|
+
inputSchema: { type: 'object', properties: {} }
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'log_clear',
|
|
413
|
+
description: 'Clear the session log and start fresh. Use this at the start of a new task.',
|
|
414
|
+
inputSchema: { type: 'object', properties: {} }
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'list_attachments',
|
|
418
|
+
description: 'List all attachments in an email without downloading them. Returns filename, MIME type, size, and IMAP part ID for each attachment.',
|
|
419
|
+
inputSchema: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {
|
|
422
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
423
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }
|
|
424
|
+
},
|
|
425
|
+
required: ['uid']
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'get_attachment',
|
|
430
|
+
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 request; use offset+length for larger files.',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
435
|
+
partId: { type: 'string', description: 'IMAP body part ID from list_attachments (e.g. "2", "1.2")' },
|
|
436
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' },
|
|
437
|
+
offset: { type: 'number', description: 'Byte offset for paginated download (returns raw encoded bytes, not decoded)' },
|
|
438
|
+
length: { type: 'number', description: 'Max bytes to return for paginated download (default 20 MB)' }
|
|
439
|
+
},
|
|
440
|
+
required: ['uid', 'partId']
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'get_unsubscribe_info',
|
|
445
|
+
description: 'Get the List-Unsubscribe header from an email, parsed into email and URL components. Useful for AI-assisted inbox cleanup.',
|
|
446
|
+
inputSchema: {
|
|
447
|
+
type: 'object',
|
|
448
|
+
properties: {
|
|
449
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
450
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }
|
|
451
|
+
},
|
|
452
|
+
required: ['uid']
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: 'mark_older_than_read',
|
|
457
|
+
description: 'Mark all unread emails older than N days as read. Useful for bulk triage of a cluttered inbox.',
|
|
458
|
+
inputSchema: {
|
|
459
|
+
type: 'object',
|
|
460
|
+
properties: {
|
|
461
|
+
days: { type: 'number', description: 'Mark emails older than this many days as read' },
|
|
462
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
463
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
464
|
+
},
|
|
465
|
+
required: ['days']
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'bulk_move_by_domain',
|
|
470
|
+
description: 'Move all emails from a specific domain to a folder. Convenience wrapper around bulk_move with a domain filter.',
|
|
471
|
+
inputSchema: {
|
|
472
|
+
type: 'object',
|
|
473
|
+
properties: {
|
|
474
|
+
domain: { type: 'string', description: 'Sender domain to match (e.g. github.com, substack.com)' },
|
|
475
|
+
targetMailbox: { type: 'string', description: 'Destination folder' },
|
|
476
|
+
sourceMailbox: { type: 'string', description: 'Source mailbox (default INBOX)' },
|
|
477
|
+
dryRun: { type: 'boolean', description: 'Preview only — return count without moving' }
|
|
478
|
+
},
|
|
479
|
+
required: ['domain', 'targetMailbox']
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: 'get_email_raw',
|
|
484
|
+
description: 'Get the raw RFC 2822 source of an email (full headers + MIME body) as base64-encoded data. Useful for debugging or export. Capped at 1 MB.',
|
|
485
|
+
inputSchema: {
|
|
486
|
+
type: 'object',
|
|
487
|
+
properties: {
|
|
488
|
+
uid: { type: 'number', description: 'Email UID' },
|
|
489
|
+
mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }
|
|
490
|
+
},
|
|
491
|
+
required: ['uid']
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'bulk_flag_by_sender',
|
|
496
|
+
description: 'Flag or unflag all emails from a specific sender',
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: 'object',
|
|
499
|
+
properties: {
|
|
500
|
+
sender: { type: 'string', description: 'Sender email address' },
|
|
501
|
+
flagged: { type: 'boolean', description: 'True to flag, false to unflag' },
|
|
502
|
+
mailbox: { type: 'string', description: 'Mailbox (default INBOX)' },
|
|
503
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
504
|
+
},
|
|
505
|
+
required: ['sender', 'flagged']
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: 'archive_older_than',
|
|
510
|
+
description: 'Safely move emails older than N days from a source mailbox to an archive folder. Uses the same safe copy-verify-delete pipeline as bulk_move. Use dryRun: true to preview.',
|
|
511
|
+
inputSchema: {
|
|
512
|
+
type: 'object',
|
|
513
|
+
properties: {
|
|
514
|
+
days: { type: 'number', description: 'Archive emails older than this many days' },
|
|
515
|
+
targetMailbox: { type: 'string', description: 'Destination archive folder (e.g. Archive)' },
|
|
516
|
+
sourceMailbox: { type: 'string', description: 'Source mailbox (default INBOX)' },
|
|
517
|
+
dryRun: { type: 'boolean', description: 'If true, preview what would be moved without moving' }
|
|
518
|
+
},
|
|
519
|
+
required: ['days', 'targetMailbox']
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: 'get_storage_report',
|
|
524
|
+
description: 'Estimate storage usage by size bucket and identify top senders by email size. Uses SEARCH LARGER queries for bucketing and samples large emails for sender analysis.',
|
|
525
|
+
inputSchema: {
|
|
526
|
+
type: 'object',
|
|
527
|
+
properties: {
|
|
528
|
+
mailbox: { type: 'string', description: 'Mailbox to analyze (default INBOX)' },
|
|
529
|
+
sampleSize: { type: 'number', description: 'Max number of large emails to sample for sender analysis (default 100)' }
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: 'get_thread',
|
|
535
|
+
description: 'Find all emails in the same thread as a given email. Uses subject matching + References/In-Reply-To header filtering. Note: iCloud does not support server-side threading — results are approximate.',
|
|
536
|
+
inputSchema: {
|
|
537
|
+
type: 'object',
|
|
538
|
+
properties: {
|
|
539
|
+
uid: { type: 'number', description: 'Email UID to find the thread for' },
|
|
540
|
+
mailbox: { type: 'string', description: 'Mailbox to search (default INBOX)' }
|
|
541
|
+
},
|
|
542
|
+
required: ['uid']
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
// ── Saved Rules ──
|
|
546
|
+
{
|
|
547
|
+
name: 'create_rule',
|
|
548
|
+
description: 'Create a saved rule that applies a specific action to emails matching a set of filters. Rules are stored persistently and can be run on demand or all at once with run_all_rules.',
|
|
549
|
+
inputSchema: {
|
|
550
|
+
type: 'object',
|
|
551
|
+
properties: {
|
|
552
|
+
name: { type: 'string', description: 'Unique rule name (used to run or delete the rule)' },
|
|
553
|
+
description: { type: 'string', description: 'Optional human-readable description of what the rule does' },
|
|
554
|
+
filters: {
|
|
555
|
+
type: 'object',
|
|
556
|
+
description: 'Email filters (same as bulk_move/bulk_delete filters: sender, domain, subject, before, since, unread, flagged, larger, smaller)',
|
|
557
|
+
properties: filtersSchema
|
|
558
|
+
},
|
|
559
|
+
action: {
|
|
560
|
+
type: 'object',
|
|
561
|
+
description: 'Action to apply to matching emails',
|
|
562
|
+
properties: {
|
|
563
|
+
type: { type: 'string', enum: ['move', 'delete', 'mark_read', 'mark_unread', 'flag', 'unflag'], description: 'Action type' },
|
|
564
|
+
targetMailbox: { type: 'string', description: 'Destination folder (required for move)' },
|
|
565
|
+
sourceMailbox: { type: 'string', description: 'Source mailbox (default INBOX)' }
|
|
566
|
+
},
|
|
567
|
+
required: ['type']
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
required: ['name', 'filters', 'action']
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: 'list_rules',
|
|
575
|
+
description: 'List all saved rules with their filters, actions, and run history.',
|
|
576
|
+
inputSchema: { type: 'object', properties: {} }
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: 'run_rule',
|
|
580
|
+
description: 'Run a specific saved rule by name. Use dryRun: true to preview what would be affected without making changes.',
|
|
581
|
+
inputSchema: {
|
|
582
|
+
type: 'object',
|
|
583
|
+
properties: {
|
|
584
|
+
name: { type: 'string', description: 'Rule name to run' },
|
|
585
|
+
dryRun: { type: 'boolean', description: 'If true, preview what would be affected without making changes' }
|
|
586
|
+
},
|
|
587
|
+
required: ['name']
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: 'delete_rule',
|
|
592
|
+
description: 'Delete a saved rule by name.',
|
|
593
|
+
inputSchema: {
|
|
594
|
+
type: 'object',
|
|
595
|
+
properties: {
|
|
596
|
+
name: { type: 'string', description: 'Rule name to delete' },
|
|
597
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
598
|
+
},
|
|
599
|
+
required: ['name']
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: 'run_all_rules',
|
|
604
|
+
description: 'Run all saved rules in sequence. Use dryRun: true to preview all rules without making changes.',
|
|
605
|
+
inputSchema: {
|
|
606
|
+
type: 'object',
|
|
607
|
+
properties: {
|
|
608
|
+
dryRun: { type: 'boolean', description: 'If true, preview all rules without making changes' }
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
// ── SMTP / Email sending ──
|
|
613
|
+
{
|
|
614
|
+
name: 'compose_email',
|
|
615
|
+
description: 'Compose and send a new email via iCloud SMTP. The From address is always your iCloud account. Supports plain text, HTML, or both (multipart/alternative).',
|
|
616
|
+
inputSchema: {
|
|
617
|
+
type: 'object',
|
|
618
|
+
properties: {
|
|
619
|
+
to: { type: 'string', description: 'Recipient email address(es), comma-separated or array' },
|
|
620
|
+
subject: { type: 'string', description: 'Email subject' },
|
|
621
|
+
body: { type: 'string', description: 'Plain text body (used as fallback when html is also provided)' },
|
|
622
|
+
html: { type: 'string', description: 'HTML body. If provided without body, plain text is auto-generated. If provided with body, sends multipart/alternative.' },
|
|
623
|
+
cc: { type: 'string', description: 'CC recipient(s), comma-separated or array' },
|
|
624
|
+
bcc: { type: 'string', description: 'BCC recipient(s), comma-separated or array' },
|
|
625
|
+
replyTo: { type: 'string', description: 'Reply-To address override' }
|
|
626
|
+
},
|
|
627
|
+
required: ['to', 'subject']
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: 'reply_to_email',
|
|
632
|
+
description: 'Reply to an existing email. Automatically sets correct threading headers (In-Reply-To, References) and prefixes the subject with Re:. Supports plain text and/or HTML body.',
|
|
633
|
+
inputSchema: {
|
|
634
|
+
type: 'object',
|
|
635
|
+
properties: {
|
|
636
|
+
uid: { type: 'number', description: 'UID of the email to reply to' },
|
|
637
|
+
body: { type: 'string', description: 'Plain text reply body' },
|
|
638
|
+
html: { type: 'string', description: 'HTML reply body (auto-generates plain text fallback if body not provided)' },
|
|
639
|
+
mailbox: { type: 'string', description: 'Mailbox containing the original email (default INBOX)' },
|
|
640
|
+
replyAll: { type: 'boolean', description: 'If true, reply to all recipients (To + Cc). Default false.' },
|
|
641
|
+
cc: { type: 'string', description: 'Additional CC recipients for this reply' }
|
|
642
|
+
},
|
|
643
|
+
required: ['uid']
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: 'forward_email',
|
|
648
|
+
description: 'Forward an existing email to one or more recipients. Fetches the original email body and includes it as a forwarded message block. Supports plain text and/or HTML note.',
|
|
649
|
+
inputSchema: {
|
|
650
|
+
type: 'object',
|
|
651
|
+
properties: {
|
|
652
|
+
uid: { type: 'number', description: 'UID of the email to forward' },
|
|
653
|
+
to: { type: 'string', description: 'Recipient(s) to forward to, comma-separated or array' },
|
|
654
|
+
note: { type: 'string', description: 'Optional plain text note to prepend before the forwarded message' },
|
|
655
|
+
html: { type: 'string', description: 'Optional HTML note to prepend (overrides plain text note for HTML rendering)' },
|
|
656
|
+
mailbox: { type: 'string', description: 'Mailbox containing the original email (default INBOX)' },
|
|
657
|
+
cc: { type: 'string', description: 'CC recipients' }
|
|
658
|
+
},
|
|
659
|
+
required: ['uid', 'to']
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: 'save_draft',
|
|
664
|
+
description: 'Save a draft email to your iCloud Drafts folder without sending it. Supports plain text, HTML, or both. The draft can be edited and sent later from Mail.app or iCloud.com.',
|
|
665
|
+
inputSchema: {
|
|
666
|
+
type: 'object',
|
|
667
|
+
properties: {
|
|
668
|
+
to: { type: 'string', description: 'Intended recipient(s), comma-separated or array' },
|
|
669
|
+
subject: { type: 'string', description: 'Email subject' },
|
|
670
|
+
body: { type: 'string', description: 'Plain text body (used as fallback when html is also provided)' },
|
|
671
|
+
html: { type: 'string', description: 'HTML body. If provided without body, plain text is auto-generated. If provided with body, saves multipart/alternative.' },
|
|
672
|
+
cc: { type: 'string', description: 'CC recipient(s)' },
|
|
673
|
+
bcc: { type: 'string', description: 'BCC recipient(s)' },
|
|
674
|
+
inReplyTo: { type: 'string', description: 'Message-ID of the email being replied to — sets In-Reply-To header for threading' },
|
|
675
|
+
references: { type: 'string', description: 'Space-separated Message-IDs for the References header — enables full thread linking' },
|
|
676
|
+
account: { type: 'string', description: 'Account whose Drafts folder to save into (e.g. "work", "personal", "school"). Defaults to iCloud.' }
|
|
677
|
+
},
|
|
678
|
+
required: ['to', 'subject']
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
// ── Digest State ──
|
|
682
|
+
{
|
|
683
|
+
name: 'get_digest_state',
|
|
684
|
+
description: 'Get the current inbox digest state — last run timestamp, processed email UIDs (to skip on next run), pending actions, and per-sender skip counts for smart unsubscribe.',
|
|
685
|
+
inputSchema: { type: 'object', properties: {} }
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: 'update_digest_state',
|
|
689
|
+
description: 'Update the digest state after a run. Merges new processed UIDs into the existing list, updates lastRun, replaces pendingActions, and accumulates per-sender skip counts.',
|
|
690
|
+
inputSchema: {
|
|
691
|
+
type: 'object',
|
|
692
|
+
properties: {
|
|
693
|
+
lastRun: { type: 'string', description: 'ISO timestamp of this run' },
|
|
694
|
+
processedUids: { type: 'array', items: { type: 'number' }, description: 'Email UIDs processed in this run — merged with existing and capped at 5000' },
|
|
695
|
+
pendingActions: { type: 'array', description: 'Full replacement list of pending action items to track across runs (deadlines, waiting-for-reply, etc.). Each item: { type, subject, to/from, dueDate?, notes? }' },
|
|
696
|
+
skipCounts: { type: 'object', description: 'Map of sender address to skip count increment for this run, e.g. { "news@example.com": 3 }. Accumulated across runs for smart unsubscribe.' },
|
|
697
|
+
dismissedReminders: { type: 'array', description: 'Full replacement list of reminders the user has deleted from the "claude" list (treated as "not interested"). Each: { title, notes?, dismissedAt (ISO) }. Entries older than 30 days are pruned automatically. Capped at 500.' },
|
|
698
|
+
seenReminders: { type: 'array', description: 'Snapshot of all active reminders in the "claude" list at end of this run — used next run to detect which ones the user deleted. Each: { id, title, notes? }.' }
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
];
|
|
703
|
+
|
|
704
|
+
const _mailTools_NAMES = new Set(mailTools.map((tool) => tool.name));
|
|
705
|
+
|
|
706
|
+
export async function handleMailTool(name, args, ctx) {
|
|
707
|
+
if (!_mailTools_NAMES.has(name)) return undefined;
|
|
708
|
+
const { resolveCreds, resolveMailbox, accounts: ACCOUNTS } = ctx;
|
|
709
|
+
let result;
|
|
710
|
+
if (name === 'list_accounts') {
|
|
711
|
+
result = Object.entries(ACCOUNTS).map(([n, c]) => ({ name: n, host: c.host, smtpHost: c.smtpHost }));
|
|
712
|
+
// ── Metadata tier (15s) ──
|
|
713
|
+
} else if (name === 'get_inbox_summary') {
|
|
714
|
+
const creds = resolveCreds(args.account);
|
|
715
|
+
result = await withTimeout('get_inbox_summary', TIMEOUT.METADATA, () => getInboxSummary(resolveMailbox(args.mailbox || 'INBOX', creds), creds));
|
|
716
|
+
} else if (name === 'get_mailbox_summary') {
|
|
717
|
+
const creds = resolveCreds(args.account);
|
|
718
|
+
result = await withTimeout('get_mailbox_summary', TIMEOUT.METADATA, () => getMailboxSummary(resolveMailbox(args.mailbox, creds), creds));
|
|
719
|
+
} else if (name === 'get_top_senders') {
|
|
720
|
+
const creds = resolveCreds(args.account);
|
|
721
|
+
result = await withTimeout('get_top_senders', TIMEOUT.SCAN, () => getTopSenders(resolveMailbox(args.mailbox || 'INBOX', creds), args.sampleSize || 500, args.maxResults || 20, creds));
|
|
722
|
+
} else if (name === 'get_unread_senders') {
|
|
723
|
+
const creds = resolveCreds(args.account);
|
|
724
|
+
result = await withTimeout('get_unread_senders', TIMEOUT.SCAN, () => getUnreadSenders(resolveMailbox(args.mailbox || 'INBOX', creds), args.sampleSize || 500, args.maxResults || 20, creds));
|
|
725
|
+
} else if (name === 'get_emails_by_sender') {
|
|
726
|
+
const creds = resolveCreds(args.account);
|
|
727
|
+
result = await withTimeout('get_emails_by_sender', TIMEOUT.FETCH, () => getEmailsBySender(args.sender, resolveMailbox(args.mailbox || 'INBOX', creds), args.limit || 10, creds));
|
|
728
|
+
} else if (name === 'read_inbox') {
|
|
729
|
+
const creds = resolveCreds(args.account);
|
|
730
|
+
result = await withTimeout('read_inbox', TIMEOUT.FETCH, () => fetchEmails(resolveMailbox(args.mailbox || 'INBOX', creds), args.limit || 10, args.onlyUnread || false, args.page || 1, creds));
|
|
731
|
+
} else if (name === 'get_email') {
|
|
732
|
+
const creds = resolveCreds(args.account);
|
|
733
|
+
result = await withTimeout('get_email', TIMEOUT.FETCH, () => getEmailContent(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), args.maxChars || 8000, args.includeHeaders || false, creds));
|
|
734
|
+
} else if (name === 'search_emails') {
|
|
735
|
+
const { query, mailbox, limit, queryMode, subjectQuery, bodyQuery, fromQuery, includeSnippet, account, ...filters } = args;
|
|
736
|
+
const creds = resolveCreds(account);
|
|
737
|
+
result = await withTimeout('search_emails', TIMEOUT.FETCH, () => searchEmails(query, resolveMailbox(mailbox || 'INBOX', creds), limit || 10, filters, { queryMode, subjectQuery, bodyQuery, fromQuery, includeSnippet }, creds));
|
|
738
|
+
} else if (name === 'count_emails') {
|
|
739
|
+
const { mailbox, account, ...filters } = args;
|
|
740
|
+
const creds = resolveCreds(account);
|
|
741
|
+
result = await withTimeout('count_emails', TIMEOUT.METADATA, () => countEmails(filters, resolveMailbox(mailbox || 'INBOX', creds), creds));
|
|
742
|
+
} else if (name === 'bulk_move') {
|
|
743
|
+
const { targetMailbox, sourceMailbox, dryRun, limit, account, ...filters } = args;
|
|
744
|
+
const creds = resolveCreds(account);
|
|
745
|
+
result = await bulkMove(filters, resolveMailbox(targetMailbox, creds), resolveMailbox(sourceMailbox || 'INBOX', creds), dryRun || false, limit ?? null, creds);
|
|
746
|
+
} else if (name === 'bulk_delete') {
|
|
747
|
+
const { sourceMailbox, dryRun, account, ...filters } = args;
|
|
748
|
+
const creds = resolveCreds(account);
|
|
749
|
+
result = await bulkDelete(filters, resolveMailbox(sourceMailbox || 'INBOX', creds), dryRun || false, creds);
|
|
750
|
+
// ── Single-email tier (15s) ──
|
|
751
|
+
} else if (name === 'bulk_flag') {
|
|
752
|
+
const { flagged, mailbox, account, dryRun, ...filters } = args;
|
|
753
|
+
const creds = resolveCreds(account);
|
|
754
|
+
result = await withTimeout('bulk_flag', TIMEOUT.BULK_OP, () => bulkFlag(filters, flagged, resolveMailbox(mailbox || 'INBOX', creds), creds, dryRun || false));
|
|
755
|
+
} else if (name === 'bulk_delete_by_sender') {
|
|
756
|
+
const creds = resolveCreds(args.account);
|
|
757
|
+
result = await withTimeout('bulk_delete_by_sender', TIMEOUT.BULK_OP, () => bulkDeleteBySender(args.sender, resolveMailbox(args.mailbox || 'INBOX', creds), creds, args.dryRun || false));
|
|
758
|
+
} else if (name === 'bulk_move_by_sender') {
|
|
759
|
+
const creds = resolveCreds(args.account);
|
|
760
|
+
result = await bulkMoveBySender(args.sender, resolveMailbox(args.targetMailbox, creds), resolveMailbox(args.sourceMailbox || 'INBOX', creds), args.dryRun || false, creds);
|
|
761
|
+
} else if (name === 'bulk_delete_by_subject') {
|
|
762
|
+
const creds = resolveCreds(args.account);
|
|
763
|
+
result = await withTimeout('bulk_delete_by_subject', TIMEOUT.BULK_OP, () => bulkDeleteBySubject(args.subject, resolveMailbox(args.mailbox || 'INBOX', creds), creds, args.dryRun || false));
|
|
764
|
+
} else if (name === 'bulk_mark_read') {
|
|
765
|
+
const creds = resolveCreds(args.account);
|
|
766
|
+
result = await withTimeout('bulk_mark_read', TIMEOUT.BULK_OP, () => bulkMarkRead(resolveMailbox(args.mailbox || 'INBOX', creds), args.sender || null, creds, args.dryRun || false));
|
|
767
|
+
} else if (name === 'bulk_mark_unread') {
|
|
768
|
+
const creds = resolveCreds(args.account);
|
|
769
|
+
result = await withTimeout('bulk_mark_unread', TIMEOUT.BULK_OP, () => bulkMarkUnread(resolveMailbox(args.mailbox || 'INBOX', creds), args.sender || null, creds, args.dryRun || false));
|
|
770
|
+
} else if (name === 'delete_older_than') {
|
|
771
|
+
const creds = resolveCreds(args.account);
|
|
772
|
+
result = await withTimeout('delete_older_than', TIMEOUT.BULK_OP, () => deleteOlderThan(args.days, resolveMailbox(args.mailbox || 'INBOX', creds), creds, args.dryRun || false));
|
|
773
|
+
} else if (name === 'get_emails_by_date_range') {
|
|
774
|
+
const creds = resolveCreds(args.account);
|
|
775
|
+
result = await withTimeout('get_emails_by_date_range', TIMEOUT.FETCH, () => getEmailsByDateRange(args.startDate, args.endDate, resolveMailbox(args.mailbox || 'INBOX', creds), args.limit || 10, creds));
|
|
776
|
+
// ── Scan tier (60s) ──
|
|
777
|
+
} else if (name === 'flag_email') {
|
|
778
|
+
const creds = resolveCreds(args.account);
|
|
779
|
+
result = await withTimeout('flag_email', TIMEOUT.SINGLE, () => flagEmail(args.uid, args.flagged, resolveMailbox(args.mailbox || 'INBOX', creds), creds));
|
|
780
|
+
} else if (name === 'mark_as_read') {
|
|
781
|
+
const creds = resolveCreds(args.account);
|
|
782
|
+
result = await withTimeout('mark_as_read', TIMEOUT.SINGLE, () => markAsRead(args.uid, args.seen, resolveMailbox(args.mailbox || 'INBOX', creds), creds));
|
|
783
|
+
} else if (name === 'delete_email') {
|
|
784
|
+
const creds = resolveCreds(args.account);
|
|
785
|
+
result = await withTimeout('delete_email', TIMEOUT.SINGLE, () => deleteEmail(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), creds, args.dryRun || false));
|
|
786
|
+
} else if (name === 'move_email') {
|
|
787
|
+
const creds = resolveCreds(args.account);
|
|
788
|
+
result = await withTimeout('move_email', TIMEOUT.SINGLE, () => moveEmail(args.uid, resolveMailbox(args.targetMailbox, creds), resolveMailbox(args.sourceMailbox || 'INBOX', creds), creds, args.dryRun || false));
|
|
789
|
+
// ── Move status (synchronous, no timeout needed) ──
|
|
790
|
+
} else if (name === 'list_mailboxes') {
|
|
791
|
+
const creds = resolveCreds(args.account);
|
|
792
|
+
result = await withTimeout('list_mailboxes', TIMEOUT.METADATA, () => listMailboxes(creds));
|
|
793
|
+
} else if (name === 'create_mailbox') {
|
|
794
|
+
const creds = resolveCreds(args.account);
|
|
795
|
+
result = await withTimeout('create_mailbox', TIMEOUT.METADATA, () => createMailbox(args.name, creds));
|
|
796
|
+
} else if (name === 'rename_mailbox') {
|
|
797
|
+
const creds = resolveCreds(args.account);
|
|
798
|
+
result = await renameMailbox(args.oldName, args.newName, creds); // already has its own 15s timeout
|
|
799
|
+
} else if (name === 'delete_mailbox') {
|
|
800
|
+
const creds = resolveCreds(args.account);
|
|
801
|
+
result = await deleteMailbox(args.name, creds, args.dryRun || false); // already has its own 15s timeout
|
|
802
|
+
// ── Fetch tier (30s) ──
|
|
803
|
+
} else if (name === 'empty_trash') {
|
|
804
|
+
const creds = resolveCreds(args.account);
|
|
805
|
+
result = await withTimeout('empty_trash', TIMEOUT.BULK_OP, () => emptyTrash(args.dryRun || false, creds));
|
|
806
|
+
// ── No top-level timeout — chunked with internal timeouts ──
|
|
807
|
+
} else if (name === 'get_move_status') {
|
|
808
|
+
result = getMoveStatus();
|
|
809
|
+
} else if (name === 'abandon_move') {
|
|
810
|
+
result = abandonMove();
|
|
811
|
+
// ── Session log (synchronous, no timeout needed) ──
|
|
812
|
+
} else if (name === 'log_write') {
|
|
813
|
+
result = logWrite(args.step);
|
|
814
|
+
} else if (name === 'log_read') {
|
|
815
|
+
result = logRead();
|
|
816
|
+
} else if (name === 'log_clear') {
|
|
817
|
+
result = logClear();
|
|
818
|
+
// ── Digest state (synchronous, no timeout needed) ──
|
|
819
|
+
} else if (name === 'list_attachments') {
|
|
820
|
+
const creds = resolveCreds(args.account);
|
|
821
|
+
result = await withTimeout('list_attachments', TIMEOUT.FETCH, () => listAttachments(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), creds));
|
|
822
|
+
} else if (name === 'get_attachment') {
|
|
823
|
+
const creds = resolveCreds(args.account);
|
|
824
|
+
result = await withTimeout('get_attachment', TIMEOUT.FETCH, () => getAttachment(args.uid, args.partId, resolveMailbox(args.mailbox || 'INBOX', creds), args.offset ?? null, args.length ?? null, creds));
|
|
825
|
+
} else if (name === 'get_unsubscribe_info') {
|
|
826
|
+
const creds = resolveCreds(args.account);
|
|
827
|
+
result = await withTimeout('get_unsubscribe_info', TIMEOUT.FETCH, () => getUnsubscribeInfo(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), creds));
|
|
828
|
+
} else if (name === 'mark_older_than_read') {
|
|
829
|
+
const creds = resolveCreds(args.account);
|
|
830
|
+
result = await withTimeout('mark_older_than_read', TIMEOUT.BULK_OP, () => markOlderThanRead(args.days, resolveMailbox(args.mailbox || 'INBOX', creds), creds, args.dryRun || false));
|
|
831
|
+
} else if (name === 'bulk_move_by_domain') {
|
|
832
|
+
const creds = resolveCreds(args.account);
|
|
833
|
+
result = await bulkMoveByDomain(args.domain, resolveMailbox(args.targetMailbox, creds), resolveMailbox(args.sourceMailbox || 'INBOX', creds), args.dryRun || false, creds);
|
|
834
|
+
} else if (name === 'get_email_raw') {
|
|
835
|
+
const creds = resolveCreds(args.account);
|
|
836
|
+
result = await withTimeout('get_email_raw', TIMEOUT.FETCH, () => getEmailRaw(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), creds));
|
|
837
|
+
} else if (name === 'bulk_flag_by_sender') {
|
|
838
|
+
const creds = resolveCreds(args.account);
|
|
839
|
+
result = await withTimeout('bulk_flag_by_sender', TIMEOUT.BULK_OP, () => bulkFlagBySender(args.sender, args.flagged, resolveMailbox(args.mailbox || 'INBOX', creds), creds, args.dryRun || false));
|
|
840
|
+
} else if (name === 'archive_older_than') {
|
|
841
|
+
const creds = resolveCreds(args.account);
|
|
842
|
+
result = await archiveOlderThan(args.days, resolveMailbox(args.targetMailbox, creds), resolveMailbox(args.sourceMailbox || 'INBOX', creds), args.dryRun || false, creds);
|
|
843
|
+
} else if (name === 'get_storage_report') {
|
|
844
|
+
const creds = resolveCreds(args.account);
|
|
845
|
+
result = await withTimeout('get_storage_report', TIMEOUT.SCAN, () => getStorageReport(resolveMailbox(args.mailbox || 'INBOX', creds), args.sampleSize || 100, creds));
|
|
846
|
+
// ── Bulk operation tier (60s) ──
|
|
847
|
+
} else if (name === 'get_thread') {
|
|
848
|
+
const creds = resolveCreds(args.account);
|
|
849
|
+
result = await withTimeout('get_thread', TIMEOUT.FETCH, () => getThread(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), creds));
|
|
850
|
+
} else if (name === 'create_rule') {
|
|
851
|
+
result = createRule(args.name, args.filters || {}, args.action, args.description || '');
|
|
852
|
+
} else if (name === 'list_rules') {
|
|
853
|
+
result = listRules();
|
|
854
|
+
} else if (name === 'run_rule') {
|
|
855
|
+
result = await runRule(args.name, args.dryRun || false);
|
|
856
|
+
} else if (name === 'delete_rule') {
|
|
857
|
+
result = deleteRule(args.name, args.dryRun || false);
|
|
858
|
+
} else if (name === 'run_all_rules') {
|
|
859
|
+
result = await runAllRules(args.dryRun || false);
|
|
860
|
+
// ── SMTP (email sending — uses SCAN tier 60s for two-phase fetch+send) ──
|
|
861
|
+
} else if (name === 'compose_email') {
|
|
862
|
+
const creds = resolveCreds(args.account);
|
|
863
|
+
result = await withTimeout('compose_email', TIMEOUT.SCAN, () =>
|
|
864
|
+
composeEmail(args.to, args.subject, args.body, { html: args.html, cc: args.cc, bcc: args.bcc, replyTo: args.replyTo }, creds)
|
|
865
|
+
);
|
|
866
|
+
} else if (name === 'reply_to_email') {
|
|
867
|
+
const creds = resolveCreds(args.account);
|
|
868
|
+
const origEmail = await withTimeout('get_email_for_reply', TIMEOUT.FETCH, () =>
|
|
869
|
+
getEmailContent(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), 5000, true, creds)
|
|
870
|
+
);
|
|
871
|
+
result = await withTimeout('reply_to_email', TIMEOUT.FETCH, () =>
|
|
872
|
+
replyToEmail(origEmail, args.body, { html: args.html, replyAll: args.replyAll || false, cc: args.cc }, creds)
|
|
873
|
+
);
|
|
874
|
+
} else if (name === 'forward_email') {
|
|
875
|
+
const creds = resolveCreds(args.account);
|
|
876
|
+
const origEmail = await withTimeout('get_email_for_forward', TIMEOUT.FETCH, () =>
|
|
877
|
+
getEmailContent(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), 5000, false, creds)
|
|
878
|
+
);
|
|
879
|
+
result = await withTimeout('forward_email', TIMEOUT.FETCH, () =>
|
|
880
|
+
forwardEmail(origEmail, args.to, args.note || '', { html: args.html, cc: args.cc }, creds)
|
|
881
|
+
);
|
|
882
|
+
} else if (name === 'save_draft') {
|
|
883
|
+
const creds = resolveCreds(args.account);
|
|
884
|
+
result = await withTimeout('save_draft', TIMEOUT.FETCH, () =>
|
|
885
|
+
saveDraft(args.to, args.subject, args.body, { html: args.html, cc: args.cc, bcc: args.bcc, inReplyTo: args.inReplyTo, references: args.references }, creds)
|
|
886
|
+
);
|
|
887
|
+
// ── CardDAV / Contacts (FETCH tier 30s) ──
|
|
888
|
+
} else if (name === 'get_digest_state') {
|
|
889
|
+
result = getDigestState();
|
|
890
|
+
} else if (name === 'update_digest_state') {
|
|
891
|
+
result = updateDigestState({
|
|
892
|
+
lastRun: args.lastRun,
|
|
893
|
+
processedUids: args.processedUids,
|
|
894
|
+
pendingActions: args.pendingActions,
|
|
895
|
+
skipCounts: args.skipCounts,
|
|
896
|
+
dismissedReminders: args.dismissedReminders,
|
|
897
|
+
seenReminders: args.seenReminders
|
|
898
|
+
});
|
|
899
|
+
// ── Saved rules (synchronous CRUD; run_rule/run_all_rules use internal chunk timeouts) ──
|
|
900
|
+
}
|
|
901
|
+
return result;
|
|
902
|
+
}
|
|
903
|
+
|