@littlebearapps/outlook-assistant 3.3.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/.env.example +22 -0
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/advanced/index.js +652 -0
- package/auth/index.js +32 -0
- package/auth/oauth-server.js +233 -0
- package/auth/token-manager.js +105 -0
- package/auth/token-storage.js +359 -0
- package/auth/tools.js +159 -0
- package/calendar/accept.js +72 -0
- package/calendar/cancel.js +72 -0
- package/calendar/create.js +115 -0
- package/calendar/decline.js +72 -0
- package/calendar/delete.js +67 -0
- package/calendar/index.js +130 -0
- package/calendar/list.js +108 -0
- package/categories/index.js +955 -0
- package/config.js +95 -0
- package/contacts/index.js +754 -0
- package/email/attachments.js +365 -0
- package/email/conversations.js +666 -0
- package/email/delta.js +210 -0
- package/email/export.js +572 -0
- package/email/folder-utils.js +192 -0
- package/email/headers.js +344 -0
- package/email/index.js +537 -0
- package/email/list.js +136 -0
- package/email/mark-as-read.js +114 -0
- package/email/mime.js +286 -0
- package/email/read.js +161 -0
- package/email/search.js +628 -0
- package/email/send.js +169 -0
- package/folder/create.js +137 -0
- package/folder/delete.js +108 -0
- package/folder/index.js +112 -0
- package/folder/list.js +289 -0
- package/folder/move.js +186 -0
- package/folder/stats.js +322 -0
- package/index.js +162 -0
- package/llms.txt +76 -0
- package/outlook-auth-server.js +384 -0
- package/package.json +97 -0
- package/rules/create.js +273 -0
- package/rules/index.js +276 -0
- package/rules/list.js +216 -0
- package/settings/index.js +678 -0
- package/utils/field-presets.js +311 -0
- package/utils/graph-api.js +268 -0
- package/utils/mock-data.js +154 -0
- package/utils/odata-helpers.js +33 -0
- package/utils/response-formatter.js +457 -0
- package/utils/safety.js +123 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categories module for Outlook Assistant server
|
|
3
|
+
*
|
|
4
|
+
* Manages Outlook master categories and Focused Inbox overrides.
|
|
5
|
+
*/
|
|
6
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
7
|
+
const { ensureAuthenticated } = require('../auth');
|
|
8
|
+
|
|
9
|
+
// Category color presets (Outlook uses these names)
|
|
10
|
+
const CATEGORY_COLORS = [
|
|
11
|
+
'preset0',
|
|
12
|
+
'preset1',
|
|
13
|
+
'preset2',
|
|
14
|
+
'preset3',
|
|
15
|
+
'preset4',
|
|
16
|
+
'preset5',
|
|
17
|
+
'preset6',
|
|
18
|
+
'preset7',
|
|
19
|
+
'preset8',
|
|
20
|
+
'preset9',
|
|
21
|
+
'preset10',
|
|
22
|
+
'preset11',
|
|
23
|
+
'preset12',
|
|
24
|
+
'preset13',
|
|
25
|
+
'preset14',
|
|
26
|
+
'preset15',
|
|
27
|
+
'preset16',
|
|
28
|
+
'preset17',
|
|
29
|
+
'preset18',
|
|
30
|
+
'preset19',
|
|
31
|
+
'preset20',
|
|
32
|
+
'preset21',
|
|
33
|
+
'preset22',
|
|
34
|
+
'preset23',
|
|
35
|
+
'preset24',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Map preset numbers to human-readable colors
|
|
39
|
+
const COLOR_NAMES = {
|
|
40
|
+
preset0: 'Red',
|
|
41
|
+
preset1: 'Orange',
|
|
42
|
+
preset2: 'Brown',
|
|
43
|
+
preset3: 'Yellow',
|
|
44
|
+
preset4: 'Green',
|
|
45
|
+
preset5: 'Teal',
|
|
46
|
+
preset6: 'Olive',
|
|
47
|
+
preset7: 'Blue',
|
|
48
|
+
preset8: 'Purple',
|
|
49
|
+
preset9: 'Cranberry',
|
|
50
|
+
preset10: 'Steel',
|
|
51
|
+
preset11: 'DarkSteel',
|
|
52
|
+
preset12: 'Gray',
|
|
53
|
+
preset13: 'DarkGray',
|
|
54
|
+
preset14: 'Black',
|
|
55
|
+
preset15: 'DarkRed',
|
|
56
|
+
preset16: 'DarkOrange',
|
|
57
|
+
preset17: 'DarkBrown',
|
|
58
|
+
preset18: 'DarkYellow',
|
|
59
|
+
preset19: 'DarkGreen',
|
|
60
|
+
preset20: 'DarkTeal',
|
|
61
|
+
preset21: 'DarkOlive',
|
|
62
|
+
preset22: 'DarkBlue',
|
|
63
|
+
preset23: 'DarkPurple',
|
|
64
|
+
preset24: 'DarkCranberry',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format a category for display
|
|
69
|
+
*/
|
|
70
|
+
function formatCategory(category) {
|
|
71
|
+
const colorName = COLOR_NAMES[category.color] || category.color;
|
|
72
|
+
return {
|
|
73
|
+
id: category.id,
|
|
74
|
+
displayName: category.displayName,
|
|
75
|
+
color: category.color,
|
|
76
|
+
colorName: colorName,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* List master categories handler
|
|
82
|
+
*/
|
|
83
|
+
async function handleListCategories(args) {
|
|
84
|
+
const outputVerbosity = args.outputVerbosity || 'standard';
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const accessToken = await ensureAuthenticated();
|
|
88
|
+
|
|
89
|
+
const response = await callGraphAPI(
|
|
90
|
+
accessToken,
|
|
91
|
+
'GET',
|
|
92
|
+
'me/outlook/masterCategories'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const categories = response.value || [];
|
|
96
|
+
|
|
97
|
+
if (categories.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: 'No categories found. Use manage-category with action=create to create your first category.',
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Format output based on verbosity
|
|
109
|
+
let output = [];
|
|
110
|
+
output.push(`# Master Categories (${categories.length})\n`);
|
|
111
|
+
|
|
112
|
+
if (outputVerbosity === 'minimal') {
|
|
113
|
+
output.push(categories.map((c) => `- ${c.displayName}`).join('\n'));
|
|
114
|
+
} else {
|
|
115
|
+
output.push('| Category | Color | ID |');
|
|
116
|
+
output.push('|----------|-------|-----|');
|
|
117
|
+
categories.forEach((cat) => {
|
|
118
|
+
const colorName = COLOR_NAMES[cat.color] || cat.color;
|
|
119
|
+
const idDisplay =
|
|
120
|
+
outputVerbosity === 'full' ? cat.id : cat.id.substring(0, 8) + '...';
|
|
121
|
+
output.push(`| ${cat.displayName} | ${colorName} | ${idDisplay} |`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: output.join('\n'),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
_meta: {
|
|
133
|
+
count: categories.length,
|
|
134
|
+
categories: categories.map(formatCategory),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error.message === 'Authentication required') {
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: `Error listing categories: ${error.message}`,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create category handler
|
|
161
|
+
*/
|
|
162
|
+
async function handleCreateCategory(args) {
|
|
163
|
+
const { displayName, color } = args;
|
|
164
|
+
|
|
165
|
+
if (!displayName) {
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: 'Category name (displayName) is required.',
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Validate color if provided
|
|
177
|
+
if (color && !CATEGORY_COLORS.includes(color)) {
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: 'text',
|
|
182
|
+
text: `Invalid color. Valid options: ${CATEGORY_COLORS.join(', ')}\n\nColor names: ${Object.entries(
|
|
183
|
+
COLOR_NAMES
|
|
184
|
+
)
|
|
185
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
186
|
+
.join(', ')}`,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const accessToken = await ensureAuthenticated();
|
|
194
|
+
|
|
195
|
+
const categoryData = {
|
|
196
|
+
displayName: displayName,
|
|
197
|
+
color: color || 'preset0', // Default to red
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const response = await callGraphAPI(
|
|
201
|
+
accessToken,
|
|
202
|
+
'POST',
|
|
203
|
+
'me/outlook/masterCategories',
|
|
204
|
+
categoryData
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const colorName = COLOR_NAMES[response.color] || response.color;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: `Category created!\n\n**Name**: ${response.displayName}\n**Color**: ${colorName} (${response.color})\n**ID**: ${response.id}`,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
_meta: {
|
|
217
|
+
category: formatCategory(response),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error.message === 'Authentication required') {
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: 'text',
|
|
226
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (error.message.includes('already exists')) {
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: `A category named "${displayName}" already exists.`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: 'text',
|
|
247
|
+
text: `Error creating category: ${error.message}`,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Update category handler
|
|
256
|
+
*/
|
|
257
|
+
async function handleUpdateCategory(args) {
|
|
258
|
+
const { id, displayName, color } = args;
|
|
259
|
+
|
|
260
|
+
if (!id) {
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: 'Category ID is required. Use manage-category with action=list to find category IDs.',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!displayName && !color) {
|
|
272
|
+
return {
|
|
273
|
+
content: [
|
|
274
|
+
{
|
|
275
|
+
type: 'text',
|
|
276
|
+
text: 'At least one of displayName or color must be provided.',
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Validate color if provided
|
|
283
|
+
if (color && !CATEGORY_COLORS.includes(color)) {
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: 'text',
|
|
288
|
+
text: `Invalid color. Valid options: ${CATEGORY_COLORS.join(', ')}`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const accessToken = await ensureAuthenticated();
|
|
296
|
+
|
|
297
|
+
const updateData = {};
|
|
298
|
+
if (displayName) updateData.displayName = displayName;
|
|
299
|
+
if (color) updateData.color = color;
|
|
300
|
+
|
|
301
|
+
const response = await callGraphAPI(
|
|
302
|
+
accessToken,
|
|
303
|
+
'PATCH',
|
|
304
|
+
`me/outlook/masterCategories/${id}`,
|
|
305
|
+
updateData
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const colorName = COLOR_NAMES[response.color] || response.color;
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
content: [
|
|
312
|
+
{
|
|
313
|
+
type: 'text',
|
|
314
|
+
text: `Category updated!\n\n**Name**: ${response.displayName}\n**Color**: ${colorName} (${response.color})\n**ID**: ${response.id}`,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
_meta: {
|
|
318
|
+
category: formatCategory(response),
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error.message === 'Authentication required') {
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: 'text',
|
|
327
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
content: [
|
|
334
|
+
{
|
|
335
|
+
type: 'text',
|
|
336
|
+
text: `Error updating category: ${error.message}`,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Delete category handler
|
|
345
|
+
*/
|
|
346
|
+
async function handleDeleteCategory(args) {
|
|
347
|
+
const { id } = args;
|
|
348
|
+
|
|
349
|
+
if (!id) {
|
|
350
|
+
return {
|
|
351
|
+
content: [
|
|
352
|
+
{
|
|
353
|
+
type: 'text',
|
|
354
|
+
text: 'Category ID is required. Use manage-category with action=list to find category IDs.',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const accessToken = await ensureAuthenticated();
|
|
362
|
+
|
|
363
|
+
await callGraphAPI(
|
|
364
|
+
accessToken,
|
|
365
|
+
'DELETE',
|
|
366
|
+
`me/outlook/masterCategories/${id}`
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
content: [
|
|
371
|
+
{
|
|
372
|
+
type: 'text',
|
|
373
|
+
text: `Category deleted successfully.`,
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
};
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error.message === 'Authentication required') {
|
|
379
|
+
return {
|
|
380
|
+
content: [
|
|
381
|
+
{
|
|
382
|
+
type: 'text',
|
|
383
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (error.message.includes('not found') || error.message.includes('404')) {
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: 'text',
|
|
394
|
+
text: `Category not found. Use manage-category with action=list to see available categories.`,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
content: [
|
|
402
|
+
{
|
|
403
|
+
type: 'text',
|
|
404
|
+
text: `Error deleting category: ${error.message}`,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Apply category to message(s) handler
|
|
413
|
+
*/
|
|
414
|
+
async function handleApplyCategory(args) {
|
|
415
|
+
const { messageId, messageIds, categories, action } = args;
|
|
416
|
+
|
|
417
|
+
// Support single ID or array
|
|
418
|
+
const ids = messageIds || (messageId ? [messageId] : []);
|
|
419
|
+
|
|
420
|
+
if (ids.length === 0) {
|
|
421
|
+
return {
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: 'text',
|
|
425
|
+
text: 'Message ID (messageId) or IDs (messageIds) required.',
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!categories || !Array.isArray(categories) || categories.length === 0) {
|
|
432
|
+
return {
|
|
433
|
+
content: [
|
|
434
|
+
{
|
|
435
|
+
type: 'text',
|
|
436
|
+
text: 'Categories array is required. Provide category display names.',
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const applyAction = action || 'set'; // 'set', 'add', 'remove'
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const accessToken = await ensureAuthenticated();
|
|
446
|
+
|
|
447
|
+
const results = [];
|
|
448
|
+
const errors = [];
|
|
449
|
+
|
|
450
|
+
for (const id of ids) {
|
|
451
|
+
try {
|
|
452
|
+
let newCategories = categories;
|
|
453
|
+
|
|
454
|
+
// If adding or removing, get current categories first
|
|
455
|
+
if (applyAction === 'add' || applyAction === 'remove') {
|
|
456
|
+
const current = await callGraphAPI(
|
|
457
|
+
accessToken,
|
|
458
|
+
'GET',
|
|
459
|
+
`me/messages/${id}`,
|
|
460
|
+
null,
|
|
461
|
+
{ $select: 'categories' }
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const currentCategories = current.categories || [];
|
|
465
|
+
|
|
466
|
+
if (applyAction === 'add') {
|
|
467
|
+
newCategories = [...new Set([...currentCategories, ...categories])];
|
|
468
|
+
} else if (applyAction === 'remove') {
|
|
469
|
+
newCategories = currentCategories.filter(
|
|
470
|
+
(c) => !categories.includes(c)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
await callGraphAPI(accessToken, 'PATCH', `me/messages/${id}`, {
|
|
476
|
+
categories: newCategories,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
results.push({ id, success: true, categories: newCategories });
|
|
480
|
+
} catch (err) {
|
|
481
|
+
errors.push({ id, error: err.message });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let output = [];
|
|
486
|
+
|
|
487
|
+
if (results.length > 0) {
|
|
488
|
+
output.push(
|
|
489
|
+
`Categories ${applyAction === 'remove' ? 'removed from' : 'applied to'} ${results.length} message(s)\n`
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
if (ids.length <= 5) {
|
|
493
|
+
results.forEach((r) => {
|
|
494
|
+
output.push(
|
|
495
|
+
`- ${r.id.substring(0, 20)}...: [${r.categories.join(', ')}]`
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (errors.length > 0) {
|
|
502
|
+
output.push(`\n${errors.length} error(s):`);
|
|
503
|
+
errors.forEach((e) => {
|
|
504
|
+
output.push(`- ${e.id.substring(0, 20)}...: ${e.error}`);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
content: [
|
|
510
|
+
{
|
|
511
|
+
type: 'text',
|
|
512
|
+
text: output.join('\n'),
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
_meta: {
|
|
516
|
+
successful: results.length,
|
|
517
|
+
failed: errors.length,
|
|
518
|
+
action: applyAction,
|
|
519
|
+
results,
|
|
520
|
+
errors,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (error.message === 'Authentication required') {
|
|
525
|
+
return {
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: 'text',
|
|
529
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
content: [
|
|
536
|
+
{
|
|
537
|
+
type: 'text',
|
|
538
|
+
text: `Error applying categories: ${error.message}`,
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get Focused Inbox overrides handler
|
|
547
|
+
*/
|
|
548
|
+
async function handleGetFocusedInboxOverrides(args) {
|
|
549
|
+
const outputVerbosity = args.outputVerbosity || 'standard';
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const accessToken = await ensureAuthenticated();
|
|
553
|
+
|
|
554
|
+
const response = await callGraphAPI(
|
|
555
|
+
accessToken,
|
|
556
|
+
'GET',
|
|
557
|
+
'me/inferenceClassification/overrides'
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
const overrides = response.value || [];
|
|
561
|
+
|
|
562
|
+
if (overrides.length === 0) {
|
|
563
|
+
return {
|
|
564
|
+
content: [
|
|
565
|
+
{
|
|
566
|
+
type: 'text',
|
|
567
|
+
text: 'No Focused Inbox overrides configured.\n\nUse manage-focused-inbox with action=set to always show emails from specific senders in Focused or Other.',
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let output = [];
|
|
574
|
+
output.push(`# Focused Inbox Overrides (${overrides.length})\n`);
|
|
575
|
+
|
|
576
|
+
// Group by classification
|
|
577
|
+
const focused = overrides.filter((o) => o.classifyAs === 'focused');
|
|
578
|
+
const other = overrides.filter((o) => o.classifyAs === 'other');
|
|
579
|
+
|
|
580
|
+
if (focused.length > 0) {
|
|
581
|
+
output.push('## Always Focused');
|
|
582
|
+
focused.forEach((o) => {
|
|
583
|
+
const addr = o.senderEmailAddress;
|
|
584
|
+
if (outputVerbosity === 'minimal') {
|
|
585
|
+
output.push(`- ${addr.address}`);
|
|
586
|
+
} else {
|
|
587
|
+
output.push(`- **${addr.name || addr.address}** <${addr.address}>`);
|
|
588
|
+
if (outputVerbosity === 'full') {
|
|
589
|
+
output.push(` - ID: ${o.id}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
output.push('');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (other.length > 0) {
|
|
597
|
+
output.push('## Always Other');
|
|
598
|
+
other.forEach((o) => {
|
|
599
|
+
const addr = o.senderEmailAddress;
|
|
600
|
+
if (outputVerbosity === 'minimal') {
|
|
601
|
+
output.push(`- ${addr.address}`);
|
|
602
|
+
} else {
|
|
603
|
+
output.push(`- **${addr.name || addr.address}** <${addr.address}>`);
|
|
604
|
+
if (outputVerbosity === 'full') {
|
|
605
|
+
output.push(` - ID: ${o.id}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
content: [
|
|
613
|
+
{
|
|
614
|
+
type: 'text',
|
|
615
|
+
text: output.join('\n'),
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
_meta: {
|
|
619
|
+
count: overrides.length,
|
|
620
|
+
focused: focused.length,
|
|
621
|
+
other: other.length,
|
|
622
|
+
overrides: overrides,
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error.message === 'Authentication required') {
|
|
627
|
+
return {
|
|
628
|
+
content: [
|
|
629
|
+
{
|
|
630
|
+
type: 'text',
|
|
631
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
content: [
|
|
638
|
+
{
|
|
639
|
+
type: 'text',
|
|
640
|
+
text: `Error getting Focused Inbox overrides: ${error.message}`,
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Set Focused Inbox override handler
|
|
649
|
+
*/
|
|
650
|
+
async function handleSetFocusedInboxOverride(args) {
|
|
651
|
+
const { emailAddress, name, classifyAs } = args;
|
|
652
|
+
// Note: args.action is used by the consolidated dispatcher and also checked here for 'delete'
|
|
653
|
+
const overrideAction = args.action;
|
|
654
|
+
|
|
655
|
+
if (!emailAddress) {
|
|
656
|
+
return {
|
|
657
|
+
content: [
|
|
658
|
+
{
|
|
659
|
+
type: 'text',
|
|
660
|
+
text: 'Email address is required.',
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const classification = classifyAs || 'focused';
|
|
667
|
+
if (!['focused', 'other'].includes(classification)) {
|
|
668
|
+
return {
|
|
669
|
+
content: [
|
|
670
|
+
{
|
|
671
|
+
type: 'text',
|
|
672
|
+
text: "classifyAs must be 'focused' or 'other'.",
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const accessToken = await ensureAuthenticated();
|
|
680
|
+
|
|
681
|
+
// Check if override already exists
|
|
682
|
+
const existingResponse = await callGraphAPI(
|
|
683
|
+
accessToken,
|
|
684
|
+
'GET',
|
|
685
|
+
'me/inferenceClassification/overrides'
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
const existing = (existingResponse.value || []).find(
|
|
689
|
+
(o) =>
|
|
690
|
+
o.senderEmailAddress.address.toLowerCase() ===
|
|
691
|
+
emailAddress.toLowerCase()
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
// Handle delete action
|
|
695
|
+
if (overrideAction === 'delete') {
|
|
696
|
+
if (!existing) {
|
|
697
|
+
return {
|
|
698
|
+
content: [
|
|
699
|
+
{
|
|
700
|
+
type: 'text',
|
|
701
|
+
text: `No override found for ${emailAddress}.`,
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
await callGraphAPI(
|
|
708
|
+
accessToken,
|
|
709
|
+
'DELETE',
|
|
710
|
+
`me/inferenceClassification/overrides/${existing.id}`
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
content: [
|
|
715
|
+
{
|
|
716
|
+
type: 'text',
|
|
717
|
+
text: `Removed override for ${emailAddress}. Emails will now follow normal Focused Inbox rules.`,
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Create or update override
|
|
724
|
+
const overrideData = {
|
|
725
|
+
classifyAs: classification,
|
|
726
|
+
senderEmailAddress: {
|
|
727
|
+
address: emailAddress,
|
|
728
|
+
name: name || emailAddress,
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
let response;
|
|
733
|
+
if (existing) {
|
|
734
|
+
// Update existing
|
|
735
|
+
response = await callGraphAPI(
|
|
736
|
+
accessToken,
|
|
737
|
+
'PATCH',
|
|
738
|
+
`me/inferenceClassification/overrides/${existing.id}`,
|
|
739
|
+
overrideData
|
|
740
|
+
);
|
|
741
|
+
} else {
|
|
742
|
+
// Create new
|
|
743
|
+
response = await callGraphAPI(
|
|
744
|
+
accessToken,
|
|
745
|
+
'POST',
|
|
746
|
+
'me/inferenceClassification/overrides',
|
|
747
|
+
overrideData
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const actionWord = existing ? 'Updated' : 'Created';
|
|
752
|
+
const destination =
|
|
753
|
+
classification === 'focused' ? 'Focused inbox' : 'Other';
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
content: [
|
|
757
|
+
{
|
|
758
|
+
type: 'text',
|
|
759
|
+
text: `${actionWord} override!\n\nEmails from **${response.senderEmailAddress.name || emailAddress}** <${emailAddress}> will always go to **${destination}**.`,
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
_meta: {
|
|
763
|
+
action: existing ? 'updated' : 'created',
|
|
764
|
+
override: response,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
} catch (error) {
|
|
768
|
+
if (error.message === 'Authentication required') {
|
|
769
|
+
return {
|
|
770
|
+
content: [
|
|
771
|
+
{
|
|
772
|
+
type: 'text',
|
|
773
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
content: [
|
|
780
|
+
{
|
|
781
|
+
type: 'text',
|
|
782
|
+
text: `Error setting Focused Inbox override: ${error.message}`,
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Consolidated tool definitions (7 → 3)
|
|
790
|
+
const categoriesTools = [
|
|
791
|
+
{
|
|
792
|
+
name: 'manage-category',
|
|
793
|
+
description:
|
|
794
|
+
'Manage master categories. action=list (default) lists categories. action=create creates a category. action=update changes name/color. action=delete removes a category.',
|
|
795
|
+
annotations: {
|
|
796
|
+
title: 'Master Categories',
|
|
797
|
+
readOnlyHint: false,
|
|
798
|
+
destructiveHint: false,
|
|
799
|
+
openWorldHint: false,
|
|
800
|
+
},
|
|
801
|
+
inputSchema: {
|
|
802
|
+
type: 'object',
|
|
803
|
+
properties: {
|
|
804
|
+
action: {
|
|
805
|
+
type: 'string',
|
|
806
|
+
enum: ['list', 'create', 'update', 'delete'],
|
|
807
|
+
description: 'Action to perform (default: list)',
|
|
808
|
+
},
|
|
809
|
+
// list params
|
|
810
|
+
outputVerbosity: {
|
|
811
|
+
type: 'string',
|
|
812
|
+
enum: ['minimal', 'standard', 'full'],
|
|
813
|
+
description: 'Output detail level (action=list, default: standard)',
|
|
814
|
+
},
|
|
815
|
+
// create/update params
|
|
816
|
+
displayName: {
|
|
817
|
+
type: 'string',
|
|
818
|
+
description:
|
|
819
|
+
'Category name (action=create required, action=update optional)',
|
|
820
|
+
},
|
|
821
|
+
color: {
|
|
822
|
+
type: 'string',
|
|
823
|
+
enum: CATEGORY_COLORS,
|
|
824
|
+
description:
|
|
825
|
+
'Color preset, e.g. preset0=Red, preset7=Blue (action=create/update)',
|
|
826
|
+
},
|
|
827
|
+
// update/delete params
|
|
828
|
+
id: {
|
|
829
|
+
type: 'string',
|
|
830
|
+
description: 'Category ID (action=update/delete, required)',
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
required: [],
|
|
834
|
+
},
|
|
835
|
+
handler: async (args) => {
|
|
836
|
+
const action = args.action || 'list';
|
|
837
|
+
switch (action) {
|
|
838
|
+
case 'create':
|
|
839
|
+
return handleCreateCategory(args);
|
|
840
|
+
case 'update':
|
|
841
|
+
return handleUpdateCategory(args);
|
|
842
|
+
case 'delete':
|
|
843
|
+
return handleDeleteCategory(args);
|
|
844
|
+
case 'list':
|
|
845
|
+
default:
|
|
846
|
+
return handleListCategories(args);
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
name: 'apply-category',
|
|
852
|
+
description: 'Apply, add, or remove categories on email message(s)',
|
|
853
|
+
annotations: {
|
|
854
|
+
title: 'Apply Categories',
|
|
855
|
+
readOnlyHint: false,
|
|
856
|
+
destructiveHint: false,
|
|
857
|
+
openWorldHint: false,
|
|
858
|
+
},
|
|
859
|
+
inputSchema: {
|
|
860
|
+
type: 'object',
|
|
861
|
+
properties: {
|
|
862
|
+
messageId: {
|
|
863
|
+
type: 'string',
|
|
864
|
+
description: 'Single message ID to categorise',
|
|
865
|
+
},
|
|
866
|
+
messageIds: {
|
|
867
|
+
type: 'array',
|
|
868
|
+
items: { type: 'string' },
|
|
869
|
+
description: 'Array of message IDs to categorise (batch operation)',
|
|
870
|
+
},
|
|
871
|
+
categories: {
|
|
872
|
+
type: 'array',
|
|
873
|
+
items: { type: 'string' },
|
|
874
|
+
description: 'Category display names to apply/remove (required)',
|
|
875
|
+
},
|
|
876
|
+
action: {
|
|
877
|
+
type: 'string',
|
|
878
|
+
enum: ['set', 'add', 'remove'],
|
|
879
|
+
description:
|
|
880
|
+
'set (replace all), add (append), remove (remove specific). Default: set',
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
required: ['categories'],
|
|
884
|
+
},
|
|
885
|
+
handler: handleApplyCategory,
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
name: 'manage-focused-inbox',
|
|
889
|
+
description:
|
|
890
|
+
'Manage Focused Inbox overrides. action=list (default) shows overrides. action=set creates/updates an override. action=delete removes an override.',
|
|
891
|
+
annotations: {
|
|
892
|
+
title: 'Focused Inbox',
|
|
893
|
+
readOnlyHint: false,
|
|
894
|
+
destructiveHint: false,
|
|
895
|
+
openWorldHint: false,
|
|
896
|
+
},
|
|
897
|
+
inputSchema: {
|
|
898
|
+
type: 'object',
|
|
899
|
+
properties: {
|
|
900
|
+
action: {
|
|
901
|
+
type: 'string',
|
|
902
|
+
enum: ['list', 'set', 'delete'],
|
|
903
|
+
description: 'Action to perform (default: list)',
|
|
904
|
+
},
|
|
905
|
+
// list params
|
|
906
|
+
outputVerbosity: {
|
|
907
|
+
type: 'string',
|
|
908
|
+
enum: ['minimal', 'standard', 'full'],
|
|
909
|
+
description: 'Output detail level (action=list, default: standard)',
|
|
910
|
+
},
|
|
911
|
+
// set/delete params
|
|
912
|
+
emailAddress: {
|
|
913
|
+
type: 'string',
|
|
914
|
+
description: 'Sender email address (action=set/delete, required)',
|
|
915
|
+
},
|
|
916
|
+
name: {
|
|
917
|
+
type: 'string',
|
|
918
|
+
description: 'Sender display name (action=set)',
|
|
919
|
+
},
|
|
920
|
+
classifyAs: {
|
|
921
|
+
type: 'string',
|
|
922
|
+
enum: ['focused', 'other'],
|
|
923
|
+
description:
|
|
924
|
+
'Where to put emails from this sender (action=set, default: focused)',
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
required: [],
|
|
928
|
+
},
|
|
929
|
+
handler: async (args) => {
|
|
930
|
+
const action = args.action || 'list';
|
|
931
|
+
switch (action) {
|
|
932
|
+
case 'set':
|
|
933
|
+
return handleSetFocusedInboxOverride(args);
|
|
934
|
+
case 'delete':
|
|
935
|
+
return handleSetFocusedInboxOverride(args);
|
|
936
|
+
case 'list':
|
|
937
|
+
default:
|
|
938
|
+
return handleGetFocusedInboxOverrides(args);
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
];
|
|
943
|
+
|
|
944
|
+
module.exports = {
|
|
945
|
+
categoriesTools,
|
|
946
|
+
handleListCategories,
|
|
947
|
+
handleCreateCategory,
|
|
948
|
+
handleUpdateCategory,
|
|
949
|
+
handleDeleteCategory,
|
|
950
|
+
handleApplyCategory,
|
|
951
|
+
handleGetFocusedInboxOverrides,
|
|
952
|
+
handleSetFocusedInboxOverride,
|
|
953
|
+
CATEGORY_COLORS,
|
|
954
|
+
COLOR_NAMES,
|
|
955
|
+
};
|