ms365-mcp-server 1.1.10 → 1.1.11
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/dist/index.js +1 -1
- package/dist/utils/ms365-operations.js +746 -108
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -643,7 +643,7 @@ export class MS365Operations {
|
|
|
643
643
|
}
|
|
644
644
|
}
|
|
645
645
|
/**
|
|
646
|
-
* Search
|
|
646
|
+
* Advanced email search using Microsoft Graph Search API with KQL - Persistent search until results found
|
|
647
647
|
*/
|
|
648
648
|
async searchEmails(criteria = {}) {
|
|
649
649
|
return await this.executeWithAuth(async () => {
|
|
@@ -654,152 +654,491 @@ export class MS365Operations {
|
|
|
654
654
|
if (cachedResults) {
|
|
655
655
|
return cachedResults;
|
|
656
656
|
}
|
|
657
|
-
|
|
657
|
+
const maxResults = criteria.maxResults || 50;
|
|
658
|
+
let allMessages = [];
|
|
659
|
+
let searchAttempts = 0;
|
|
660
|
+
const maxAttempts = 6; // Try multiple strategies
|
|
661
|
+
logger.log(`🔍 Starting persistent search with criteria: ${JSON.stringify(criteria)}`);
|
|
658
662
|
try {
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
.
|
|
663
|
-
.
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
})) || [],
|
|
680
|
-
receivedDateTime: email.receivedDateTime,
|
|
681
|
-
sentDateTime: email.sentDateTime,
|
|
682
|
-
bodyPreview: email.bodyPreview || '',
|
|
683
|
-
isRead: email.isRead || false,
|
|
684
|
-
hasAttachments: email.hasAttachments || false,
|
|
685
|
-
importance: email.importance || 'normal',
|
|
686
|
-
conversationId: email.conversationId || '',
|
|
687
|
-
parentFolderId: email.parentFolderId || '',
|
|
688
|
-
webLink: email.webLink || ''
|
|
689
|
-
})) || [];
|
|
690
|
-
// Apply manual filtering for all criteria
|
|
691
|
-
messages = this.applyManualFiltering(messages, criteria);
|
|
692
|
-
// For emails with attachments, get attachment counts
|
|
693
|
-
for (const message of messages) {
|
|
694
|
-
if (message.hasAttachments) {
|
|
695
|
-
try {
|
|
696
|
-
const attachments = await graphClient
|
|
697
|
-
.api(`/me/messages/${message.id}/attachments`)
|
|
698
|
-
.select('id')
|
|
699
|
-
.get();
|
|
700
|
-
message.attachments = new Array(attachments.value?.length || 0);
|
|
663
|
+
// Strategy 1: Use Microsoft Graph Search API for text-based queries
|
|
664
|
+
if (criteria.query && searchAttempts < maxAttempts) {
|
|
665
|
+
searchAttempts++;
|
|
666
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using Graph Search API for query: "${criteria.query}"`);
|
|
667
|
+
const searchResults = await this.performGraphSearch(criteria.query, maxResults * 2); // Search for more to increase chances
|
|
668
|
+
allMessages.push(...searchResults);
|
|
669
|
+
if (allMessages.length > 0) {
|
|
670
|
+
logger.log(`✅ Found ${allMessages.length} results with Graph Search API`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Strategy 2: Use KQL (Keyword Query Language) for advanced searches
|
|
674
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
675
|
+
const kqlQuery = this.buildKQLQuery(criteria);
|
|
676
|
+
if (kqlQuery) {
|
|
677
|
+
searchAttempts++;
|
|
678
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using KQL search: "${kqlQuery}"`);
|
|
679
|
+
const kqlResults = await this.performKQLSearch(kqlQuery, maxResults * 2);
|
|
680
|
+
allMessages.push(...kqlResults);
|
|
681
|
+
if (allMessages.length > 0) {
|
|
682
|
+
logger.log(`✅ Found ${allMessages.length} results with KQL search`);
|
|
701
683
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Strategy 3: Try relaxed KQL search (remove some constraints)
|
|
687
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
688
|
+
const relaxedKQL = this.buildRelaxedKQLQuery(criteria);
|
|
689
|
+
if (relaxedKQL && relaxedKQL !== this.buildKQLQuery(criteria)) {
|
|
690
|
+
searchAttempts++;
|
|
691
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using relaxed KQL search: "${relaxedKQL}"`);
|
|
692
|
+
const relaxedResults = await this.performKQLSearch(relaxedKQL, maxResults * 2);
|
|
693
|
+
allMessages.push(...relaxedResults);
|
|
694
|
+
if (allMessages.length > 0) {
|
|
695
|
+
logger.log(`✅ Found ${allMessages.length} results with relaxed KQL search`);
|
|
705
696
|
}
|
|
706
697
|
}
|
|
707
698
|
}
|
|
699
|
+
// Strategy 4: OData filter search with broader scope
|
|
700
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
701
|
+
searchAttempts++;
|
|
702
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using OData filter search with broader scope`);
|
|
703
|
+
const filterResults = await this.performFilteredSearch(criteria, maxResults * 3); // Even broader search
|
|
704
|
+
allMessages.push(...filterResults);
|
|
705
|
+
if (allMessages.length > 0) {
|
|
706
|
+
logger.log(`✅ Found ${allMessages.length} results with OData filter search`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Strategy 5: Partial text search across recent emails (expanded scope)
|
|
710
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
711
|
+
searchAttempts++;
|
|
712
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using expanded partial text search`);
|
|
713
|
+
const partialResults = await this.performPartialTextSearch(criteria, maxResults * 4); // Very broad search
|
|
714
|
+
allMessages.push(...partialResults);
|
|
715
|
+
if (allMessages.length > 0) {
|
|
716
|
+
logger.log(`✅ Found ${allMessages.length} results with partial text search`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Strategy 6: Fallback to basic search with maximum scope
|
|
720
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
721
|
+
searchAttempts++;
|
|
722
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using fallback basic search with maximum scope`);
|
|
723
|
+
const basicResult = await this.performBasicSearch({
|
|
724
|
+
...criteria,
|
|
725
|
+
maxResults: Math.max(maxResults * 5, 500) // Very large scope
|
|
726
|
+
});
|
|
727
|
+
allMessages.push(...basicResult.messages);
|
|
728
|
+
if (allMessages.length > 0) {
|
|
729
|
+
logger.log(`✅ Found ${allMessages.length} results with basic search fallback`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Remove duplicates and apply advanced filtering
|
|
733
|
+
const uniqueMessages = this.removeDuplicateMessages(allMessages);
|
|
734
|
+
logger.log(`📧 After deduplication: ${uniqueMessages.length} unique messages`);
|
|
735
|
+
const filteredMessages = this.applyAdvancedFiltering(uniqueMessages, criteria);
|
|
736
|
+
logger.log(`📧 After advanced filtering: ${filteredMessages.length} filtered messages`);
|
|
737
|
+
// Sort by relevance and date
|
|
738
|
+
const sortedMessages = this.sortSearchResults(filteredMessages, criteria);
|
|
739
|
+
// If still no results, try one more time with very relaxed criteria
|
|
740
|
+
if (sortedMessages.length === 0 && (criteria.query || criteria.from || criteria.subject)) {
|
|
741
|
+
logger.log(`🔍 Final attempt: Ultra-relaxed search for any partial matches`);
|
|
742
|
+
const ultraRelaxedResults = await this.performUltraRelaxedSearch(criteria, maxResults * 3);
|
|
743
|
+
const finalFiltered = this.applyUltraRelaxedFiltering(ultraRelaxedResults, criteria);
|
|
744
|
+
if (finalFiltered.length > 0) {
|
|
745
|
+
logger.log(`✅ Ultra-relaxed search found ${finalFiltered.length} results`);
|
|
746
|
+
const limitedMessages = finalFiltered.slice(0, maxResults);
|
|
747
|
+
const searchResult = {
|
|
748
|
+
messages: limitedMessages,
|
|
749
|
+
hasMore: finalFiltered.length > maxResults
|
|
750
|
+
};
|
|
751
|
+
this.setCachedResults(cacheKey, searchResult);
|
|
752
|
+
return searchResult;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// Limit results
|
|
756
|
+
const limitedMessages = sortedMessages.slice(0, maxResults);
|
|
708
757
|
const searchResult = {
|
|
709
|
-
messages,
|
|
710
|
-
hasMore:
|
|
758
|
+
messages: limitedMessages,
|
|
759
|
+
hasMore: sortedMessages.length > maxResults
|
|
711
760
|
};
|
|
712
761
|
this.setCachedResults(cacheKey, searchResult);
|
|
762
|
+
logger.log(`🔍 Search completed after ${searchAttempts} attempts: ${limitedMessages.length} final results`);
|
|
763
|
+
if (limitedMessages.length === 0) {
|
|
764
|
+
logger.log(`❌ No results found despite ${searchAttempts} search strategies. Consider broadening search criteria.`);
|
|
765
|
+
}
|
|
713
766
|
return searchResult;
|
|
714
767
|
}
|
|
715
768
|
catch (error) {
|
|
716
|
-
logger.error('Error in email search:', error);
|
|
717
|
-
|
|
769
|
+
logger.error('Error in persistent email search:', error);
|
|
770
|
+
// Final fallback - get some recent emails and filter them
|
|
771
|
+
logger.log('🔄 Final fallback: getting recent emails to filter manually');
|
|
772
|
+
return await this.performBasicSearch(criteria);
|
|
718
773
|
}
|
|
719
774
|
}, 'searchEmails');
|
|
720
775
|
}
|
|
721
776
|
/**
|
|
722
|
-
*
|
|
777
|
+
* Use Microsoft Graph Search API for full-text search
|
|
723
778
|
*/
|
|
724
|
-
|
|
779
|
+
async performGraphSearch(query, maxResults) {
|
|
780
|
+
try {
|
|
781
|
+
const graphClient = await this.getGraphClient();
|
|
782
|
+
const searchRequest = {
|
|
783
|
+
requests: [
|
|
784
|
+
{
|
|
785
|
+
entityTypes: ['message'],
|
|
786
|
+
query: {
|
|
787
|
+
queryString: query
|
|
788
|
+
},
|
|
789
|
+
from: 0,
|
|
790
|
+
size: Math.min(maxResults, 1000), // Graph Search max is 1000
|
|
791
|
+
fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
|
|
792
|
+
}
|
|
793
|
+
]
|
|
794
|
+
};
|
|
795
|
+
const result = await graphClient
|
|
796
|
+
.api('/search/query')
|
|
797
|
+
.post(searchRequest);
|
|
798
|
+
const messages = [];
|
|
799
|
+
if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
|
|
800
|
+
for (const hit of result.value[0].hitsContainers[0].hits) {
|
|
801
|
+
const email = hit.resource;
|
|
802
|
+
messages.push(this.mapEmailResult(email));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
logger.log(`📧 Graph Search returned ${messages.length} results`);
|
|
806
|
+
return messages;
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
logger.log(`❌ Graph Search failed: ${error}. Falling back to alternative search.`);
|
|
810
|
+
return [];
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Build KQL (Keyword Query Language) for advanced searches
|
|
815
|
+
*/
|
|
816
|
+
buildKQLQuery(criteria) {
|
|
817
|
+
const kqlParts = [];
|
|
818
|
+
if (criteria.from) {
|
|
819
|
+
// Smart sender search - handles partial names, emails, display names
|
|
820
|
+
const fromTerm = criteria.from.trim();
|
|
821
|
+
if (fromTerm.includes('@')) {
|
|
822
|
+
kqlParts.push(`from:${fromTerm}`);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
// For names, search in both from field and sender
|
|
826
|
+
kqlParts.push(`(from:"${fromTerm}" OR sender:"${fromTerm}" OR from:*${fromTerm}*)`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (criteria.to) {
|
|
830
|
+
kqlParts.push(`to:${criteria.to}`);
|
|
831
|
+
}
|
|
832
|
+
if (criteria.cc) {
|
|
833
|
+
kqlParts.push(`cc:${criteria.cc}`);
|
|
834
|
+
}
|
|
835
|
+
if (criteria.subject) {
|
|
836
|
+
kqlParts.push(`subject:"${criteria.subject}"`);
|
|
837
|
+
}
|
|
838
|
+
if (criteria.hasAttachment === true) {
|
|
839
|
+
kqlParts.push('hasattachment:true');
|
|
840
|
+
}
|
|
841
|
+
else if (criteria.hasAttachment === false) {
|
|
842
|
+
kqlParts.push('hasattachment:false');
|
|
843
|
+
}
|
|
844
|
+
if (criteria.isUnread === true) {
|
|
845
|
+
kqlParts.push('isread:false');
|
|
846
|
+
}
|
|
847
|
+
else if (criteria.isUnread === false) {
|
|
848
|
+
kqlParts.push('isread:true');
|
|
849
|
+
}
|
|
850
|
+
if (criteria.importance) {
|
|
851
|
+
kqlParts.push(`importance:${criteria.importance}`);
|
|
852
|
+
}
|
|
853
|
+
if (criteria.after) {
|
|
854
|
+
const afterDate = new Date(criteria.after).toISOString().split('T')[0];
|
|
855
|
+
kqlParts.push(`received>=${afterDate}`);
|
|
856
|
+
}
|
|
857
|
+
if (criteria.before) {
|
|
858
|
+
const beforeDate = new Date(criteria.before).toISOString().split('T')[0];
|
|
859
|
+
kqlParts.push(`received<=${beforeDate}`);
|
|
860
|
+
}
|
|
861
|
+
if (criteria.folder) {
|
|
862
|
+
kqlParts.push(`foldernames:"${criteria.folder}"`);
|
|
863
|
+
}
|
|
864
|
+
return kqlParts.join(' AND ');
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Perform KQL-based search using Graph Search API
|
|
868
|
+
*/
|
|
869
|
+
async performKQLSearch(kqlQuery, maxResults) {
|
|
870
|
+
try {
|
|
871
|
+
const graphClient = await this.getGraphClient();
|
|
872
|
+
const searchRequest = {
|
|
873
|
+
requests: [
|
|
874
|
+
{
|
|
875
|
+
entityTypes: ['message'],
|
|
876
|
+
query: {
|
|
877
|
+
queryString: kqlQuery
|
|
878
|
+
},
|
|
879
|
+
from: 0,
|
|
880
|
+
size: Math.min(maxResults, 1000),
|
|
881
|
+
fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
|
|
882
|
+
}
|
|
883
|
+
]
|
|
884
|
+
};
|
|
885
|
+
const result = await graphClient
|
|
886
|
+
.api('/search/query')
|
|
887
|
+
.post(searchRequest);
|
|
888
|
+
const messages = [];
|
|
889
|
+
if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
|
|
890
|
+
for (const hit of result.value[0].hitsContainers[0].hits) {
|
|
891
|
+
const email = hit.resource;
|
|
892
|
+
messages.push(this.mapEmailResult(email));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
logger.log(`📧 KQL Search returned ${messages.length} results`);
|
|
896
|
+
return messages;
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
logger.log(`❌ KQL Search failed: ${error}. Falling back to filter search.`);
|
|
900
|
+
return [];
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Fallback to OData filter-based search
|
|
905
|
+
*/
|
|
906
|
+
async performFilteredSearch(criteria, maxResults) {
|
|
907
|
+
try {
|
|
908
|
+
const graphClient = await this.getGraphClient();
|
|
909
|
+
let apiCall = graphClient
|
|
910
|
+
.api('/me/messages')
|
|
911
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
912
|
+
.orderby('receivedDateTime desc')
|
|
913
|
+
.top(Math.min(maxResults, 999));
|
|
914
|
+
// Apply OData filters where possible
|
|
915
|
+
const filters = [];
|
|
916
|
+
if (criteria.from && criteria.from.includes('@')) {
|
|
917
|
+
filters.push(`from/emailAddress/address eq '${criteria.from}'`);
|
|
918
|
+
}
|
|
919
|
+
if (criteria.to && criteria.to.includes('@')) {
|
|
920
|
+
filters.push(`toRecipients/any(r: r/emailAddress/address eq '${criteria.to}')`);
|
|
921
|
+
}
|
|
922
|
+
if (criteria.isUnread !== undefined) {
|
|
923
|
+
filters.push(`isRead eq ${!criteria.isUnread}`);
|
|
924
|
+
}
|
|
925
|
+
if (criteria.hasAttachment !== undefined) {
|
|
926
|
+
filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
|
|
927
|
+
}
|
|
928
|
+
if (criteria.importance) {
|
|
929
|
+
filters.push(`importance eq '${criteria.importance}'`);
|
|
930
|
+
}
|
|
931
|
+
if (filters.length > 0) {
|
|
932
|
+
apiCall = apiCall.filter(filters.join(' and '));
|
|
933
|
+
}
|
|
934
|
+
// Apply folder filter using specific folder API
|
|
935
|
+
if (criteria.folder) {
|
|
936
|
+
const folders = await this.findFolderByName(criteria.folder);
|
|
937
|
+
if (folders.length > 0) {
|
|
938
|
+
apiCall = graphClient
|
|
939
|
+
.api(`/me/mailFolders/${folders[0].id}/messages`)
|
|
940
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
941
|
+
.orderby('receivedDateTime desc')
|
|
942
|
+
.top(Math.min(maxResults, 999));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const result = await apiCall.get();
|
|
946
|
+
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
947
|
+
logger.log(`📧 Filtered Search returned ${messages.length} results`);
|
|
948
|
+
return messages;
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
logger.log(`❌ Filtered Search failed: ${error}`);
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Fallback to basic search (original implementation)
|
|
957
|
+
*/
|
|
958
|
+
async performBasicSearch(criteria) {
|
|
959
|
+
logger.log('🔄 Using fallback basic search');
|
|
960
|
+
const graphClient = await this.getGraphClient();
|
|
961
|
+
const result = await graphClient
|
|
962
|
+
.api('/me/messages')
|
|
963
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
964
|
+
.orderby('receivedDateTime desc')
|
|
965
|
+
.top(criteria.maxResults || 50)
|
|
966
|
+
.get();
|
|
967
|
+
let messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
968
|
+
messages = this.applyManualFiltering(messages, criteria);
|
|
969
|
+
return {
|
|
970
|
+
messages,
|
|
971
|
+
hasMore: !!result['@odata.nextLink']
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Map email result to EmailInfo format
|
|
976
|
+
*/
|
|
977
|
+
mapEmailResult(email) {
|
|
978
|
+
return {
|
|
979
|
+
id: email.id,
|
|
980
|
+
subject: email.subject || '',
|
|
981
|
+
from: {
|
|
982
|
+
name: email.from?.emailAddress?.name || '',
|
|
983
|
+
address: email.from?.emailAddress?.address || ''
|
|
984
|
+
},
|
|
985
|
+
toRecipients: email.toRecipients?.map((recipient) => ({
|
|
986
|
+
name: recipient.emailAddress?.name || '',
|
|
987
|
+
address: recipient.emailAddress?.address || ''
|
|
988
|
+
})) || [],
|
|
989
|
+
ccRecipients: email.ccRecipients?.map((recipient) => ({
|
|
990
|
+
name: recipient.emailAddress?.name || '',
|
|
991
|
+
address: recipient.emailAddress?.address || ''
|
|
992
|
+
})) || [],
|
|
993
|
+
receivedDateTime: email.receivedDateTime,
|
|
994
|
+
sentDateTime: email.sentDateTime,
|
|
995
|
+
bodyPreview: email.bodyPreview || '',
|
|
996
|
+
isRead: email.isRead || false,
|
|
997
|
+
hasAttachments: email.hasAttachments || false,
|
|
998
|
+
importance: email.importance || 'normal',
|
|
999
|
+
conversationId: email.conversationId || '',
|
|
1000
|
+
parentFolderId: email.parentFolderId || '',
|
|
1001
|
+
webLink: email.webLink || '',
|
|
1002
|
+
attachments: []
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Remove duplicate messages based on ID
|
|
1007
|
+
*/
|
|
1008
|
+
removeDuplicateMessages(messages) {
|
|
1009
|
+
const seen = new Set();
|
|
725
1010
|
return messages.filter(message => {
|
|
726
|
-
|
|
1011
|
+
if (seen.has(message.id)) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
seen.add(message.id);
|
|
1015
|
+
return true;
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Apply advanced filtering with better logic
|
|
1020
|
+
*/
|
|
1021
|
+
applyAdvancedFiltering(messages, criteria) {
|
|
1022
|
+
return messages.filter(message => {
|
|
1023
|
+
// Enhanced text search across multiple fields
|
|
727
1024
|
if (criteria.query) {
|
|
728
|
-
const searchText = criteria.query.toLowerCase();
|
|
729
|
-
const
|
|
730
|
-
|
|
1025
|
+
const searchText = criteria.query.toLowerCase().trim();
|
|
1026
|
+
const searchableContent = [
|
|
1027
|
+
message.subject,
|
|
1028
|
+
message.bodyPreview,
|
|
1029
|
+
message.from.name,
|
|
1030
|
+
message.from.address,
|
|
1031
|
+
...message.toRecipients.map(r => `${r.name} ${r.address}`),
|
|
1032
|
+
...message.ccRecipients.map(r => `${r.name} ${r.address}`)
|
|
1033
|
+
].join(' ').toLowerCase();
|
|
1034
|
+
// Support multiple search terms (AND logic)
|
|
1035
|
+
const searchTerms = searchText.split(/\s+/).filter(term => term.length > 0);
|
|
1036
|
+
if (!searchTerms.every(term => searchableContent.includes(term))) {
|
|
731
1037
|
return false;
|
|
1038
|
+
}
|
|
732
1039
|
}
|
|
1040
|
+
// Enhanced sender search
|
|
733
1041
|
if (criteria.from) {
|
|
734
1042
|
const searchTerm = criteria.from.toLowerCase().trim();
|
|
735
1043
|
const fromName = message.from.name.toLowerCase();
|
|
736
1044
|
const fromAddress = message.from.address.toLowerCase();
|
|
737
|
-
// Multiple matching strategies for better partial name support
|
|
738
1045
|
const matches = [
|
|
739
|
-
// Direct name or email match
|
|
740
1046
|
fromName.includes(searchTerm),
|
|
741
1047
|
fromAddress.includes(searchTerm),
|
|
742
|
-
// Split search
|
|
1048
|
+
// Split name search
|
|
743
1049
|
searchTerm.split(/\s+/).every(part => fromName.includes(part)),
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
// Handle initials (e.g., "M Kumar" for "Madan Kumar")
|
|
751
|
-
searchTerm.split(/\s+/).length === 2 &&
|
|
752
|
-
fromName.split(/\s+/).length >= 2 &&
|
|
753
|
-
fromName.split(/\s+/)[0].startsWith(searchTerm.split(/\s+/)[0][0]) &&
|
|
754
|
-
fromName.includes(searchTerm.split(/\s+/)[1])
|
|
1050
|
+
// Word boundary search
|
|
1051
|
+
new RegExp(`\\b${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i').test(fromName),
|
|
1052
|
+
// Email domain search
|
|
1053
|
+
searchTerm.includes('@') && fromAddress === searchTerm,
|
|
1054
|
+
// Partial email search
|
|
1055
|
+
!searchTerm.includes('@') && fromAddress.includes(searchTerm)
|
|
755
1056
|
];
|
|
756
1057
|
if (!matches.some(match => match))
|
|
757
1058
|
return false;
|
|
758
1059
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (!toMatch)
|
|
762
|
-
return false;
|
|
763
|
-
}
|
|
764
|
-
if (criteria.cc) {
|
|
765
|
-
// Handle case where ccRecipients might be undefined
|
|
766
|
-
const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
|
|
767
|
-
message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
|
|
768
|
-
if (!ccMatch)
|
|
769
|
-
return false;
|
|
770
|
-
}
|
|
771
|
-
if (criteria.subject) {
|
|
772
|
-
if (!message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
773
|
-
return false;
|
|
774
|
-
}
|
|
775
|
-
// Apply date filters
|
|
776
|
-
if (criteria.after) {
|
|
777
|
-
const afterDate = new Date(criteria.after);
|
|
778
|
-
const messageDate = new Date(message.receivedDateTime);
|
|
779
|
-
if (messageDate < afterDate)
|
|
780
|
-
return false;
|
|
781
|
-
}
|
|
782
|
-
if (criteria.before) {
|
|
783
|
-
const beforeDate = new Date(criteria.before);
|
|
1060
|
+
// Date range filters
|
|
1061
|
+
if (criteria.after || criteria.before) {
|
|
784
1062
|
const messageDate = new Date(message.receivedDateTime);
|
|
785
|
-
if (
|
|
786
|
-
|
|
1063
|
+
if (criteria.after) {
|
|
1064
|
+
const afterDate = new Date(criteria.after);
|
|
1065
|
+
if (messageDate < afterDate)
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
if (criteria.before) {
|
|
1069
|
+
const beforeDate = new Date(criteria.before);
|
|
1070
|
+
if (messageDate > beforeDate)
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
787
1073
|
}
|
|
788
|
-
//
|
|
789
|
-
if (criteria.
|
|
1074
|
+
// Other filters remain the same but are more robust
|
|
1075
|
+
if (criteria.to && !message.toRecipients.some(r => r.address.toLowerCase().includes(criteria.to.toLowerCase())))
|
|
790
1076
|
return false;
|
|
791
|
-
|
|
792
|
-
// Apply read status filter
|
|
793
|
-
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread) {
|
|
1077
|
+
if (criteria.cc && (!message.ccRecipients || !message.ccRecipients.some(r => r.address.toLowerCase().includes(criteria.cc.toLowerCase()))))
|
|
794
1078
|
return false;
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (criteria.
|
|
1079
|
+
if (criteria.subject && !message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
1080
|
+
return false;
|
|
1081
|
+
if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment)
|
|
1082
|
+
return false;
|
|
1083
|
+
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread)
|
|
1084
|
+
return false;
|
|
1085
|
+
if (criteria.importance && message.importance !== criteria.importance)
|
|
798
1086
|
return false;
|
|
799
|
-
}
|
|
800
1087
|
return true;
|
|
801
1088
|
});
|
|
802
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Sort search results by relevance and date
|
|
1092
|
+
*/
|
|
1093
|
+
sortSearchResults(messages, criteria) {
|
|
1094
|
+
return messages.sort((a, b) => {
|
|
1095
|
+
// Calculate relevance score
|
|
1096
|
+
const scoreA = this.calculateRelevanceScore(a, criteria);
|
|
1097
|
+
const scoreB = this.calculateRelevanceScore(b, criteria);
|
|
1098
|
+
if (scoreA !== scoreB) {
|
|
1099
|
+
return scoreB - scoreA; // Higher score first
|
|
1100
|
+
}
|
|
1101
|
+
// If relevance is same, sort by date (newer first)
|
|
1102
|
+
return new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime();
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Calculate relevance score for search results
|
|
1107
|
+
*/
|
|
1108
|
+
calculateRelevanceScore(message, criteria) {
|
|
1109
|
+
let score = 0;
|
|
1110
|
+
if (criteria.query) {
|
|
1111
|
+
const query = criteria.query.toLowerCase();
|
|
1112
|
+
// Subject matches get higher score
|
|
1113
|
+
if (message.subject.toLowerCase().includes(query))
|
|
1114
|
+
score += 10;
|
|
1115
|
+
// Sender name matches
|
|
1116
|
+
if (message.from.name.toLowerCase().includes(query))
|
|
1117
|
+
score += 5;
|
|
1118
|
+
// Body preview matches
|
|
1119
|
+
if (message.bodyPreview.toLowerCase().includes(query))
|
|
1120
|
+
score += 3;
|
|
1121
|
+
// Exact word matches get bonus
|
|
1122
|
+
const words = query.split(/\s+/);
|
|
1123
|
+
words.forEach(word => {
|
|
1124
|
+
if (message.subject.toLowerCase().includes(word))
|
|
1125
|
+
score += 2;
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
// Recent emails get slight boost
|
|
1129
|
+
const daysOld = (Date.now() - new Date(message.receivedDateTime).getTime()) / (1000 * 60 * 60 * 24);
|
|
1130
|
+
if (daysOld < 7)
|
|
1131
|
+
score += 2;
|
|
1132
|
+
else if (daysOld < 30)
|
|
1133
|
+
score += 1;
|
|
1134
|
+
// Unread emails get boost
|
|
1135
|
+
if (!message.isRead)
|
|
1136
|
+
score += 1;
|
|
1137
|
+
// Important emails get boost
|
|
1138
|
+
if (message.importance === 'high')
|
|
1139
|
+
score += 3;
|
|
1140
|
+
return score;
|
|
1141
|
+
}
|
|
803
1142
|
/**
|
|
804
1143
|
* List emails in a folder
|
|
805
1144
|
*/
|
|
@@ -957,10 +1296,13 @@ export class MS365Operations {
|
|
|
957
1296
|
const graphClient = await this.getGraphClient();
|
|
958
1297
|
const allFolders = [];
|
|
959
1298
|
// Get top-level folders
|
|
1299
|
+
logger.log('📁 Fetching top-level mail folders...');
|
|
960
1300
|
const result = await graphClient
|
|
961
1301
|
.api('/me/mailFolders')
|
|
962
1302
|
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1303
|
+
.top(999) // Request up to 999 folders to avoid pagination
|
|
963
1304
|
.get();
|
|
1305
|
+
logger.log(`📁 Found ${result.value?.length || 0} top-level folders from API`);
|
|
964
1306
|
// Process top-level folders
|
|
965
1307
|
const topLevelFolders = result.value?.map((folder) => ({
|
|
966
1308
|
id: folder.id,
|
|
@@ -971,12 +1313,17 @@ export class MS365Operations {
|
|
|
971
1313
|
depth: 0,
|
|
972
1314
|
fullPath: folder.displayName || ''
|
|
973
1315
|
})) || [];
|
|
1316
|
+
logger.log(`📁 Processed ${topLevelFolders.length} top-level folders: ${topLevelFolders.map((f) => f.displayName).join(', ')}`);
|
|
974
1317
|
allFolders.push(...topLevelFolders);
|
|
975
1318
|
// Recursively get child folders for each top-level folder
|
|
1319
|
+
logger.log('📂 Starting recursive child folder discovery...');
|
|
976
1320
|
for (const folder of topLevelFolders) {
|
|
1321
|
+
logger.log(`📂 Checking children of: ${folder.displayName} (${folder.id})`);
|
|
977
1322
|
const childFolders = await this.getChildFolders(folder.id, folder.fullPath || folder.displayName, 1);
|
|
1323
|
+
logger.log(`📂 Found ${childFolders.length} child folders for ${folder.displayName}`);
|
|
978
1324
|
allFolders.push(...childFolders);
|
|
979
1325
|
}
|
|
1326
|
+
logger.log(`📁 Total folders discovered: ${allFolders.length}`);
|
|
980
1327
|
// Sort folders by full path for better organization
|
|
981
1328
|
allFolders.sort((a, b) => (a.fullPath || '').localeCompare(b.fullPath || ''));
|
|
982
1329
|
return allFolders;
|
|
@@ -993,11 +1340,14 @@ export class MS365Operations {
|
|
|
993
1340
|
try {
|
|
994
1341
|
const graphClient = await this.getGraphClient();
|
|
995
1342
|
const childFolders = [];
|
|
1343
|
+
logger.log(`📂 [Depth ${depth}] Getting child folders for: ${parentPath} (${parentFolderId})`);
|
|
996
1344
|
// Get child folders of the specified parent
|
|
997
1345
|
const result = await graphClient
|
|
998
1346
|
.api(`/me/mailFolders/${parentFolderId}/childFolders`)
|
|
999
1347
|
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1348
|
+
.top(999) // Request up to 999 child folders to avoid pagination
|
|
1000
1349
|
.get();
|
|
1350
|
+
logger.log(`📂 [Depth ${depth}] API returned ${result.value?.length || 0} child folders for ${parentPath}`);
|
|
1001
1351
|
if (result.value && result.value.length > 0) {
|
|
1002
1352
|
const folders = result.value.map((folder) => ({
|
|
1003
1353
|
id: folder.id,
|
|
@@ -1008,20 +1358,26 @@ export class MS365Operations {
|
|
|
1008
1358
|
depth,
|
|
1009
1359
|
fullPath: `${parentPath}/${folder.displayName || ''}`
|
|
1010
1360
|
}));
|
|
1361
|
+
logger.log(`📂 [Depth ${depth}] Mapped child folders: ${folders.map((f) => f.displayName).join(', ')}`);
|
|
1011
1362
|
childFolders.push(...folders);
|
|
1012
1363
|
// Recursively get child folders (limit depth to prevent infinite recursion)
|
|
1013
1364
|
if (depth < 10) { // Max depth of 10 levels
|
|
1365
|
+
logger.log(`📂 [Depth ${depth}] Recursing into ${folders.length} child folders...`);
|
|
1014
1366
|
for (const folder of folders) {
|
|
1015
1367
|
const subChildFolders = await this.getChildFolders(folder.id, folder.fullPath || folder.displayName, depth + 1);
|
|
1016
1368
|
childFolders.push(...subChildFolders);
|
|
1017
1369
|
}
|
|
1018
1370
|
}
|
|
1371
|
+
else {
|
|
1372
|
+
logger.log(`📂 [Depth ${depth}] Maximum depth reached, stopping recursion`);
|
|
1373
|
+
}
|
|
1019
1374
|
}
|
|
1375
|
+
logger.log(`📂 [Depth ${depth}] Returning ${childFolders.length} total child folders for ${parentPath}`);
|
|
1020
1376
|
return childFolders;
|
|
1021
1377
|
}
|
|
1022
1378
|
catch (error) {
|
|
1023
1379
|
// Log the error but don't throw - some folders might not have children or access might be restricted
|
|
1024
|
-
logger.log(
|
|
1380
|
+
logger.log(`❌ [Depth ${depth}] Could not access child folders for ${parentFolderId} (${parentPath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
1025
1381
|
return [];
|
|
1026
1382
|
}
|
|
1027
1383
|
}
|
|
@@ -1034,6 +1390,7 @@ export class MS365Operations {
|
|
|
1034
1390
|
const result = await graphClient
|
|
1035
1391
|
.api(`/me/mailFolders/${parentFolderId}/childFolders`)
|
|
1036
1392
|
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1393
|
+
.top(999) // Request up to 999 child folders to avoid pagination
|
|
1037
1394
|
.get();
|
|
1038
1395
|
return result.value?.map((folder) => ({
|
|
1039
1396
|
id: folder.id,
|
|
@@ -1337,5 +1694,286 @@ export class MS365Operations {
|
|
|
1337
1694
|
throw error;
|
|
1338
1695
|
}
|
|
1339
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Debug method to check raw folder API response
|
|
1699
|
+
*/
|
|
1700
|
+
async debugFolders() {
|
|
1701
|
+
try {
|
|
1702
|
+
const graphClient = await this.getGraphClient();
|
|
1703
|
+
logger.log('🔍 DEBUG: Testing folder API calls...');
|
|
1704
|
+
// Test basic folder listing
|
|
1705
|
+
const basicResult = await graphClient
|
|
1706
|
+
.api('/me/mailFolders')
|
|
1707
|
+
.get();
|
|
1708
|
+
logger.log(`🔍 DEBUG: Raw /me/mailFolders response: ${JSON.stringify(basicResult, null, 2)}`);
|
|
1709
|
+
// Test with specific selection
|
|
1710
|
+
const selectResult = await graphClient
|
|
1711
|
+
.api('/me/mailFolders')
|
|
1712
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1713
|
+
.get();
|
|
1714
|
+
logger.log(`🔍 DEBUG: Selected fields response: ${JSON.stringify(selectResult, null, 2)}`);
|
|
1715
|
+
// Test well-known folder access
|
|
1716
|
+
try {
|
|
1717
|
+
const inboxResult = await graphClient
|
|
1718
|
+
.api('/me/mailFolders/inbox/childFolders')
|
|
1719
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1720
|
+
.get();
|
|
1721
|
+
logger.log(`🔍 DEBUG: Inbox children: ${JSON.stringify(inboxResult, null, 2)}`);
|
|
1722
|
+
}
|
|
1723
|
+
catch (inboxError) {
|
|
1724
|
+
logger.log(`🔍 DEBUG: Inbox children error: ${inboxError}`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
catch (error) {
|
|
1728
|
+
logger.error('🔍 DEBUG: Error in debugFolders:', error);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Apply manual filtering to search results (used when $filter can't be used with $search)
|
|
1733
|
+
*/
|
|
1734
|
+
applyManualFiltering(messages, criteria) {
|
|
1735
|
+
return messages.filter(message => {
|
|
1736
|
+
// Apply text search filters manually
|
|
1737
|
+
if (criteria.query) {
|
|
1738
|
+
const searchText = criteria.query.toLowerCase();
|
|
1739
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
1740
|
+
if (!messageText.includes(searchText))
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
if (criteria.from) {
|
|
1744
|
+
const searchTerm = criteria.from.toLowerCase().trim();
|
|
1745
|
+
const fromName = message.from.name.toLowerCase();
|
|
1746
|
+
const fromAddress = message.from.address.toLowerCase();
|
|
1747
|
+
const matches = [
|
|
1748
|
+
fromName.includes(searchTerm),
|
|
1749
|
+
fromAddress.includes(searchTerm),
|
|
1750
|
+
searchTerm.split(/\s+/).every(part => fromName.includes(part)),
|
|
1751
|
+
fromName.split(/\s+/).some(namePart => namePart.startsWith(searchTerm))
|
|
1752
|
+
];
|
|
1753
|
+
if (!matches.some(match => match))
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
if (criteria.to) {
|
|
1757
|
+
const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
|
|
1758
|
+
if (!toMatch)
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
if (criteria.cc) {
|
|
1762
|
+
const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
|
|
1763
|
+
message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
|
|
1764
|
+
if (!ccMatch)
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
if (criteria.subject) {
|
|
1768
|
+
if (!message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
if (criteria.after) {
|
|
1772
|
+
const afterDate = new Date(criteria.after);
|
|
1773
|
+
const messageDate = new Date(message.receivedDateTime);
|
|
1774
|
+
if (messageDate < afterDate)
|
|
1775
|
+
return false;
|
|
1776
|
+
}
|
|
1777
|
+
if (criteria.before) {
|
|
1778
|
+
const beforeDate = new Date(criteria.before);
|
|
1779
|
+
const messageDate = new Date(message.receivedDateTime);
|
|
1780
|
+
if (messageDate > beforeDate)
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment) {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread) {
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
if (criteria.importance && message.importance !== criteria.importance) {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
return true;
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Build relaxed KQL query with fewer constraints
|
|
1797
|
+
*/
|
|
1798
|
+
buildRelaxedKQLQuery(criteria) {
|
|
1799
|
+
const kqlParts = [];
|
|
1800
|
+
// Only include the most important criteria for relaxed search
|
|
1801
|
+
if (criteria.from) {
|
|
1802
|
+
const fromTerm = criteria.from.trim();
|
|
1803
|
+
if (fromTerm.includes('@')) {
|
|
1804
|
+
// For email addresses, try domain search too
|
|
1805
|
+
const domain = fromTerm.split('@')[1];
|
|
1806
|
+
if (domain) {
|
|
1807
|
+
kqlParts.push(`(from:${fromTerm} OR from:*@${domain})`);
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
kqlParts.push(`from:${fromTerm}`);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
// For names, use wildcard search
|
|
1815
|
+
kqlParts.push(`from:*${fromTerm}*`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (criteria.subject) {
|
|
1819
|
+
// Use partial subject matching
|
|
1820
|
+
kqlParts.push(`subject:*${criteria.subject}*`);
|
|
1821
|
+
}
|
|
1822
|
+
// Skip attachment and read status filters in relaxed mode
|
|
1823
|
+
// Keep only critical filters
|
|
1824
|
+
if (criteria.importance === 'high') {
|
|
1825
|
+
kqlParts.push(`importance:high`);
|
|
1826
|
+
}
|
|
1827
|
+
// Expand date range for relaxed search
|
|
1828
|
+
if (criteria.after) {
|
|
1829
|
+
const afterDate = new Date(criteria.after);
|
|
1830
|
+
afterDate.setDate(afterDate.getDate() - 7); // Expand by a week
|
|
1831
|
+
const expandedDate = afterDate.toISOString().split('T')[0];
|
|
1832
|
+
kqlParts.push(`received>=${expandedDate}`);
|
|
1833
|
+
}
|
|
1834
|
+
return kqlParts.join(' AND ');
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Perform partial text search across recent emails with broader matching
|
|
1838
|
+
*/
|
|
1839
|
+
async performPartialTextSearch(criteria, maxResults) {
|
|
1840
|
+
try {
|
|
1841
|
+
const graphClient = await this.getGraphClient();
|
|
1842
|
+
// Get a large set of recent emails to search through
|
|
1843
|
+
const result = await graphClient
|
|
1844
|
+
.api('/me/messages')
|
|
1845
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1846
|
+
.orderby('receivedDateTime desc')
|
|
1847
|
+
.top(Math.min(maxResults, 1000))
|
|
1848
|
+
.get();
|
|
1849
|
+
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
1850
|
+
// Apply very broad partial matching
|
|
1851
|
+
const partialMatches = messages.filter(message => {
|
|
1852
|
+
let matches = true;
|
|
1853
|
+
// Very flexible text search
|
|
1854
|
+
if (criteria.query) {
|
|
1855
|
+
const searchTerms = criteria.query.toLowerCase().split(/\s+/);
|
|
1856
|
+
const searchableText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
1857
|
+
// At least 50% of search terms should match
|
|
1858
|
+
const matchingTerms = searchTerms.filter(term => searchableText.includes(term));
|
|
1859
|
+
matches = matchingTerms.length >= Math.ceil(searchTerms.length * 0.5);
|
|
1860
|
+
}
|
|
1861
|
+
// Very flexible sender search
|
|
1862
|
+
if (criteria.from && matches) {
|
|
1863
|
+
const fromTerm = criteria.from.toLowerCase();
|
|
1864
|
+
const fromText = `${message.from.name} ${message.from.address}`.toLowerCase();
|
|
1865
|
+
// Partial matching with character-level similarity
|
|
1866
|
+
matches = fromText.includes(fromTerm) ||
|
|
1867
|
+
fromTerm.split('').some(char => fromText.includes(char)) ||
|
|
1868
|
+
this.calculateStringSimilarity(fromTerm, fromText) > 0.3;
|
|
1869
|
+
}
|
|
1870
|
+
return matches;
|
|
1871
|
+
});
|
|
1872
|
+
logger.log(`📧 Partial text search found ${partialMatches.length} potential matches`);
|
|
1873
|
+
return partialMatches;
|
|
1874
|
+
}
|
|
1875
|
+
catch (error) {
|
|
1876
|
+
logger.log(`❌ Partial text search failed: ${error}`);
|
|
1877
|
+
return [];
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Ultra-relaxed search that casts a very wide net
|
|
1882
|
+
*/
|
|
1883
|
+
async performUltraRelaxedSearch(criteria, maxResults) {
|
|
1884
|
+
try {
|
|
1885
|
+
const graphClient = await this.getGraphClient();
|
|
1886
|
+
// Search across multiple folders and time ranges
|
|
1887
|
+
const searches = [];
|
|
1888
|
+
// Search recent emails
|
|
1889
|
+
searches.push(graphClient
|
|
1890
|
+
.api('/me/messages')
|
|
1891
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1892
|
+
.orderby('receivedDateTime desc')
|
|
1893
|
+
.top(500)
|
|
1894
|
+
.get());
|
|
1895
|
+
// Search sent items if looking for specific people
|
|
1896
|
+
if (criteria.from || criteria.to) {
|
|
1897
|
+
searches.push(graphClient
|
|
1898
|
+
.api('/me/mailFolders/sentitems/messages')
|
|
1899
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1900
|
+
.orderby('receivedDateTime desc')
|
|
1901
|
+
.top(200)
|
|
1902
|
+
.get());
|
|
1903
|
+
}
|
|
1904
|
+
const results = await Promise.allSettled(searches);
|
|
1905
|
+
const allEmails = [];
|
|
1906
|
+
results.forEach((result, index) => {
|
|
1907
|
+
if (result.status === 'fulfilled') {
|
|
1908
|
+
const emails = result.value.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
1909
|
+
allEmails.push(...emails);
|
|
1910
|
+
logger.log(`📧 Ultra-relaxed search ${index + 1} found ${emails.length} emails`);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
return this.removeDuplicateMessages(allEmails);
|
|
1914
|
+
}
|
|
1915
|
+
catch (error) {
|
|
1916
|
+
logger.log(`❌ Ultra-relaxed search failed: ${error}`);
|
|
1917
|
+
return [];
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Ultra-relaxed filtering with very permissive matching
|
|
1922
|
+
*/
|
|
1923
|
+
applyUltraRelaxedFiltering(messages, criteria) {
|
|
1924
|
+
return messages.filter(message => {
|
|
1925
|
+
let score = 0;
|
|
1926
|
+
let hasAnyMatch = false;
|
|
1927
|
+
// Any partial query match
|
|
1928
|
+
if (criteria.query) {
|
|
1929
|
+
const searchText = criteria.query.toLowerCase();
|
|
1930
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name}`.toLowerCase();
|
|
1931
|
+
// Check for any word matches
|
|
1932
|
+
const queryWords = searchText.split(/\s+/);
|
|
1933
|
+
const matchingWords = queryWords.filter(word => messageText.includes(word));
|
|
1934
|
+
if (matchingWords.length > 0) {
|
|
1935
|
+
hasAnyMatch = true;
|
|
1936
|
+
score += matchingWords.length;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
// Any sender similarity
|
|
1940
|
+
if (criteria.from) {
|
|
1941
|
+
const fromTerm = criteria.from.toLowerCase();
|
|
1942
|
+
const fromText = `${message.from.name} ${message.from.address}`.toLowerCase();
|
|
1943
|
+
if (fromText.includes(fromTerm) ||
|
|
1944
|
+
this.calculateStringSimilarity(fromTerm, fromText) > 0.2) {
|
|
1945
|
+
hasAnyMatch = true;
|
|
1946
|
+
score += 2;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
// Any subject similarity
|
|
1950
|
+
if (criteria.subject) {
|
|
1951
|
+
const subjectTerm = criteria.subject.toLowerCase();
|
|
1952
|
+
if (message.subject.toLowerCase().includes(subjectTerm)) {
|
|
1953
|
+
hasAnyMatch = true;
|
|
1954
|
+
score += 3;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
// If no specific criteria, return recent emails
|
|
1958
|
+
if (!criteria.query && !criteria.from && !criteria.subject) {
|
|
1959
|
+
hasAnyMatch = true;
|
|
1960
|
+
}
|
|
1961
|
+
return hasAnyMatch;
|
|
1962
|
+
}).sort((a, b) => {
|
|
1963
|
+
// Sort by date for ultra-relaxed results
|
|
1964
|
+
return new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime();
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Calculate string similarity (simple version)
|
|
1969
|
+
*/
|
|
1970
|
+
calculateStringSimilarity(str1, str2) {
|
|
1971
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
1972
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
1973
|
+
if (longer.length === 0)
|
|
1974
|
+
return 1.0;
|
|
1975
|
+
const matches = shorter.split('').filter(char => longer.includes(char)).length;
|
|
1976
|
+
return matches / longer.length;
|
|
1977
|
+
}
|
|
1340
1978
|
}
|
|
1341
1979
|
export const ms365Operations = new MS365Operations();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ms365-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"description": "Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|