@littlebearapps/outlook-assistant 3.7.0 → 3.7.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/email/search.js +278 -23
- package/package.json +1 -1
package/email/search.js
CHANGED
|
@@ -145,6 +145,12 @@ async function progressiveSearch(
|
|
|
145
145
|
console.error(
|
|
146
146
|
`Raw KQL search successful: found ${response.value.length} results`
|
|
147
147
|
);
|
|
148
|
+
response._searchInfo = {
|
|
149
|
+
attemptsCount: searchAttempts.length,
|
|
150
|
+
strategies: searchAttempts,
|
|
151
|
+
originalTerms: searchTerms,
|
|
152
|
+
filterTerms: filterTerms,
|
|
153
|
+
};
|
|
148
154
|
return response;
|
|
149
155
|
}
|
|
150
156
|
} catch (error) {
|
|
@@ -188,6 +194,12 @@ async function progressiveSearch(
|
|
|
188
194
|
console.error(
|
|
189
195
|
`Combined search successful: found ${response.value.length} results`
|
|
190
196
|
);
|
|
197
|
+
response._searchInfo = {
|
|
198
|
+
attemptsCount: searchAttempts.length,
|
|
199
|
+
strategies: searchAttempts,
|
|
200
|
+
originalTerms: searchTerms,
|
|
201
|
+
filterTerms: filterTerms,
|
|
202
|
+
};
|
|
191
203
|
return response;
|
|
192
204
|
}
|
|
193
205
|
} catch (error) {
|
|
@@ -241,10 +253,109 @@ async function progressiveSearch(
|
|
|
241
253
|
console.error(
|
|
242
254
|
`Search with ${term} successful: found ${response.value.length} results`
|
|
243
255
|
);
|
|
256
|
+
response._searchInfo = {
|
|
257
|
+
attemptsCount: searchAttempts.length,
|
|
258
|
+
strategies: searchAttempts,
|
|
259
|
+
originalTerms: searchTerms,
|
|
260
|
+
filterTerms: filterTerms,
|
|
261
|
+
};
|
|
244
262
|
return response;
|
|
245
263
|
}
|
|
264
|
+
|
|
265
|
+
// Client-side fallback for 'to' filter — toRecipients/any() lambda
|
|
266
|
+
// returns 0 results on personal accounts even when emails exist
|
|
267
|
+
if (term === 'to') {
|
|
268
|
+
console.error(
|
|
269
|
+
'to filter returned 0 results, trying client-side filtering'
|
|
270
|
+
);
|
|
271
|
+
searchAttempts.push('client-side-to');
|
|
272
|
+
const messages = await fetchForClientSideFilter(
|
|
273
|
+
accessToken,
|
|
274
|
+
endpoint,
|
|
275
|
+
maxCount
|
|
276
|
+
);
|
|
277
|
+
const matched = filterToClientSide(messages, searchTerms[term]);
|
|
278
|
+
if (matched.length > 0) {
|
|
279
|
+
console.error(
|
|
280
|
+
`Client-side to filter matched ${matched.length} of ${messages.length} messages`
|
|
281
|
+
);
|
|
282
|
+
return { value: matched.slice(0, maxCount) };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Client-side fallback for 'query' — search bodyPreview, subject, from
|
|
287
|
+
if (term === 'query') {
|
|
288
|
+
console.error(
|
|
289
|
+
'query contains(subject) returned 0 results, trying client-side body search'
|
|
290
|
+
);
|
|
291
|
+
searchAttempts.push('client-side-query');
|
|
292
|
+
const messages = await fetchForClientSideFilter(
|
|
293
|
+
accessToken,
|
|
294
|
+
endpoint,
|
|
295
|
+
maxCount
|
|
296
|
+
);
|
|
297
|
+
const matched = filterQueryClientSide(messages, searchTerms[term]);
|
|
298
|
+
if (matched.length > 0) {
|
|
299
|
+
console.error(
|
|
300
|
+
`Client-side query matched ${matched.length} of ${messages.length} messages`
|
|
301
|
+
);
|
|
302
|
+
return { value: matched.slice(0, maxCount) };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
246
305
|
} catch (error) {
|
|
247
306
|
console.error(`Search with ${term} failed: ${error.message}`);
|
|
307
|
+
|
|
308
|
+
// Client-side fallback for 'to' when API throws (e.g. InefficientFilter)
|
|
309
|
+
if (term === 'to') {
|
|
310
|
+
try {
|
|
311
|
+
console.error(
|
|
312
|
+
'to filter threw error, trying client-side filtering'
|
|
313
|
+
);
|
|
314
|
+
searchAttempts.push('client-side-to');
|
|
315
|
+
const messages = await fetchForClientSideFilter(
|
|
316
|
+
accessToken,
|
|
317
|
+
endpoint,
|
|
318
|
+
maxCount
|
|
319
|
+
);
|
|
320
|
+
const matched = filterToClientSide(messages, searchTerms[term]);
|
|
321
|
+
if (matched.length > 0) {
|
|
322
|
+
console.error(
|
|
323
|
+
`Client-side to filter matched ${matched.length} of ${messages.length} messages`
|
|
324
|
+
);
|
|
325
|
+
return { value: matched.slice(0, maxCount) };
|
|
326
|
+
}
|
|
327
|
+
} catch (fallbackError) {
|
|
328
|
+
console.error(
|
|
329
|
+
`Client-side to fallback also failed: ${fallbackError.message}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Client-side fallback for 'query' when API throws
|
|
335
|
+
if (term === 'query') {
|
|
336
|
+
try {
|
|
337
|
+
console.error(
|
|
338
|
+
'query filter threw error, trying client-side body search'
|
|
339
|
+
);
|
|
340
|
+
searchAttempts.push('client-side-query');
|
|
341
|
+
const messages = await fetchForClientSideFilter(
|
|
342
|
+
accessToken,
|
|
343
|
+
endpoint,
|
|
344
|
+
maxCount
|
|
345
|
+
);
|
|
346
|
+
const matched = filterQueryClientSide(messages, searchTerms[term]);
|
|
347
|
+
if (matched.length > 0) {
|
|
348
|
+
console.error(
|
|
349
|
+
`Client-side query matched ${matched.length} of ${messages.length} messages`
|
|
350
|
+
);
|
|
351
|
+
return { value: matched.slice(0, maxCount) };
|
|
352
|
+
}
|
|
353
|
+
} catch (fallbackError) {
|
|
354
|
+
console.error(
|
|
355
|
+
`Client-side query fallback also failed: ${fallbackError.message}`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
248
359
|
}
|
|
249
360
|
}
|
|
250
361
|
}
|
|
@@ -282,6 +393,12 @@ async function progressiveSearch(
|
|
|
282
393
|
console.error(
|
|
283
394
|
`Boolean filter search found ${response.value?.length || 0} results`
|
|
284
395
|
);
|
|
396
|
+
response._searchInfo = {
|
|
397
|
+
attemptsCount: searchAttempts.length,
|
|
398
|
+
strategies: searchAttempts,
|
|
399
|
+
originalTerms: searchTerms,
|
|
400
|
+
filterTerms: filterTerms,
|
|
401
|
+
};
|
|
285
402
|
return response;
|
|
286
403
|
} catch (error) {
|
|
287
404
|
console.error(`Boolean filter search failed: ${error.message}`);
|
|
@@ -301,6 +418,12 @@ async function progressiveSearch(
|
|
|
301
418
|
retryParams,
|
|
302
419
|
maxCount
|
|
303
420
|
);
|
|
421
|
+
response._searchInfo = {
|
|
422
|
+
attemptsCount: searchAttempts.length,
|
|
423
|
+
strategies: searchAttempts,
|
|
424
|
+
originalTerms: searchTerms,
|
|
425
|
+
filterTerms: filterTerms,
|
|
426
|
+
};
|
|
304
427
|
return response;
|
|
305
428
|
} catch (retryError) {
|
|
306
429
|
console.error(
|
|
@@ -311,8 +434,35 @@ async function progressiveSearch(
|
|
|
311
434
|
}
|
|
312
435
|
}
|
|
313
436
|
|
|
314
|
-
// 4. Final fallback
|
|
315
|
-
|
|
437
|
+
// 4. Final fallback
|
|
438
|
+
// If the user specified search filters, return 0 results with guidance
|
|
439
|
+
// instead of silently returning unfiltered recent emails.
|
|
440
|
+
const hasAnyFilters =
|
|
441
|
+
searchTerms.query ||
|
|
442
|
+
searchTerms.from ||
|
|
443
|
+
searchTerms.to ||
|
|
444
|
+
searchTerms.subject ||
|
|
445
|
+
searchTerms.kqlQuery;
|
|
446
|
+
|
|
447
|
+
if (hasAnyFilters) {
|
|
448
|
+
console.error(
|
|
449
|
+
'All search strategies exhausted with filters active — returning 0 results'
|
|
450
|
+
);
|
|
451
|
+
searchAttempts.push('no-results');
|
|
452
|
+
return {
|
|
453
|
+
value: [],
|
|
454
|
+
_searchInfo: {
|
|
455
|
+
attemptsCount: searchAttempts.length,
|
|
456
|
+
strategies: searchAttempts,
|
|
457
|
+
originalTerms: searchTerms,
|
|
458
|
+
filterTerms: filterTerms,
|
|
459
|
+
noResults: true,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// No search filters specified — return recent emails (list mode)
|
|
465
|
+
console.error('No search filters specified, returning recent emails');
|
|
316
466
|
searchAttempts.push('recent-emails');
|
|
317
467
|
|
|
318
468
|
const basicParams = {
|
|
@@ -328,11 +478,8 @@ async function progressiveSearch(
|
|
|
328
478
|
basicParams,
|
|
329
479
|
maxCount
|
|
330
480
|
);
|
|
331
|
-
console.error(
|
|
332
|
-
`Fallback to recent emails found ${response.value?.length || 0} results`
|
|
333
|
-
);
|
|
481
|
+
console.error(`Recent emails: found ${response.value?.length || 0} results`);
|
|
334
482
|
|
|
335
|
-
// Add a note to the response about the search attempts
|
|
336
483
|
response._searchInfo = {
|
|
337
484
|
attemptsCount: searchAttempts.length,
|
|
338
485
|
strategies: searchAttempts,
|
|
@@ -391,6 +538,74 @@ function buildToFilter(val) {
|
|
|
391
538
|
return `toRecipients/any(r: contains(r/emailAddress/name, '${val}'))`;
|
|
392
539
|
}
|
|
393
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Client-side filter for toRecipients — used when the OData toRecipients/any()
|
|
543
|
+
* lambda expression fails on personal accounts (InefficientFilter).
|
|
544
|
+
* Mirrors the pattern from conversations.js lines 138-147.
|
|
545
|
+
* @param {Array} messages - Array of message objects with toRecipients
|
|
546
|
+
* @param {string} toValue - The to filter value (email, domain, or name)
|
|
547
|
+
* @returns {Array} - Filtered messages where at least one recipient matches
|
|
548
|
+
*/
|
|
549
|
+
function filterToClientSide(messages, toValue) {
|
|
550
|
+
const toLower = toValue.toLowerCase();
|
|
551
|
+
return messages.filter((m) =>
|
|
552
|
+
(m.toRecipients || []).some((r) => {
|
|
553
|
+
const addr = (r.emailAddress?.address || '').toLowerCase();
|
|
554
|
+
const name = (r.emailAddress?.name || '').toLowerCase();
|
|
555
|
+
return addr.includes(toLower) || name.includes(toLower);
|
|
556
|
+
})
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Client-side filter for free-text query — used when $search and
|
|
562
|
+
* contains(subject) both fail on personal accounts.
|
|
563
|
+
* Searches subject, bodyPreview, from address, and from name.
|
|
564
|
+
* @param {Array} messages - Array of message objects
|
|
565
|
+
* @param {string} queryText - The query text to search for
|
|
566
|
+
* @returns {Array} - Filtered messages matching the query
|
|
567
|
+
*/
|
|
568
|
+
function filterQueryClientSide(messages, queryText) {
|
|
569
|
+
const queryLower = queryText.toLowerCase();
|
|
570
|
+
return messages.filter((m) => {
|
|
571
|
+
const subject = (m.subject || '').toLowerCase();
|
|
572
|
+
const body = (m.bodyPreview || '').toLowerCase();
|
|
573
|
+
const fromAddr = (m.from?.emailAddress?.address || '').toLowerCase();
|
|
574
|
+
const fromName = (m.from?.emailAddress?.name || '').toLowerCase();
|
|
575
|
+
return (
|
|
576
|
+
subject.includes(queryLower) ||
|
|
577
|
+
body.includes(queryLower) ||
|
|
578
|
+
fromAddr.includes(queryLower) ||
|
|
579
|
+
fromName.includes(queryLower)
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Fetch recent messages for client-side filtering fallback.
|
|
586
|
+
* Uses the 'search' field preset which includes toRecipients and bodyPreview.
|
|
587
|
+
* @param {string} accessToken - Access token
|
|
588
|
+
* @param {string} endpoint - API endpoint
|
|
589
|
+
* @param {number} maxCount - Maximum results to fetch
|
|
590
|
+
* @returns {Promise<Array>} - Array of message objects
|
|
591
|
+
*/
|
|
592
|
+
async function fetchForClientSideFilter(accessToken, endpoint, maxCount) {
|
|
593
|
+
const searchFields = getEmailFields('search');
|
|
594
|
+
const params = {
|
|
595
|
+
$top: Math.min(200, maxCount * 5),
|
|
596
|
+
$select: searchFields,
|
|
597
|
+
$orderby: 'receivedDateTime desc',
|
|
598
|
+
};
|
|
599
|
+
const response = await callGraphAPIPaginated(
|
|
600
|
+
accessToken,
|
|
601
|
+
'GET',
|
|
602
|
+
endpoint,
|
|
603
|
+
params,
|
|
604
|
+
Math.min(200, maxCount * 5)
|
|
605
|
+
);
|
|
606
|
+
return response.value || [];
|
|
607
|
+
}
|
|
608
|
+
|
|
394
609
|
/**
|
|
395
610
|
* Build search parameters from search terms and filter terms
|
|
396
611
|
* Uses $filter for email addresses (more reliable than $search)
|
|
@@ -531,37 +746,75 @@ function addBooleanFilters(params, filterTerms) {
|
|
|
531
746
|
* @returns {object} - MCP response object
|
|
532
747
|
*/
|
|
533
748
|
function formatSearchResults(response, folder, verbosity) {
|
|
749
|
+
// Build metadata
|
|
750
|
+
const meta = {
|
|
751
|
+
returned: (response.value || []).length,
|
|
752
|
+
totalAvailable: response['@odata.count'] || null,
|
|
753
|
+
hasMore: Boolean(response['@odata.nextLink']),
|
|
754
|
+
verbosity: verbosity,
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// Add searchMetadata to _meta when available (for programmatic fallback detection)
|
|
758
|
+
if (response._searchInfo) {
|
|
759
|
+
const finalStrategy =
|
|
760
|
+
response._searchInfo.strategies[
|
|
761
|
+
response._searchInfo.strategies.length - 1
|
|
762
|
+
];
|
|
763
|
+
meta.searchMetadata = {
|
|
764
|
+
strategiesAttempted: response._searchInfo.strategies,
|
|
765
|
+
finalStrategy: finalStrategy,
|
|
766
|
+
filterApplied: !response._searchInfo.noResults,
|
|
767
|
+
originalFilters: response._searchInfo.originalTerms,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Handle 0 results
|
|
534
772
|
if (!response.value || response.value.length === 0) {
|
|
773
|
+
// Actionable guidance when filters were specified but matched nothing
|
|
774
|
+
if (response._searchInfo?.noResults) {
|
|
775
|
+
const filters = response._searchInfo.originalTerms || {};
|
|
776
|
+
const activeFilters = Object.entries(filters)
|
|
777
|
+
.filter(([, v]) => v)
|
|
778
|
+
.map(([k]) => k);
|
|
779
|
+
const filterDesc =
|
|
780
|
+
activeFilters.length > 0
|
|
781
|
+
? ` (filters: ${activeFilters.join(', ')})`
|
|
782
|
+
: '';
|
|
783
|
+
|
|
784
|
+
const text =
|
|
785
|
+
`No emails found matching your filters in "${folder}"${filterDesc}.\n\n` +
|
|
786
|
+
'**Suggestions:**\n' +
|
|
787
|
+
'- Try `searchAllFolders: true` to search across all folders including Archive\n' +
|
|
788
|
+
'- Specify the correct folder if emails have been moved (use `folders` tool to list folders)\n' +
|
|
789
|
+
'- Use `from` filter instead of `to` (more reliable on personal accounts)\n' +
|
|
790
|
+
'- Use `kqlQuery` with `searchAllFolders: true` for cross-folder search';
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
content: [{ type: 'text', text }],
|
|
794
|
+
_meta: meta,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
535
798
|
return {
|
|
536
799
|
content: [
|
|
537
800
|
{
|
|
538
801
|
type: 'text',
|
|
539
|
-
text:
|
|
802
|
+
text: 'No emails found matching your search criteria.',
|
|
540
803
|
},
|
|
541
804
|
],
|
|
805
|
+
_meta: meta,
|
|
542
806
|
};
|
|
543
807
|
}
|
|
544
808
|
|
|
545
|
-
//
|
|
546
|
-
const meta = {
|
|
547
|
-
returned: response.value.length,
|
|
548
|
-
totalAvailable: response['@odata.count'] || null,
|
|
549
|
-
hasMore: Boolean(response['@odata.nextLink']),
|
|
550
|
-
verbosity: verbosity,
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
// Add search strategy info if available (for debugging)
|
|
809
|
+
// Add search strategy note for transparency
|
|
554
810
|
let searchNote = '';
|
|
555
811
|
if (response._searchInfo) {
|
|
556
|
-
const strategy =
|
|
557
|
-
response._searchInfo.strategies[
|
|
558
|
-
response._searchInfo.strategies.length - 1
|
|
559
|
-
];
|
|
812
|
+
const strategy = meta.searchMetadata.finalStrategy;
|
|
560
813
|
if (strategy === 'recent-emails') {
|
|
561
814
|
searchNote =
|
|
562
|
-
'\n\n**Note**:
|
|
563
|
-
|
|
564
|
-
|
|
815
|
+
'\n\n**Note**: No search filters were applied — showing recent emails.';
|
|
816
|
+
} else if (strategy.startsWith('client-side-')) {
|
|
817
|
+
searchNote = `\n\n_Search strategy: ${strategy} (filtered locally due to personal account API limitations)_`;
|
|
565
818
|
} else {
|
|
566
819
|
searchNote = `\n\n_Search strategy: ${strategy}_`;
|
|
567
820
|
}
|
|
@@ -699,4 +952,6 @@ module.exports = {
|
|
|
699
952
|
buildFromFilter,
|
|
700
953
|
buildToFilter,
|
|
701
954
|
classifyEmailFilter,
|
|
955
|
+
filterToClientSide,
|
|
956
|
+
filterQueryClientSide,
|
|
702
957
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.1",
|
|
4
4
|
"mcpName": "io.github.littlebearapps/outlook-assistant",
|
|
5
5
|
"description": "Outlook Assistant — MCP server with 22 tools for email, calendar, contacts, and settings via Microsoft Graph API",
|
|
6
6
|
"main": "index.js",
|