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 CHANGED
@@ -67,7 +67,7 @@ function parseArgs() {
67
67
  }
68
68
  const server = new Server({
69
69
  name: "ms365-mcp-server",
70
- version: "1.1.10"
70
+ version: "1.1.11"
71
71
  }, {
72
72
  capabilities: {
73
73
  resources: {
@@ -643,7 +643,7 @@ export class MS365Operations {
643
643
  }
644
644
  }
645
645
  /**
646
- * Search emails with criteria
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
- // For complex searches, use a simpler approach
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
- // Start with a basic query to get recent emails
660
- const apiCall = graphClient.api('/me/messages')
661
- .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
662
- .orderby('receivedDateTime desc')
663
- .top(criteria.maxResults || 100);
664
- const result = await apiCall.get();
665
- let messages = result.value?.map((email) => ({
666
- id: email.id,
667
- subject: email.subject || '',
668
- from: {
669
- name: email.from?.emailAddress?.name || '',
670
- address: email.from?.emailAddress?.address || ''
671
- },
672
- toRecipients: email.toRecipients?.map((recipient) => ({
673
- name: recipient.emailAddress?.name || '',
674
- address: recipient.emailAddress?.address || ''
675
- })) || [],
676
- ccRecipients: email.ccRecipients?.map((recipient) => ({
677
- name: recipient.emailAddress?.name || '',
678
- address: recipient.emailAddress?.address || ''
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
- catch (error) {
703
- logger.error(`Error getting attachment count for message ${message.id}:`, error);
704
- message.attachments = [];
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: !!result['@odata.nextLink']
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
- throw error;
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
- * Apply manual filtering to search results (used when $filter can't be used with $search)
777
+ * Use Microsoft Graph Search API for full-text search
723
778
  */
724
- applyManualFiltering(messages, criteria) {
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
- // Apply text search filters manually
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 messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
730
- if (!messageText.includes(searchText))
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 term and check if all parts exist in name
1048
+ // Split name search
743
1049
  searchTerm.split(/\s+/).every(part => fromName.includes(part)),
744
- // Check if any word in the name starts with the search term
745
- fromName.split(/\s+/).some(namePart => namePart.startsWith(searchTerm)),
746
- // Check if search term matches any word in the name exactly
747
- fromName.split(/\s+/).some(namePart => namePart === searchTerm),
748
- // Handle "Last, First" format
749
- fromName.replace(/,\s*/g, ' ').includes(searchTerm),
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
- if (criteria.to) {
760
- const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
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 (messageDate > beforeDate)
786
- return false;
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
- // Apply attachment filter
789
- if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment) {
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
- // Apply importance filter
797
- if (criteria.importance && message.importance !== criteria.importance) {
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(`Could not access child folders for ${parentFolderId}: ${error instanceof Error ? error.message : String(error)}`);
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.10",
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",