@littlebearapps/outlook-assistant 3.13.0 → 3.14.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/.env.example +30 -3
- package/README.md +67 -27
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/oauth-server.js +7 -1
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +28 -30
- package/auth/tools.js +61 -82
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +461 -0
- package/calendar/update.js +55 -83
- package/categories/index.js +68 -265
- package/config.js +29 -1
- package/contacts/index.js +72 -128
- package/email/attachments.js +43 -125
- package/email/conversations.js +44 -78
- package/email/delta.js +69 -46
- package/email/draft.js +170 -103
- package/email/export.js +145 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +86 -110
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +39 -51
- package/email/read.js +16 -50
- package/email/search.js +47 -87
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +19 -17
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +18 -27
- package/index.js +39 -45
- package/llms-install.md +22 -4
- package/llms.txt +20 -11
- package/outlook-auth-server.js +10 -3
- package/package.json +4 -1
- package/request-handler.js +217 -116
- package/rules/create.js +28 -71
- package/rules/index.js +52 -93
- package/rules/list.js +7 -19
- package/rules/rule-builder.js +59 -22
- package/rules/update.js +27 -61
- package/server.js +41 -0
- package/settings/index.js +162 -145
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +247 -42
- package/utils/server-instructions.js +73 -0
- package/utils/tool-error.js +33 -0
package/contacts/index.js
CHANGED
|
@@ -9,6 +9,10 @@ const {
|
|
|
9
9
|
} = require('../utils/graph-api');
|
|
10
10
|
const { ensureAuthenticated } = require('../auth');
|
|
11
11
|
const { quoteSearchPhrase } = require('../utils/odata-helpers');
|
|
12
|
+
const { toolMetadata } = require('../utils/risk-classes');
|
|
13
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
14
|
+
const { dryRunResult, dryRunUnsupported } = require('../utils/safety');
|
|
15
|
+
const { log } = require('../utils/logger');
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Contact field presets for different use cases
|
|
@@ -195,20 +199,9 @@ async function handleListContacts(args) {
|
|
|
195
199
|
};
|
|
196
200
|
} catch (error) {
|
|
197
201
|
if (error.message === 'Authentication required') {
|
|
198
|
-
return
|
|
199
|
-
content: [
|
|
200
|
-
{
|
|
201
|
-
type: 'text',
|
|
202
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
203
|
-
},
|
|
204
|
-
],
|
|
205
|
-
};
|
|
202
|
+
return authRequiredError();
|
|
206
203
|
}
|
|
207
|
-
return {
|
|
208
|
-
content: [
|
|
209
|
-
{ type: 'text', text: `Error listing contacts: ${error.message}` },
|
|
210
|
-
],
|
|
211
|
-
};
|
|
204
|
+
return toolError(`Error listing contacts: ${error.message}`);
|
|
212
205
|
}
|
|
213
206
|
}
|
|
214
207
|
|
|
@@ -221,7 +214,7 @@ async function handleSearchContacts(args) {
|
|
|
221
214
|
const verbosity = args.outputVerbosity || 'standard';
|
|
222
215
|
|
|
223
216
|
if (!query) {
|
|
224
|
-
return
|
|
217
|
+
return toolError('Search query is required.');
|
|
225
218
|
}
|
|
226
219
|
|
|
227
220
|
try {
|
|
@@ -252,7 +245,7 @@ async function handleSearchContacts(args) {
|
|
|
252
245
|
);
|
|
253
246
|
} catch (filterError) {
|
|
254
247
|
// Fallback: fetch all contacts and filter client-side if $filter is unsupported
|
|
255
|
-
|
|
248
|
+
log.debug(
|
|
256
249
|
`Contact $filter failed (${filterError.message}), falling back to client-side filter`
|
|
257
250
|
);
|
|
258
251
|
const allParams = {
|
|
@@ -295,20 +288,9 @@ async function handleSearchContacts(args) {
|
|
|
295
288
|
};
|
|
296
289
|
} catch (error) {
|
|
297
290
|
if (error.message === 'Authentication required') {
|
|
298
|
-
return
|
|
299
|
-
content: [
|
|
300
|
-
{
|
|
301
|
-
type: 'text',
|
|
302
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
303
|
-
},
|
|
304
|
-
],
|
|
305
|
-
};
|
|
291
|
+
return authRequiredError();
|
|
306
292
|
}
|
|
307
|
-
return {
|
|
308
|
-
content: [
|
|
309
|
-
{ type: 'text', text: `Error searching contacts: ${error.message}` },
|
|
310
|
-
],
|
|
311
|
-
};
|
|
293
|
+
return toolError(`Error searching contacts: ${error.message}`);
|
|
312
294
|
}
|
|
313
295
|
}
|
|
314
296
|
|
|
@@ -319,7 +301,7 @@ async function handleGetContact(args) {
|
|
|
319
301
|
const contactId = args.id;
|
|
320
302
|
|
|
321
303
|
if (!contactId) {
|
|
322
|
-
return
|
|
304
|
+
return toolError('Contact ID is required.');
|
|
323
305
|
}
|
|
324
306
|
|
|
325
307
|
try {
|
|
@@ -346,20 +328,9 @@ async function handleGetContact(args) {
|
|
|
346
328
|
};
|
|
347
329
|
} catch (error) {
|
|
348
330
|
if (error.message === 'Authentication required') {
|
|
349
|
-
return
|
|
350
|
-
content: [
|
|
351
|
-
{
|
|
352
|
-
type: 'text',
|
|
353
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
354
|
-
},
|
|
355
|
-
],
|
|
356
|
-
};
|
|
331
|
+
return authRequiredError();
|
|
357
332
|
}
|
|
358
|
-
return {
|
|
359
|
-
content: [
|
|
360
|
-
{ type: 'text', text: `Error getting contact: ${error.message}` },
|
|
361
|
-
],
|
|
362
|
-
};
|
|
333
|
+
return toolError(`Error getting contact: ${error.message}`);
|
|
363
334
|
}
|
|
364
335
|
}
|
|
365
336
|
|
|
@@ -387,14 +358,9 @@ async function handleCreateContact(args) {
|
|
|
387
358
|
(Array.isArray(emails) && emails[0]);
|
|
388
359
|
|
|
389
360
|
if (!resolvedDisplayName && !email && !(emails && emails.length > 0)) {
|
|
390
|
-
return
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
type: 'text',
|
|
394
|
-
text: 'At least displayName, firstName/lastName, email, or emails is required.',
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
};
|
|
361
|
+
return toolError(
|
|
362
|
+
'At least displayName, firstName/lastName, email, or emails is required.'
|
|
363
|
+
);
|
|
398
364
|
}
|
|
399
365
|
|
|
400
366
|
try {
|
|
@@ -453,20 +419,9 @@ async function handleCreateContact(args) {
|
|
|
453
419
|
};
|
|
454
420
|
} catch (error) {
|
|
455
421
|
if (error.message === 'Authentication required') {
|
|
456
|
-
return
|
|
457
|
-
content: [
|
|
458
|
-
{
|
|
459
|
-
type: 'text',
|
|
460
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
461
|
-
},
|
|
462
|
-
],
|
|
463
|
-
};
|
|
422
|
+
return authRequiredError();
|
|
464
423
|
}
|
|
465
|
-
return {
|
|
466
|
-
content: [
|
|
467
|
-
{ type: 'text', text: `Error creating contact: ${error.message}` },
|
|
468
|
-
],
|
|
469
|
-
};
|
|
424
|
+
return toolError(`Error creating contact: ${error.message}`);
|
|
470
425
|
}
|
|
471
426
|
}
|
|
472
427
|
|
|
@@ -478,7 +433,7 @@ async function handleUpdateContact(args) {
|
|
|
478
433
|
args;
|
|
479
434
|
|
|
480
435
|
if (!id) {
|
|
481
|
-
return
|
|
436
|
+
return toolError('Contact ID is required.');
|
|
482
437
|
}
|
|
483
438
|
|
|
484
439
|
try {
|
|
@@ -521,23 +476,37 @@ async function handleUpdateContact(args) {
|
|
|
521
476
|
};
|
|
522
477
|
} catch (error) {
|
|
523
478
|
if (error.message === 'Authentication required') {
|
|
524
|
-
return
|
|
525
|
-
content: [
|
|
526
|
-
{
|
|
527
|
-
type: 'text',
|
|
528
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
529
|
-
},
|
|
530
|
-
],
|
|
531
|
-
};
|
|
479
|
+
return authRequiredError();
|
|
532
480
|
}
|
|
533
|
-
return {
|
|
534
|
-
content: [
|
|
535
|
-
{ type: 'text', text: `Error updating contact: ${error.message}` },
|
|
536
|
-
],
|
|
537
|
-
};
|
|
481
|
+
return toolError(`Error updating contact: ${error.message}`);
|
|
538
482
|
}
|
|
539
483
|
}
|
|
540
484
|
|
|
485
|
+
/**
|
|
486
|
+
* dryRun preview for delete (#274): which contact would be lost. Reads only.
|
|
487
|
+
*/
|
|
488
|
+
async function previewDeleteContact(accessToken, contactId) {
|
|
489
|
+
const contact = await callGraphAPI(
|
|
490
|
+
accessToken,
|
|
491
|
+
'GET',
|
|
492
|
+
`me/contacts/${contactId}`,
|
|
493
|
+
null,
|
|
494
|
+
{ $select: 'displayName,emailAddresses,companyName' }
|
|
495
|
+
);
|
|
496
|
+
const details = [
|
|
497
|
+
...(contact.emailAddresses || []).map((e) => e.address).filter(Boolean),
|
|
498
|
+
contact.companyName,
|
|
499
|
+
].filter(Boolean);
|
|
500
|
+
const name = contact.displayName || '(no name)';
|
|
501
|
+
return dryRunResult(
|
|
502
|
+
[
|
|
503
|
+
`Deletes contact '${name}'${details.length > 0 ? ` (${details.join('; ')})` : ''}.`,
|
|
504
|
+
"It doesn't go to Deleted Items, and Graph doesn't document a way to restore it (Outlook's \"Recover deleted items\" may work for a limited time, but don't rely on it), so treat it as permanent.",
|
|
505
|
+
],
|
|
506
|
+
{ action: 'delete', contactId, displayName: contact.displayName }
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
541
510
|
/**
|
|
542
511
|
* Delete contact handler
|
|
543
512
|
*/
|
|
@@ -545,12 +514,17 @@ async function handleDeleteContact(args) {
|
|
|
545
514
|
const contactId = args.id;
|
|
546
515
|
|
|
547
516
|
if (!contactId) {
|
|
548
|
-
return
|
|
517
|
+
return toolError('Contact ID is required.');
|
|
549
518
|
}
|
|
550
519
|
|
|
551
520
|
try {
|
|
552
521
|
const accessToken = await ensureAuthenticated();
|
|
553
522
|
|
|
523
|
+
// dryRun: say which contact would be lost; delete nothing.
|
|
524
|
+
if (args.dryRun) {
|
|
525
|
+
return await previewDeleteContact(accessToken, contactId);
|
|
526
|
+
}
|
|
527
|
+
|
|
554
528
|
const endpoint = `me/contacts/${contactId}`;
|
|
555
529
|
await callGraphAPI(accessToken, 'DELETE', endpoint);
|
|
556
530
|
|
|
@@ -565,20 +539,9 @@ async function handleDeleteContact(args) {
|
|
|
565
539
|
};
|
|
566
540
|
} catch (error) {
|
|
567
541
|
if (error.message === 'Authentication required') {
|
|
568
|
-
return
|
|
569
|
-
content: [
|
|
570
|
-
{
|
|
571
|
-
type: 'text',
|
|
572
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
573
|
-
},
|
|
574
|
-
],
|
|
575
|
-
};
|
|
542
|
+
return authRequiredError();
|
|
576
543
|
}
|
|
577
|
-
return {
|
|
578
|
-
content: [
|
|
579
|
-
{ type: 'text', text: `Error deleting contact: ${error.message}` },
|
|
580
|
-
],
|
|
581
|
-
};
|
|
544
|
+
return toolError(`Error deleting contact: ${error.message}`);
|
|
582
545
|
}
|
|
583
546
|
}
|
|
584
547
|
|
|
@@ -590,7 +553,7 @@ async function handleSearchPeople(args) {
|
|
|
590
553
|
const count = Math.min(args.count || 25, 50);
|
|
591
554
|
|
|
592
555
|
if (!query) {
|
|
593
|
-
return
|
|
556
|
+
return toolError('Search query is required.');
|
|
594
557
|
}
|
|
595
558
|
|
|
596
559
|
try {
|
|
@@ -658,20 +621,9 @@ async function handleSearchPeople(args) {
|
|
|
658
621
|
};
|
|
659
622
|
} catch (error) {
|
|
660
623
|
if (error.message === 'Authentication required') {
|
|
661
|
-
return
|
|
662
|
-
content: [
|
|
663
|
-
{
|
|
664
|
-
type: 'text',
|
|
665
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
666
|
-
},
|
|
667
|
-
],
|
|
668
|
-
};
|
|
624
|
+
return authRequiredError();
|
|
669
625
|
}
|
|
670
|
-
return {
|
|
671
|
-
content: [
|
|
672
|
-
{ type: 'text', text: `Error searching people: ${error.message}` },
|
|
673
|
-
],
|
|
674
|
-
};
|
|
626
|
+
return toolError(`Error searching people: ${error.message}`);
|
|
675
627
|
}
|
|
676
628
|
}
|
|
677
629
|
|
|
@@ -680,13 +632,8 @@ const contactsTools = [
|
|
|
680
632
|
{
|
|
681
633
|
name: 'manage-contact',
|
|
682
634
|
description:
|
|
683
|
-
"Full CRUD over the signed-in user's personal Outlook contacts (destructive: covers `delete` action). action=`list` (default) returns contacts with pagination via `skip`/`count` (default 50). action=`search` returns contacts matching `query` against name/email (default 25). action=`get` returns full contact detail by `id`. action=`create` adds a new contact and returns its `id`. action=`update` patches the given fields by `id` (only fields passed are changed). action=`delete`
|
|
684
|
-
|
|
685
|
-
title: 'Contacts',
|
|
686
|
-
readOnlyHint: false,
|
|
687
|
-
destructiveHint: true,
|
|
688
|
-
openWorldHint: false,
|
|
689
|
-
},
|
|
635
|
+
"Full CRUD over the signed-in user's personal Outlook contacts (destructive: covers `delete` action). action=`list` (default) returns contacts with pagination via `skip`/`count` (default 50). action=`search` returns contacts matching `query` against name/email (default 25). action=`get` returns full contact detail by `id`. action=`create` adds a new contact and returns its `id`. action=`update` patches the given fields by `id` (only fields passed are changed). action=`delete` removes the contact by `id`; it skips Deleted Items, so treat it as permanent and pass `dryRun: true` first to confirm which contact it is. Use `outputVerbosity` (minimal/standard/full) on list/search to control field count. Searches only your personal contact store; for relevance-ranked search across contacts, the directory and recent communications, use `search-people`.",
|
|
636
|
+
...toolMetadata('manage-contact', 'Contacts'),
|
|
690
637
|
inputSchema: {
|
|
691
638
|
type: 'object',
|
|
692
639
|
properties: {
|
|
@@ -768,12 +715,20 @@ const contactsTools = [
|
|
|
768
715
|
type: 'string',
|
|
769
716
|
description: 'Personal notes (action=create/update)',
|
|
770
717
|
},
|
|
718
|
+
dryRun: {
|
|
719
|
+
type: 'boolean',
|
|
720
|
+
description:
|
|
721
|
+
'Preview only (action=delete): nothing is deleted. Shows which contact would be removed. Other actions refuse dryRun and change nothing. Default false.',
|
|
722
|
+
},
|
|
771
723
|
},
|
|
772
724
|
additionalProperties: false,
|
|
773
725
|
required: [],
|
|
774
726
|
},
|
|
775
727
|
handler: async (args) => {
|
|
776
728
|
const action = args.action || 'list';
|
|
729
|
+
if (args.dryRun && action !== 'delete') {
|
|
730
|
+
return dryRunUnsupported('manage-contact', action, 'delete');
|
|
731
|
+
}
|
|
777
732
|
switch (action) {
|
|
778
733
|
case 'search':
|
|
779
734
|
return handleSearchContacts(args);
|
|
@@ -788,28 +743,17 @@ const contactsTools = [
|
|
|
788
743
|
case 'list':
|
|
789
744
|
return handleListContacts(args);
|
|
790
745
|
default:
|
|
791
|
-
return
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
type: 'text',
|
|
795
|
-
text: `Unknown action '${action}'. Valid actions: list, search, get, create, update, delete.`,
|
|
796
|
-
},
|
|
797
|
-
],
|
|
798
|
-
};
|
|
746
|
+
return toolError(
|
|
747
|
+
`Unknown action '${action}'. Valid actions: list, search, get, create, update, delete.`
|
|
748
|
+
);
|
|
799
749
|
}
|
|
800
750
|
},
|
|
801
751
|
},
|
|
802
752
|
{
|
|
803
753
|
name: 'search-people',
|
|
804
754
|
description:
|
|
805
|
-
'Relevance-ranked search across personal contacts, organisation directory, and recent communications via the Microsoft Graph People API (read-only). Returns people objects with `displayName`, `emailAddresses`, `companyName`, `jobTitle`, and relevance metadata
|
|
806
|
-
|
|
807
|
-
title: 'People Search',
|
|
808
|
-
readOnlyHint: true,
|
|
809
|
-
// openWorldHint: returns directory/people data for external contacts
|
|
810
|
-
// (org directory + inferred from recent comms). (#92)
|
|
811
|
-
openWorldHint: true,
|
|
812
|
-
},
|
|
755
|
+
'Relevance-ranked search across personal contacts, organisation directory, and recent communications via the Microsoft Graph People API (read-only). Returns people objects with `displayName`, `emailAddresses`, `companyName`, `jobTitle`, and relevance metadata, for "who is X?" or "who do I email about Y?" lookups. For entries from your personal contact store only, use `manage-contact` action=`search`.',
|
|
756
|
+
...toolMetadata('search-people', 'People Search'),
|
|
813
757
|
inputSchema: {
|
|
814
758
|
type: 'object',
|
|
815
759
|
properties: {
|
package/email/attachments.js
CHANGED
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
* Provides tools to list and download email attachments via Microsoft Graph API
|
|
4
4
|
*/
|
|
5
5
|
const _https = require('https'); // Reserved for future use
|
|
6
|
-
const fs = require('fs');
|
|
7
6
|
const os = require('os');
|
|
8
7
|
const path = require('path');
|
|
9
8
|
const _config = require('../config'); // Reserved for future use
|
|
10
9
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
11
10
|
const { ensureAuthenticated } = require('../auth');
|
|
12
11
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
13
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
writeClaimedFile,
|
|
14
|
+
ensureOutputDir,
|
|
15
|
+
confineOutputPath,
|
|
16
|
+
OutputPathError,
|
|
17
|
+
} = require('../utils/safe-write');
|
|
18
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
19
|
+
const { log } = require('../utils/logger');
|
|
14
20
|
|
|
15
21
|
const MAX_FILENAME_LENGTH = 200;
|
|
16
22
|
|
|
@@ -52,14 +58,7 @@ async function handleListAttachments(args) {
|
|
|
52
58
|
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
53
59
|
|
|
54
60
|
if (!messageId) {
|
|
55
|
-
return
|
|
56
|
-
content: [
|
|
57
|
-
{
|
|
58
|
-
type: 'text',
|
|
59
|
-
text: 'Error: messageId is required',
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
};
|
|
61
|
+
return toolError('Error: messageId is required');
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
try {
|
|
@@ -71,7 +70,7 @@ async function handleListAttachments(args) {
|
|
|
71
70
|
$select: 'id,name,contentType,size,isInline',
|
|
72
71
|
};
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
log.debug(`Fetching attachments for message: ${messageId}`);
|
|
75
74
|
const response = await callGraphAPI(
|
|
76
75
|
accessToken,
|
|
77
76
|
'GET',
|
|
@@ -113,24 +112,10 @@ async function handleListAttachments(args) {
|
|
|
113
112
|
error.message === 'Authentication required' ||
|
|
114
113
|
error.message === 'UNAUTHORIZED'
|
|
115
114
|
) {
|
|
116
|
-
return
|
|
117
|
-
content: [
|
|
118
|
-
{
|
|
119
|
-
type: 'text',
|
|
120
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
121
|
-
},
|
|
122
|
-
],
|
|
123
|
-
};
|
|
115
|
+
return authRequiredError();
|
|
124
116
|
}
|
|
125
117
|
|
|
126
|
-
return {
|
|
127
|
-
content: [
|
|
128
|
-
{
|
|
129
|
-
type: 'text',
|
|
130
|
-
text: `Error listing attachments: ${error.message}`,
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
};
|
|
118
|
+
return toolError(`Error listing attachments: ${error.message}`);
|
|
134
119
|
}
|
|
135
120
|
}
|
|
136
121
|
|
|
@@ -139,7 +124,7 @@ async function handleListAttachments(args) {
|
|
|
139
124
|
* @param {object} args - Tool arguments
|
|
140
125
|
* @param {string} args.messageId - The ID of the email message
|
|
141
126
|
* @param {string} args.attachmentId - The ID of the attachment
|
|
142
|
-
* @param {string} args.savePath -
|
|
127
|
+
* @param {string} args.savePath - Deprecated alias for outputDir (absolute or ~/…; default: system temp directory)
|
|
143
128
|
* @returns {object} - MCP response with download result
|
|
144
129
|
*/
|
|
145
130
|
async function handleDownloadAttachment(args) {
|
|
@@ -152,14 +137,19 @@ async function handleDownloadAttachment(args) {
|
|
|
152
137
|
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
153
138
|
|
|
154
139
|
if (!messageId || !attachmentId) {
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
140
|
+
return toolError('Error: Both messageId and attachmentId are required');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Determine save location before fetching anything. F-19: default to
|
|
144
|
+
// os.tmpdir() instead of cwd so attachments don't silently land in the
|
|
145
|
+
// source tree when the caller forgets to pass outputDir. The directory
|
|
146
|
+
// must be inside an allowed base; write only to the resolved path.
|
|
147
|
+
let outputDir;
|
|
148
|
+
try {
|
|
149
|
+
outputDir = confineOutputPath(savePath || os.tmpdir());
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (!(error instanceof OutputPathError)) throw error;
|
|
152
|
+
return toolError(error.message, { nextStep: error.nextStep });
|
|
163
153
|
}
|
|
164
154
|
|
|
165
155
|
try {
|
|
@@ -167,7 +157,7 @@ async function handleDownloadAttachment(args) {
|
|
|
167
157
|
|
|
168
158
|
// First, get attachment metadata to get the filename and content
|
|
169
159
|
const metadataEndpoint = `${prefix}/messages/${messageId}/attachments/${attachmentId}`;
|
|
170
|
-
|
|
160
|
+
log.debug(`Fetching attachment metadata: ${attachmentId}`);
|
|
171
161
|
|
|
172
162
|
const metadata = await callGraphAPI(
|
|
173
163
|
accessToken,
|
|
@@ -178,14 +168,7 @@ async function handleDownloadAttachment(args) {
|
|
|
178
168
|
);
|
|
179
169
|
|
|
180
170
|
if (!metadata) {
|
|
181
|
-
return
|
|
182
|
-
content: [
|
|
183
|
-
{
|
|
184
|
-
type: 'text',
|
|
185
|
-
text: 'Error: Attachment not found',
|
|
186
|
-
},
|
|
187
|
-
],
|
|
188
|
-
};
|
|
171
|
+
return toolError('Error: Attachment not found');
|
|
189
172
|
}
|
|
190
173
|
|
|
191
174
|
const filename = metadata.name || 'attachment';
|
|
@@ -196,24 +179,13 @@ async function handleDownloadAttachment(args) {
|
|
|
196
179
|
const contentBytes = metadata.contentBytes;
|
|
197
180
|
|
|
198
181
|
if (!contentBytes) {
|
|
199
|
-
return
|
|
200
|
-
content: [
|
|
201
|
-
{
|
|
202
|
-
type: 'text',
|
|
203
|
-
text: 'Error: No content found in attachment',
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
};
|
|
182
|
+
return toolError('Error: No content found in attachment');
|
|
207
183
|
}
|
|
208
184
|
|
|
209
|
-
//
|
|
210
|
-
// of cwd so attachments don't silently land in the source tree
|
|
211
|
-
// when the caller forgets to pass outputDir. Auto-create the
|
|
212
|
-
// target directory.
|
|
185
|
+
// Auto-create the target directory.
|
|
213
186
|
// The filename is sender-controlled (GHSA-755c-c45g-69rv): reduce it
|
|
214
187
|
// to a safe basename and never overwrite or follow a symlink.
|
|
215
|
-
|
|
216
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
188
|
+
ensureOutputDir(outputDir);
|
|
217
189
|
|
|
218
190
|
// Decode base64 and save to file
|
|
219
191
|
const buffer = Buffer.from(contentBytes, 'base64');
|
|
@@ -239,14 +211,9 @@ async function handleDownloadAttachment(args) {
|
|
|
239
211
|
};
|
|
240
212
|
} else if (metadata['@odata.type'] === '#microsoft.graph.itemAttachment') {
|
|
241
213
|
// Item attachments (embedded emails, calendar items) need different handling
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
type: 'text',
|
|
246
|
-
text: `This is an embedded item attachment (${metadata.name}). Item attachments cannot be downloaded as files directly. They contain embedded Outlook items like emails or calendar events.`,
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
|
-
};
|
|
214
|
+
return toolError(
|
|
215
|
+
`This is an embedded item attachment (${metadata.name}). Item attachments cannot be downloaded as files directly. They contain embedded Outlook items like emails or calendar events.`
|
|
216
|
+
);
|
|
250
217
|
} else if (
|
|
251
218
|
metadata['@odata.type'] === '#microsoft.graph.referenceAttachment'
|
|
252
219
|
) {
|
|
@@ -260,38 +227,17 @@ async function handleDownloadAttachment(args) {
|
|
|
260
227
|
],
|
|
261
228
|
};
|
|
262
229
|
} else {
|
|
263
|
-
return {
|
|
264
|
-
content: [
|
|
265
|
-
{
|
|
266
|
-
type: 'text',
|
|
267
|
-
text: `Unknown attachment type: ${metadata['@odata.type']}`,
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
};
|
|
230
|
+
return toolError(`Unknown attachment type: ${metadata['@odata.type']}`);
|
|
271
231
|
}
|
|
272
232
|
} catch (error) {
|
|
273
233
|
if (
|
|
274
234
|
error.message === 'Authentication required' ||
|
|
275
235
|
error.message === 'UNAUTHORIZED'
|
|
276
236
|
) {
|
|
277
|
-
return
|
|
278
|
-
content: [
|
|
279
|
-
{
|
|
280
|
-
type: 'text',
|
|
281
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
|
-
};
|
|
237
|
+
return authRequiredError();
|
|
285
238
|
}
|
|
286
239
|
|
|
287
|
-
return {
|
|
288
|
-
content: [
|
|
289
|
-
{
|
|
290
|
-
type: 'text',
|
|
291
|
-
text: `Error downloading attachment: ${error.message}`,
|
|
292
|
-
},
|
|
293
|
-
],
|
|
294
|
-
};
|
|
240
|
+
return toolError(`Error downloading attachment: ${error.message}`);
|
|
295
241
|
}
|
|
296
242
|
}
|
|
297
243
|
|
|
@@ -307,33 +253,19 @@ async function handleGetAttachmentContent(args) {
|
|
|
307
253
|
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
308
254
|
|
|
309
255
|
if (!messageId || !attachmentId) {
|
|
310
|
-
return
|
|
311
|
-
content: [
|
|
312
|
-
{
|
|
313
|
-
type: 'text',
|
|
314
|
-
text: 'Error: Both messageId and attachmentId are required',
|
|
315
|
-
},
|
|
316
|
-
],
|
|
317
|
-
};
|
|
256
|
+
return toolError('Error: Both messageId and attachmentId are required');
|
|
318
257
|
}
|
|
319
258
|
|
|
320
259
|
try {
|
|
321
260
|
const accessToken = await ensureAuthenticated();
|
|
322
261
|
|
|
323
262
|
const endpoint = `${prefix}/messages/${messageId}/attachments/${attachmentId}`;
|
|
324
|
-
|
|
263
|
+
log.debug(`Fetching attachment content: ${attachmentId}`);
|
|
325
264
|
|
|
326
265
|
const response = await callGraphAPI(accessToken, 'GET', endpoint, null, {});
|
|
327
266
|
|
|
328
267
|
if (!response) {
|
|
329
|
-
return
|
|
330
|
-
content: [
|
|
331
|
-
{
|
|
332
|
-
type: 'text',
|
|
333
|
-
text: 'Error: Attachment not found',
|
|
334
|
-
},
|
|
335
|
-
],
|
|
336
|
-
};
|
|
268
|
+
return toolError('Error: Attachment not found');
|
|
337
269
|
}
|
|
338
270
|
|
|
339
271
|
const filename = response.name || 'attachment';
|
|
@@ -371,7 +303,7 @@ async function handleGetAttachmentContent(args) {
|
|
|
371
303
|
content: [
|
|
372
304
|
{
|
|
373
305
|
type: 'text',
|
|
374
|
-
text: `Attachment: ${filename}\nType: ${contentType}\nSize: ${sizeKB} KB\n\nThis is a binary file. Use
|
|
306
|
+
text: `Attachment: ${filename}\nType: ${contentType}\nSize: ${sizeKB} KB\n\nThis is a binary file. Use \`attachments\` action=\`download\` to save it to disk.`,
|
|
375
307
|
},
|
|
376
308
|
],
|
|
377
309
|
};
|
|
@@ -390,24 +322,10 @@ async function handleGetAttachmentContent(args) {
|
|
|
390
322
|
error.message === 'Authentication required' ||
|
|
391
323
|
error.message === 'UNAUTHORIZED'
|
|
392
324
|
) {
|
|
393
|
-
return
|
|
394
|
-
content: [
|
|
395
|
-
{
|
|
396
|
-
type: 'text',
|
|
397
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
398
|
-
},
|
|
399
|
-
],
|
|
400
|
-
};
|
|
325
|
+
return authRequiredError();
|
|
401
326
|
}
|
|
402
327
|
|
|
403
|
-
return {
|
|
404
|
-
content: [
|
|
405
|
-
{
|
|
406
|
-
type: 'text',
|
|
407
|
-
text: `Error getting attachment content: ${error.message}`,
|
|
408
|
-
},
|
|
409
|
-
],
|
|
410
|
-
};
|
|
328
|
+
return toolError(`Error getting attachment content: ${error.message}`);
|
|
411
329
|
}
|
|
412
330
|
}
|
|
413
331
|
|