@littlebearapps/outlook-assistant 3.12.1 → 3.14.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 +27 -3
- package/README.md +108 -33
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +12 -2
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +46 -33
- package/auth/tools.js +223 -93
- 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 +335 -0
- package/calendar/update.js +42 -86
- package/categories/index.js +59 -264
- package/config.js +36 -2
- package/contacts/index.js +72 -128
- package/email/attachments.js +42 -124
- package/email/conversations.js +44 -78
- package/email/delta.js +10 -34
- package/email/draft.js +140 -96
- package/email/export.js +141 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +85 -109
- 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 +14 -49
- package/email/read.js +16 -50
- package/email/search.js +46 -86
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +17 -16
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +6 -20
- package/index.js +23 -45
- package/llms-install.md +31 -7
- package/llms.txt +19 -10
- package/outlook-auth-server.js +10 -3
- package/package.json +6 -2
- package/request-handler.js +217 -116
- package/rules/create.js +27 -70
- package/rules/index.js +30 -92
- package/rules/list.js +5 -17
- package/rules/rule-builder.js +57 -20
- package/rules/update.js +26 -60
- package/server.js +37 -0
- package/settings/index.js +142 -143
- 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 +109 -25
- package/utils/server-instructions.js +62 -0
- package/utils/tool-error.js +33 -0
package/email/search.js
CHANGED
|
@@ -19,6 +19,8 @@ const {
|
|
|
19
19
|
escapeSearchPhrase,
|
|
20
20
|
quoteSearchPhrase,
|
|
21
21
|
} = require('../utils/odata-helpers');
|
|
22
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
23
|
+
const { log } = require('../utils/logger');
|
|
22
24
|
|
|
23
25
|
// Upper bound on how many recent messages the client-side fallback scans
|
|
24
26
|
// before giving up. Deliberately DECOUPLED from the requested result count so
|
|
@@ -50,9 +52,7 @@ async function handleSearchEmails(args) {
|
|
|
50
52
|
|
|
51
53
|
// Validate count
|
|
52
54
|
if (args.count !== undefined && args.count < 1) {
|
|
53
|
-
return
|
|
54
|
-
content: [{ type: 'text', text: 'count must be at least 1.' }],
|
|
55
|
-
};
|
|
55
|
+
return toolError('count must be at least 1.');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// F-17: accept `maxResults` as an alias for `count` in non-delta mode.
|
|
@@ -111,12 +111,12 @@ async function handleSearchEmails(args) {
|
|
|
111
111
|
let endpoint;
|
|
112
112
|
if (searchAllFolders) {
|
|
113
113
|
endpoint = `${buildMailboxPrefix(sharedMailbox)}/messages`;
|
|
114
|
-
|
|
114
|
+
log.debug(
|
|
115
115
|
`Searching across all mail folders${sharedMailbox ? ` in ${sharedMailbox}` : ''}`
|
|
116
116
|
);
|
|
117
117
|
} else {
|
|
118
118
|
endpoint = await resolveFolderPath(accessToken, folder, sharedMailbox);
|
|
119
|
-
|
|
119
|
+
log.debug(`Using endpoint: ${endpoint} for folder: ${folder}`);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// Execute progressive search with pagination
|
|
@@ -140,25 +140,11 @@ async function handleSearchEmails(args) {
|
|
|
140
140
|
} catch (error) {
|
|
141
141
|
// Handle authentication errors
|
|
142
142
|
if (error.message === 'Authentication required') {
|
|
143
|
-
return
|
|
144
|
-
content: [
|
|
145
|
-
{
|
|
146
|
-
type: 'text',
|
|
147
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
};
|
|
143
|
+
return authRequiredError();
|
|
151
144
|
}
|
|
152
145
|
|
|
153
146
|
// General error response
|
|
154
|
-
return {
|
|
155
|
-
content: [
|
|
156
|
-
{
|
|
157
|
-
type: 'text',
|
|
158
|
-
text: `Error searching emails: ${error.message}`,
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
};
|
|
147
|
+
return toolError(`Error searching emails: ${error.message}`);
|
|
162
148
|
}
|
|
163
149
|
}
|
|
164
150
|
|
|
@@ -256,7 +242,7 @@ async function retryFieldScopedExpression(ctx) {
|
|
|
256
242
|
const translated = parseFieldScopedExpression(trimmedKql);
|
|
257
243
|
if (!translated) return null;
|
|
258
244
|
|
|
259
|
-
|
|
245
|
+
log.debug(
|
|
260
246
|
`Retrying field-scoped searchExpression as OData filters: ${JSON.stringify(translated)}`
|
|
261
247
|
);
|
|
262
248
|
const retry = await progressiveSearch(
|
|
@@ -357,7 +343,7 @@ async function progressiveSearch(
|
|
|
357
343
|
};
|
|
358
344
|
|
|
359
345
|
try {
|
|
360
|
-
|
|
346
|
+
log.debug(`Attempting raw KQL search: ${kqlForSearch}`);
|
|
361
347
|
searchAttempts.push('raw-kql');
|
|
362
348
|
|
|
363
349
|
const kqlParams = {
|
|
@@ -373,7 +359,7 @@ async function progressiveSearch(
|
|
|
373
359
|
kqlParams,
|
|
374
360
|
maxCount
|
|
375
361
|
);
|
|
376
|
-
|
|
362
|
+
log.debug(
|
|
377
363
|
`Raw KQL search complete: ${response.value?.length || 0} results`
|
|
378
364
|
);
|
|
379
365
|
const matched = response.value?.length || 0;
|
|
@@ -400,7 +386,7 @@ async function progressiveSearch(
|
|
|
400
386
|
const retry = await retryFieldScopedExpression(translationContext);
|
|
401
387
|
if (retry) return retry;
|
|
402
388
|
} catch (retryError) {
|
|
403
|
-
|
|
389
|
+
log.debug(`Translated retry failed: ${retryError.message}`);
|
|
404
390
|
}
|
|
405
391
|
}
|
|
406
392
|
|
|
@@ -408,7 +394,7 @@ async function progressiveSearch(
|
|
|
408
394
|
// would ignore kqlQuery and return unrelated emails.
|
|
409
395
|
return response;
|
|
410
396
|
} catch (error) {
|
|
411
|
-
|
|
397
|
+
log.debug(`Raw KQL search failed: ${error.message}`);
|
|
412
398
|
searchAttempts.push('raw-kql-error');
|
|
413
399
|
|
|
414
400
|
// A field-scoped expression is what Graph rejects here on personal
|
|
@@ -422,7 +408,7 @@ async function progressiveSearch(
|
|
|
422
408
|
return retry;
|
|
423
409
|
}
|
|
424
410
|
} catch (retryError) {
|
|
425
|
-
|
|
411
|
+
log.debug(`Translated retry also failed: ${retryError.message}`);
|
|
426
412
|
}
|
|
427
413
|
|
|
428
414
|
// Surface the failure rather than masking it with unrelated results.
|
|
@@ -454,7 +440,7 @@ async function progressiveSearch(
|
|
|
454
440
|
(filterTerms.hasAttachments === true || filterTerms.unreadOnly === true)
|
|
455
441
|
) {
|
|
456
442
|
// Skip directly to boolean-only filter (step 3) — combined search is redundant
|
|
457
|
-
|
|
443
|
+
log.debug('Only boolean filters provided, skipping combined search');
|
|
458
444
|
} else {
|
|
459
445
|
try {
|
|
460
446
|
const params = buildSearchParams(
|
|
@@ -463,7 +449,7 @@ async function progressiveSearch(
|
|
|
463
449
|
Math.min(50, maxCount),
|
|
464
450
|
selectFields
|
|
465
451
|
);
|
|
466
|
-
|
|
452
|
+
log.debug('Attempting combined search with params:', params);
|
|
467
453
|
searchAttempts.push('combined-search');
|
|
468
454
|
|
|
469
455
|
const response = await callGraphAPIPaginated(
|
|
@@ -474,7 +460,7 @@ async function progressiveSearch(
|
|
|
474
460
|
maxCount
|
|
475
461
|
);
|
|
476
462
|
if (response.value && response.value.length > 0) {
|
|
477
|
-
|
|
463
|
+
log.debug(
|
|
478
464
|
`Combined search successful: found ${response.value.length} results`
|
|
479
465
|
);
|
|
480
466
|
response._searchInfo = {
|
|
@@ -488,7 +474,7 @@ async function progressiveSearch(
|
|
|
488
474
|
return response;
|
|
489
475
|
}
|
|
490
476
|
} catch (error) {
|
|
491
|
-
|
|
477
|
+
log.debug(`Combined search failed: ${error.message}`);
|
|
492
478
|
}
|
|
493
479
|
}
|
|
494
480
|
|
|
@@ -503,9 +489,7 @@ async function progressiveSearch(
|
|
|
503
489
|
// 2a. Server-side single-term attempt (isolated try/catch — a failure
|
|
504
490
|
// here just falls through to the one client-side fallback below).
|
|
505
491
|
try {
|
|
506
|
-
|
|
507
|
-
`Attempting search with only ${term}: "${searchTerms[term]}"`
|
|
508
|
-
);
|
|
492
|
+
log.debug(`Attempting search with only ${term}: "${searchTerms[term]}"`);
|
|
509
493
|
searchAttempts.push(`single-term-${term}`);
|
|
510
494
|
|
|
511
495
|
const simplifiedParams = {
|
|
@@ -565,7 +549,7 @@ async function progressiveSearch(
|
|
|
565
549
|
secondaryApplied.length > 0
|
|
566
550
|
? ` after local ${secondaryApplied.join(', ')} narrowing of ${response.value.length}`
|
|
567
551
|
: '';
|
|
568
|
-
|
|
552
|
+
log.debug(
|
|
569
553
|
`Search with ${term} successful: found ${matched.length} results${narrowing}`
|
|
570
554
|
);
|
|
571
555
|
narrowResponse(response, matched);
|
|
@@ -582,12 +566,12 @@ async function progressiveSearch(
|
|
|
582
566
|
return response;
|
|
583
567
|
}
|
|
584
568
|
recordNarrowingMiss(scanState, response.value.length);
|
|
585
|
-
|
|
569
|
+
log.debug(
|
|
586
570
|
`Search with ${term} found ${response.value.length} results, but none also satisfied ${secondaryApplied.join(', ')} — continuing`
|
|
587
571
|
);
|
|
588
572
|
}
|
|
589
573
|
} catch (error) {
|
|
590
|
-
|
|
574
|
+
log.debug(`Search with ${term} failed: ${error.message}`);
|
|
591
575
|
// Fall through to the client-side fallback below.
|
|
592
576
|
}
|
|
593
577
|
|
|
@@ -597,7 +581,7 @@ async function progressiveSearch(
|
|
|
597
581
|
// try/catch prevents the double-scan/double-label a throw inside a
|
|
598
582
|
// success-path fallback would otherwise cause. (#169)
|
|
599
583
|
if (term === 'to' || term === 'query') {
|
|
600
|
-
|
|
584
|
+
log.debug(
|
|
601
585
|
`${term} unsatisfied server-side, trying client-side filtering`
|
|
602
586
|
);
|
|
603
587
|
searchAttempts.push(`client-side-${term}`);
|
|
@@ -614,7 +598,7 @@ async function progressiveSearch(
|
|
|
614
598
|
);
|
|
615
599
|
if (fallback) return fallback;
|
|
616
600
|
} catch (fallbackError) {
|
|
617
|
-
|
|
601
|
+
log.debug(
|
|
618
602
|
`Client-side ${term} fallback also failed: ${fallbackError.message}`
|
|
619
603
|
);
|
|
620
604
|
}
|
|
@@ -628,7 +612,7 @@ async function progressiveSearch(
|
|
|
628
612
|
filterTerms.receivedAfter || filterTerms.receivedBefore;
|
|
629
613
|
if (hasBooleanFilters || hasDateFilters) {
|
|
630
614
|
try {
|
|
631
|
-
|
|
615
|
+
log.debug('Attempting search with only boolean/date filters');
|
|
632
616
|
searchAttempts.push('boolean-filters-only');
|
|
633
617
|
|
|
634
618
|
const filterOnlyParams = {
|
|
@@ -651,7 +635,7 @@ async function progressiveSearch(
|
|
|
651
635
|
filterOnlyParams,
|
|
652
636
|
maxCount
|
|
653
637
|
);
|
|
654
|
-
|
|
638
|
+
log.debug(
|
|
655
639
|
`Boolean filter search found ${response.value?.length || 0} results`
|
|
656
640
|
);
|
|
657
641
|
// This step applied only the boolean/date filters. Narrow by the
|
|
@@ -677,15 +661,15 @@ async function progressiveSearch(
|
|
|
677
661
|
return response;
|
|
678
662
|
}
|
|
679
663
|
recordNarrowingMiss(scanState, response.value.length);
|
|
680
|
-
|
|
664
|
+
log.debug(
|
|
681
665
|
`Boolean filter search found ${response.value.length} results, but none satisfied ${secondaryApplied.join(', ')} — continuing`
|
|
682
666
|
);
|
|
683
667
|
} catch (error) {
|
|
684
|
-
|
|
668
|
+
log.debug(`Boolean filter search failed: ${error.message}`);
|
|
685
669
|
// Retry without $orderby if it was the issue
|
|
686
670
|
if (error.message && error.message.includes('InefficientFilter')) {
|
|
687
671
|
try {
|
|
688
|
-
|
|
672
|
+
log.debug('Retrying boolean filters without $orderby');
|
|
689
673
|
const retryParams = {
|
|
690
674
|
$top: Math.min(50, maxCount),
|
|
691
675
|
$select: selectFields,
|
|
@@ -722,13 +706,11 @@ async function progressiveSearch(
|
|
|
722
706
|
return response;
|
|
723
707
|
}
|
|
724
708
|
recordNarrowingMiss(scanState, response.value.length);
|
|
725
|
-
|
|
709
|
+
log.debug(
|
|
726
710
|
`Boolean filter retry found ${response.value.length} results, but none satisfied ${secondaryApplied.join(', ')} — continuing`
|
|
727
711
|
);
|
|
728
712
|
} catch (retryError) {
|
|
729
|
-
|
|
730
|
-
`Boolean filter retry also failed: ${retryError.message}`
|
|
731
|
-
);
|
|
713
|
+
log.debug(`Boolean filter retry also failed: ${retryError.message}`);
|
|
732
714
|
}
|
|
733
715
|
}
|
|
734
716
|
}
|
|
@@ -745,7 +727,7 @@ async function progressiveSearch(
|
|
|
745
727
|
searchTerms.kqlQuery;
|
|
746
728
|
|
|
747
729
|
if (hasAnyFilters) {
|
|
748
|
-
|
|
730
|
+
log.debug(
|
|
749
731
|
'All search strategies exhausted with filters active — returning 0 results'
|
|
750
732
|
);
|
|
751
733
|
searchAttempts.push('no-results');
|
|
@@ -774,7 +756,7 @@ async function progressiveSearch(
|
|
|
774
756
|
}
|
|
775
757
|
|
|
776
758
|
// No search filters specified — return recent emails (list mode)
|
|
777
|
-
|
|
759
|
+
log.debug('No search filters specified, returning recent emails');
|
|
778
760
|
searchAttempts.push('recent-emails');
|
|
779
761
|
|
|
780
762
|
const basicParams = {
|
|
@@ -790,7 +772,7 @@ async function progressiveSearch(
|
|
|
790
772
|
basicParams,
|
|
791
773
|
maxCount
|
|
792
774
|
);
|
|
793
|
-
|
|
775
|
+
log.debug(`Recent emails: found ${response.value?.length || 0} results`);
|
|
794
776
|
|
|
795
777
|
response._searchInfo = {
|
|
796
778
|
attemptsCount: searchAttempts.length,
|
|
@@ -1023,6 +1005,7 @@ function narrowResponse(response, matched) {
|
|
|
1023
1005
|
if (dropped) {
|
|
1024
1006
|
delete response['@odata.count'];
|
|
1025
1007
|
delete response['@odata.nextLink'];
|
|
1008
|
+
delete response.hasMore;
|
|
1026
1009
|
}
|
|
1027
1010
|
}
|
|
1028
1011
|
|
|
@@ -1177,7 +1160,7 @@ async function runClientSideFallback(
|
|
|
1177
1160
|
if (matched.length === 0) {
|
|
1178
1161
|
return null;
|
|
1179
1162
|
}
|
|
1180
|
-
|
|
1163
|
+
log.debug(
|
|
1181
1164
|
`Client-side ${kind} matched ${matched.length} of ${messages.length} scanned messages`
|
|
1182
1165
|
);
|
|
1183
1166
|
return {
|
|
@@ -1257,7 +1240,7 @@ function buildSearchParams(searchTerms, filterTerms, count, selectFields) {
|
|
|
1257
1240
|
const afterDate = new Date(filterTerms.receivedAfter).toISOString();
|
|
1258
1241
|
filterConditions.push(`receivedDateTime ge ${afterDate}`);
|
|
1259
1242
|
} catch (_e) {
|
|
1260
|
-
|
|
1243
|
+
log.debug(`Invalid receivedAfter date: ${filterTerms.receivedAfter}`);
|
|
1261
1244
|
}
|
|
1262
1245
|
}
|
|
1263
1246
|
|
|
@@ -1266,9 +1249,7 @@ function buildSearchParams(searchTerms, filterTerms, count, selectFields) {
|
|
|
1266
1249
|
const beforeDate = new Date(filterTerms.receivedBefore).toISOString();
|
|
1267
1250
|
filterConditions.push(`receivedDateTime le ${beforeDate}`);
|
|
1268
1251
|
} catch (_e) {
|
|
1269
|
-
|
|
1270
|
-
`Invalid receivedBefore date: ${filterTerms.receivedBefore}`
|
|
1271
|
-
);
|
|
1252
|
+
log.debug(`Invalid receivedBefore date: ${filterTerms.receivedBefore}`);
|
|
1272
1253
|
}
|
|
1273
1254
|
}
|
|
1274
1255
|
|
|
@@ -1307,7 +1288,7 @@ function addBooleanFilters(params, filterTerms) {
|
|
|
1307
1288
|
const afterDate = new Date(filterTerms.receivedAfter).toISOString();
|
|
1308
1289
|
filterConditions.push(`receivedDateTime ge ${afterDate}`);
|
|
1309
1290
|
} catch (_e) {
|
|
1310
|
-
|
|
1291
|
+
log.debug(`Invalid receivedAfter date: ${filterTerms.receivedAfter}`);
|
|
1311
1292
|
}
|
|
1312
1293
|
}
|
|
1313
1294
|
|
|
@@ -1316,9 +1297,7 @@ function addBooleanFilters(params, filterTerms) {
|
|
|
1316
1297
|
const beforeDate = new Date(filterTerms.receivedBefore).toISOString();
|
|
1317
1298
|
filterConditions.push(`receivedDateTime le ${beforeDate}`);
|
|
1318
1299
|
} catch (_e) {
|
|
1319
|
-
|
|
1320
|
-
`Invalid receivedBefore date: ${filterTerms.receivedBefore}`
|
|
1321
|
-
);
|
|
1300
|
+
log.debug(`Invalid receivedBefore date: ${filterTerms.receivedBefore}`);
|
|
1322
1301
|
}
|
|
1323
1302
|
}
|
|
1324
1303
|
|
|
@@ -1440,7 +1419,7 @@ function formatSearchResults(response, folder, verbosity, searchAllFolders) {
|
|
|
1440
1419
|
const meta = {
|
|
1441
1420
|
returned: (response.value || []).length,
|
|
1442
1421
|
totalAvailable: response['@odata.count'] || null,
|
|
1443
|
-
hasMore: Boolean(response['@odata.nextLink']),
|
|
1422
|
+
hasMore: Boolean(response.hasMore || response['@odata.nextLink']),
|
|
1444
1423
|
verbosity: verbosity,
|
|
1445
1424
|
};
|
|
1446
1425
|
|
|
@@ -1585,14 +1564,9 @@ async function handleSearchByMessageId(args) {
|
|
|
1585
1564
|
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
1586
1565
|
|
|
1587
1566
|
if (!messageId) {
|
|
1588
|
-
return
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
type: 'text',
|
|
1592
|
-
text: 'Message-ID is required. Provide the full Message-ID header value (e.g., <abc123@example.com>)',
|
|
1593
|
-
},
|
|
1594
|
-
],
|
|
1595
|
-
};
|
|
1567
|
+
return toolError(
|
|
1568
|
+
'Message-ID is required. Provide the full Message-ID header value (e.g., <abc123@example.com>)'
|
|
1569
|
+
);
|
|
1596
1570
|
}
|
|
1597
1571
|
|
|
1598
1572
|
try {
|
|
@@ -1613,7 +1587,7 @@ async function handleSearchByMessageId(args) {
|
|
|
1613
1587
|
$top: '10', // Usually only one match, but allow for edge cases
|
|
1614
1588
|
};
|
|
1615
1589
|
|
|
1616
|
-
|
|
1590
|
+
log.debug(`Searching for Message-ID: ${messageId}`);
|
|
1617
1591
|
|
|
1618
1592
|
const response = await callGraphAPI(
|
|
1619
1593
|
accessToken,
|
|
@@ -1659,24 +1633,10 @@ async function handleSearchByMessageId(args) {
|
|
|
1659
1633
|
};
|
|
1660
1634
|
} catch (error) {
|
|
1661
1635
|
if (error.message === 'Authentication required') {
|
|
1662
|
-
return
|
|
1663
|
-
content: [
|
|
1664
|
-
{
|
|
1665
|
-
type: 'text',
|
|
1666
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
1667
|
-
},
|
|
1668
|
-
],
|
|
1669
|
-
};
|
|
1636
|
+
return authRequiredError();
|
|
1670
1637
|
}
|
|
1671
1638
|
|
|
1672
|
-
return {
|
|
1673
|
-
content: [
|
|
1674
|
-
{
|
|
1675
|
-
type: 'text',
|
|
1676
|
-
text: `Error searching by Message-ID: ${error.message}`,
|
|
1677
|
-
},
|
|
1678
|
-
],
|
|
1679
|
-
};
|
|
1639
|
+
return toolError(`Error searching by Message-ID: ${error.message}`);
|
|
1680
1640
|
}
|
|
1681
1641
|
}
|
|
1682
1642
|
|
package/email/send.js
CHANGED
|
@@ -10,6 +10,34 @@ const {
|
|
|
10
10
|
formatDryRunPreview,
|
|
11
11
|
} = require('../utils/safety');
|
|
12
12
|
const { handleGetMailTips } = require('./mail-tips');
|
|
13
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mail-tip issues that stop a send until the caller passes
|
|
17
|
+
* `acknowledgeWarnings: true` (#272). Custom tips and moderation are
|
|
18
|
+
* returned with the result but do not block.
|
|
19
|
+
*/
|
|
20
|
+
const BLOCKING_TIP_LABELS = {
|
|
21
|
+
outOfOffice: 'out of office',
|
|
22
|
+
mailboxFull: 'mailbox full',
|
|
23
|
+
deliveryRestricted: 'delivery restricted',
|
|
24
|
+
external: 'external recipient',
|
|
25
|
+
externalMembers: 'group with external members',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DELIVERY_CAVEAT =
|
|
29
|
+
'Mail tips are M365-only: personal Outlook.com accounts return none, and no warnings is not proof the email will be delivered.';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One line per blocking issue, e.g. "- a@b.com: mailbox full".
|
|
33
|
+
* @param {Array<{address: string, type: string}>} issues
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function formatBlockingIssues(issues) {
|
|
37
|
+
return issues
|
|
38
|
+
.map((issue) => `- ${issue.address}: ${BLOCKING_TIP_LABELS[issue.type]}`)
|
|
39
|
+
.join('\n');
|
|
40
|
+
}
|
|
13
41
|
|
|
14
42
|
/**
|
|
15
43
|
* Send email handler
|
|
@@ -27,40 +55,20 @@ async function handleSendEmail(args) {
|
|
|
27
55
|
saveToSentItems = true,
|
|
28
56
|
dryRun = false,
|
|
29
57
|
checkRecipients = false,
|
|
58
|
+
acknowledgeWarnings = false,
|
|
30
59
|
} = args;
|
|
31
60
|
|
|
32
61
|
// Validate required parameters
|
|
33
62
|
if (!to) {
|
|
34
|
-
return
|
|
35
|
-
content: [
|
|
36
|
-
{
|
|
37
|
-
type: 'text',
|
|
38
|
-
text: 'Recipient (to) is required.',
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
};
|
|
63
|
+
return toolError('Recipient (to) is required.');
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
if (!subject) {
|
|
45
|
-
return
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: 'text',
|
|
49
|
-
text: 'Subject is required.',
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
};
|
|
67
|
+
return toolError('Subject is required.');
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
if (!body) {
|
|
56
|
-
return
|
|
57
|
-
content: [
|
|
58
|
-
{
|
|
59
|
-
type: 'text',
|
|
60
|
-
text: 'Body content is required.',
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
};
|
|
71
|
+
return toolError('Body content is required.');
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
try {
|
|
@@ -122,7 +130,9 @@ async function handleSendEmail(args) {
|
|
|
122
130
|
saveToSentItems,
|
|
123
131
|
};
|
|
124
132
|
|
|
125
|
-
// Pre-send mail tips check
|
|
133
|
+
// Pre-send mail tips check. The tips are kept out of emailObject: only
|
|
134
|
+
// Graph message properties may go in the sendMail payload (#272).
|
|
135
|
+
let mailTips = null;
|
|
126
136
|
if (checkRecipients) {
|
|
127
137
|
const allAddresses = allRecipients.map((r) => r.emailAddress.address);
|
|
128
138
|
const tipsResult = await handleGetMailTips({
|
|
@@ -130,27 +140,52 @@ async function handleSendEmail(args) {
|
|
|
130
140
|
});
|
|
131
141
|
|
|
132
142
|
const tipsText = tipsResult.content[0]?.text || '';
|
|
143
|
+
const blocking = (tipsResult._meta?.issues || []).filter(
|
|
144
|
+
(issue) => BLOCKING_TIP_LABELS[issue.type]
|
|
145
|
+
);
|
|
133
146
|
|
|
134
147
|
// In dry-run mode, always include mail tips in the preview
|
|
135
148
|
if (dryRun) {
|
|
136
149
|
const preview = formatDryRunPreview(emailObject);
|
|
150
|
+
const refusalNote =
|
|
151
|
+
blocking.length > 0 && !acknowledgeWarnings
|
|
152
|
+
? `\n\nNote: a real send would be refused until acknowledgeWarnings: true is passed, because of:\n${formatBlockingIssues(blocking)}`
|
|
153
|
+
: '';
|
|
137
154
|
return {
|
|
138
155
|
content: [
|
|
139
156
|
{
|
|
140
157
|
type: 'text',
|
|
141
|
-
text: `${tipsText}\n\n---\n\n${preview.content[0].text}`,
|
|
158
|
+
text: `${tipsText}\n\n---\n\n${preview.content[0].text}${refusalNote}`,
|
|
142
159
|
},
|
|
143
160
|
],
|
|
144
161
|
_meta: { mailTips: tipsResult._meta },
|
|
145
162
|
};
|
|
146
163
|
}
|
|
147
164
|
|
|
148
|
-
//
|
|
149
|
-
if (tipsResult.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
// The caller asked for a check, so a failed check stops the send.
|
|
166
|
+
if (tipsResult.isError) {
|
|
167
|
+
return toolError(
|
|
168
|
+
`Email not sent: the recipient check failed.\n\n${tipsText}`,
|
|
169
|
+
tipsText.includes('Next step:')
|
|
170
|
+
? {}
|
|
171
|
+
: {
|
|
172
|
+
nextStep:
|
|
173
|
+
'Retry the call, or call send-email without checkRecipients to send without the check.',
|
|
174
|
+
}
|
|
175
|
+
);
|
|
153
176
|
}
|
|
177
|
+
|
|
178
|
+
if (blocking.length > 0 && !acknowledgeWarnings) {
|
|
179
|
+
return toolError(
|
|
180
|
+
`Email not sent: the recipient check flagged ${blocking.length} issue(s):\n${formatBlockingIssues(blocking)}\n\n${tipsText}`,
|
|
181
|
+
{
|
|
182
|
+
nextStep:
|
|
183
|
+
'Show these warnings to the user. If they still want to send, call send-email again with the same arguments plus acknowledgeWarnings: true; otherwise change the recipients.',
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
mailTips = { text: tipsText, meta: tipsResult._meta, blocking };
|
|
154
189
|
}
|
|
155
190
|
|
|
156
191
|
// Dry-run mode: return preview without sending
|
|
@@ -168,34 +203,33 @@ async function handleSendEmail(args) {
|
|
|
168
203
|
// Make API call to send email
|
|
169
204
|
await callGraphAPI(accessToken, 'POST', 'me/sendMail', emailObject);
|
|
170
205
|
|
|
206
|
+
const sentText = `Email sent successfully!\n\nSubject: ${subject}\nRecipients: ${toRecipients.length}${ccRecipients.length > 0 ? ` + ${ccRecipients.length} CC` : ''}${bccRecipients.length > 0 ? ` + ${bccRecipients.length} BCC` : ''}\nMessage Length: ${body.length} characters`;
|
|
207
|
+
|
|
208
|
+
if (!mailTips) {
|
|
209
|
+
return { content: [{ type: 'text', text: sentText }] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Empty tips already carry their own M365-only note; don't repeat it.
|
|
213
|
+
const caveat = mailTips.meta?.allEmpty ? '' : `\n\n${DELIVERY_CAVEAT}`;
|
|
214
|
+
const acknowledged =
|
|
215
|
+
mailTips.blocking.length > 0
|
|
216
|
+
? `\n\nSent with ${mailTips.blocking.length} acknowledged warning(s):\n${formatBlockingIssues(mailTips.blocking)}`
|
|
217
|
+
: '';
|
|
171
218
|
return {
|
|
172
219
|
content: [
|
|
173
220
|
{
|
|
174
221
|
type: 'text',
|
|
175
|
-
text:
|
|
222
|
+
text: `${sentText}${acknowledged}\n\n---\n\n${mailTips.text}${caveat}`,
|
|
176
223
|
},
|
|
177
224
|
],
|
|
225
|
+
_meta: { mailTips: mailTips.meta },
|
|
178
226
|
};
|
|
179
227
|
} catch (error) {
|
|
180
228
|
if (error.message === 'Authentication required') {
|
|
181
|
-
return
|
|
182
|
-
content: [
|
|
183
|
-
{
|
|
184
|
-
type: 'text',
|
|
185
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
186
|
-
},
|
|
187
|
-
],
|
|
188
|
-
};
|
|
229
|
+
return authRequiredError();
|
|
189
230
|
}
|
|
190
231
|
|
|
191
|
-
return {
|
|
192
|
-
content: [
|
|
193
|
-
{
|
|
194
|
-
type: 'text',
|
|
195
|
-
text: `Error sending email: ${error.message}`,
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
};
|
|
232
|
+
return toolError(`Error sending email: ${error.message}`);
|
|
199
233
|
}
|
|
200
234
|
}
|
|
201
235
|
|
package/folder/create.js
CHANGED
|
@@ -5,6 +5,8 @@ const { callGraphAPI } = require('../utils/graph-api');
|
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
6
|
const { resolveFolder, listChildFolders } = require('./resolve');
|
|
7
7
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
8
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
9
|
+
const { log } = require('../utils/logger');
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Create folder handler
|
|
@@ -18,14 +20,7 @@ async function handleCreateFolder(args) {
|
|
|
18
20
|
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
19
21
|
|
|
20
22
|
if (!folderName) {
|
|
21
|
-
return
|
|
22
|
-
content: [
|
|
23
|
-
{
|
|
24
|
-
type: 'text',
|
|
25
|
-
text: 'Folder name is required.',
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
};
|
|
23
|
+
return toolError('Folder name is required.');
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
try {
|
|
@@ -52,24 +47,10 @@ async function handleCreateFolder(args) {
|
|
|
52
47
|
};
|
|
53
48
|
} catch (error) {
|
|
54
49
|
if (error.message === 'Authentication required') {
|
|
55
|
-
return
|
|
56
|
-
content: [
|
|
57
|
-
{
|
|
58
|
-
type: 'text',
|
|
59
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
};
|
|
50
|
+
return authRequiredError();
|
|
63
51
|
}
|
|
64
52
|
|
|
65
|
-
return {
|
|
66
|
-
content: [
|
|
67
|
-
{
|
|
68
|
-
type: 'text',
|
|
69
|
-
text: `Error creating folder: ${error.message}`,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
};
|
|
53
|
+
return toolError(`Error creating folder: ${error.message}`);
|
|
73
54
|
}
|
|
74
55
|
}
|
|
75
56
|
|
|
@@ -153,7 +134,7 @@ async function createMailFolder(accessToken, folderName, parentSpec) {
|
|
|
153
134
|
};
|
|
154
135
|
}
|
|
155
136
|
} catch (error) {
|
|
156
|
-
|
|
137
|
+
log.debug(`Error creating folder "${folderName}": ${error.message}`);
|
|
157
138
|
throw error;
|
|
158
139
|
}
|
|
159
140
|
}
|