ms365-mcp-server 1.1.14 → 1.1.15

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
@@ -10,7 +10,7 @@ import path from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import fs from 'fs/promises';
12
12
  import crypto from 'crypto';
13
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
14
14
  import { logger } from './utils/api.js';
15
15
  import { MS365Operations } from './utils/ms365-operations.js';
16
16
  import { multiUserMS365Auth } from './utils/multi-user-auth.js';
@@ -67,18 +67,23 @@ function parseArgs() {
67
67
  }
68
68
  const server = new Server({
69
69
  name: "ms365-mcp-server",
70
- version: "1.1.14"
70
+ version: "1.1.15"
71
71
  }, {
72
72
  capabilities: {
73
73
  resources: {
74
74
  read: true,
75
- list: true
75
+ list: true,
76
+ templates: true
76
77
  },
77
78
  tools: {
78
79
  list: true,
79
80
  call: true
80
81
  },
81
82
  prompts: {
83
+ list: true,
84
+ get: true
85
+ },
86
+ resourceTemplates: {
82
87
  list: true
83
88
  }
84
89
  }
@@ -103,6 +108,20 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
103
108
  logger.log('Received list prompts request');
104
109
  return { prompts: [] };
105
110
  });
111
+ /**
112
+ * Handler for getting a specific prompt.
113
+ */
114
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
115
+ logger.log('Received get prompt request: ' + JSON.stringify(request));
116
+ throw new Error("Prompt getting not implemented");
117
+ });
118
+ /**
119
+ * Handler for listing available resource templates.
120
+ */
121
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
122
+ logger.log('Received list resource templates request');
123
+ return { resourceTemplates: [] };
124
+ });
106
125
  /**
107
126
  * List available tools for interacting with Microsoft 365.
108
127
  */
@@ -201,8 +220,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
201
220
  },
202
221
  action: {
203
222
  type: "string",
204
- enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me", "draft", "update_draft", "send_draft", "list_drafts", "reply_draft", "forward_draft"],
205
- description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create standalone draft - NOTE: Use reply_draft/forward_draft for threaded drafts), update_draft (modify existing draft), send_draft (send saved draft), list_drafts (list draft emails), reply_draft (create threaded reply draft that appears in conversation), forward_draft (create threaded forward draft that appears in conversation)"
223
+ enum: ["read", "search", "search_batched", "list", "mark", "move", "delete", "search_to_me", "draft", "update_draft", "send_draft", "list_drafts", "reply_draft", "forward_draft"],
224
+ description: "Action to perform: read (get email by ID), search (find emails), search_batched (find emails in batches of 50, up to 5 batches for better performance), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create standalone draft - NOTE: Use reply_draft/forward_draft for threaded drafts), update_draft (modify existing draft), send_draft (send saved draft), list_drafts (list draft emails), reply_draft (create threaded reply draft that appears in conversation), forward_draft (create threaded forward draft that appears in conversation)"
206
225
  },
207
226
  messageId: {
208
227
  type: "string",
@@ -273,11 +292,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
273
292
  },
274
293
  maxResults: {
275
294
  type: "number",
276
- description: "Maximum number of results to return (default: 50, max: 200)",
277
- minimum: 1,
278
- maximum: 200,
295
+ description: "Maximum number of results to return. Set to 0 or omit to get ALL matching results (safety limited to 1000). For large result sets, use specific maxResults values to avoid timeouts. Default: 50",
296
+ minimum: 0,
279
297
  default: 50
280
298
  },
299
+ batchSize: {
300
+ type: "number",
301
+ description: "Number of results per batch for search_batched action (default: 50)",
302
+ minimum: 10,
303
+ maximum: 100,
304
+ default: 50
305
+ },
306
+ maxBatches: {
307
+ type: "number",
308
+ description: "Maximum number of batches for search_batched action (default: 5)",
309
+ minimum: 1,
310
+ maximum: 10,
311
+ default: 5
312
+ },
281
313
  // Draft email parameters
282
314
  draftTo: {
283
315
  oneOf: [
@@ -726,6 +758,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
726
758
  }
727
759
  ]
728
760
  };
761
+ case "search_batched":
762
+ const batchSize = args?.batchSize || 50;
763
+ const maxBatches = args?.maxBatches || 5;
764
+ logger.log(`DEBUG: Starting batched search with batchSize: ${batchSize}, maxBatches: ${maxBatches}`);
765
+ const batchedSearchResults = await ms365Ops.searchEmailsBatched(args, batchSize, maxBatches);
766
+ logger.log(`DEBUG: Batched search results count: ${batchedSearchResults.messages.length} from ${batchedSearchResults.totalBatches} batches`);
767
+ // Enhanced feedback for batched search results
768
+ let batchedResponseText = `šŸ” Batched Email Search Results (${batchedSearchResults.messages.length} found from ${batchedSearchResults.totalBatches} batches)`;
769
+ if (batchedSearchResults.messages.length === 0) {
770
+ batchedResponseText = `šŸ” No emails found matching your criteria.\n\nšŸ’” Search Tips:\n`;
771
+ if (args?.from) {
772
+ batchedResponseText += `• Try partial names: "${args.from.split(' ')[0]}" or "${args.from.split(' ').pop()}"\n`;
773
+ batchedResponseText += `• Check spelling of sender name\n`;
774
+ }
775
+ if (args?.subject) {
776
+ batchedResponseText += `• Try broader subject terms\n`;
777
+ }
778
+ if (args?.after || args?.before) {
779
+ batchedResponseText += `• Try expanding date range\n`;
780
+ }
781
+ batchedResponseText += `• Remove some search criteria to get broader results`;
782
+ }
783
+ else {
784
+ batchedResponseText += `\n\n${batchedSearchResults.messages.map((email, index) => {
785
+ // Handle missing IDs more gracefully
786
+ const emailId = email.id || 'ID_MISSING';
787
+ const fromDisplay = email.from?.name ? `${email.from.name} <${email.from.address}>` : email.from?.address || 'Unknown sender';
788
+ return `${index + 1}. šŸ“§ ${email.subject || 'No subject'}\n šŸ‘¤ From: ${fromDisplay}\n šŸ“… ${email.receivedDateTime ? new Date(email.receivedDateTime).toLocaleDateString() : 'Unknown date'}\n ${email.isRead ? 'šŸ“– Read' : 'šŸ“© Unread'}\n šŸ†” ID: ${emailId}\n`;
789
+ }).join('\n')}`;
790
+ if (batchedSearchResults.hasMore) {
791
+ batchedResponseText += `\nšŸ’” There are more results available. Increase maxBatches parameter to get more emails.`;
792
+ }
793
+ batchedResponseText += `\n\nšŸ“Š Search Summary:\n`;
794
+ batchedResponseText += `• Total results: ${batchedSearchResults.messages.length}\n`;
795
+ batchedResponseText += `• Batches processed: ${batchedSearchResults.totalBatches}/${maxBatches}\n`;
796
+ batchedResponseText += `• Batch size: ${batchSize}\n`;
797
+ batchedResponseText += `• More results available: ${batchedSearchResults.hasMore ? 'Yes' : 'No'}`;
798
+ }
799
+ return {
800
+ content: [
801
+ {
802
+ type: "text",
803
+ text: batchedResponseText
804
+ }
805
+ ]
806
+ };
729
807
  case "search_to_me":
730
808
  const searchToMeResults = await ms365Ops.searchEmailsToMe(args);
731
809
  // Process attachments for each email
@@ -92,26 +92,70 @@ export class MS365Operations {
92
92
  this.searchCache.set(cacheKey, { results, timestamp: Date.now() });
93
93
  logger.log(`Cached results for search: ${cacheKey}`);
94
94
  }
95
+ /**
96
+ * Utility method to properly escape OData filter values
97
+ */
98
+ escapeODataValue(value) {
99
+ // Escape single quotes by doubling them
100
+ return value.replace(/'/g, "''");
101
+ }
102
+ /**
103
+ * Utility method to validate and format date for OData filters
104
+ */
105
+ formatDateForOData(dateString) {
106
+ try {
107
+ const date = new Date(dateString);
108
+ if (isNaN(date.getTime())) {
109
+ throw new Error(`Invalid date: ${dateString}`);
110
+ }
111
+ return date.toISOString();
112
+ }
113
+ catch (error) {
114
+ logger.error(`Error formatting date ${dateString}:`, error);
115
+ throw new Error(`Invalid date format: ${dateString}. Use YYYY-MM-DD format.`);
116
+ }
117
+ }
95
118
  /**
96
119
  * Build filter query for Microsoft Graph API
97
120
  */
98
121
  buildFilterQuery(criteria) {
99
122
  const filters = [];
100
123
  if (criteria.from) {
101
- filters.push(`from/emailAddress/address eq '${criteria.from}'`);
124
+ // Properly escape single quotes in email addresses and names
125
+ const escapedFrom = this.escapeODataValue(criteria.from);
126
+ if (criteria.from.includes('@')) {
127
+ // For email addresses, use exact match
128
+ filters.push(`from/emailAddress/address eq '${escapedFrom}'`);
129
+ }
130
+ else {
131
+ // For names, use contains for partial matching
132
+ filters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
133
+ }
134
+ }
135
+ if (criteria.to && criteria.to.includes('@')) {
136
+ // Properly escape single quotes in email addresses
137
+ const escapedTo = this.escapeODataValue(criteria.to);
138
+ filters.push(`toRecipients/any(r: r/emailAddress/address eq '${escapedTo}')`);
139
+ }
140
+ if (criteria.cc && criteria.cc.includes('@')) {
141
+ // Properly escape single quotes in email addresses
142
+ const escapedCc = this.escapeODataValue(criteria.cc);
143
+ filters.push(`ccRecipients/any(r: r/emailAddress/address eq '${escapedCc}')`);
102
144
  }
103
- // Note: Cannot filter on toRecipients or ccRecipients as they are complex collections
104
- // These will be handled by search API or manual filtering
105
145
  if (criteria.subject) {
106
- filters.push(`contains(subject,'${criteria.subject}')`);
146
+ // Properly escape single quotes in subject
147
+ const escapedSubject = this.escapeODataValue(criteria.subject);
148
+ filters.push(`contains(subject,'${escapedSubject}')`);
107
149
  }
108
150
  if (criteria.after) {
109
- const afterDate = new Date(criteria.after).toISOString();
110
- filters.push(`receivedDateTime ge ${afterDate}`);
151
+ // Fix date formatting - use proper ISO format with quotes
152
+ const afterDate = this.formatDateForOData(criteria.after);
153
+ filters.push(`receivedDateTime ge '${afterDate}'`);
111
154
  }
112
155
  if (criteria.before) {
113
- const beforeDate = new Date(criteria.before).toISOString();
114
- filters.push(`receivedDateTime le ${beforeDate}`);
156
+ // Fix date formatting - use proper ISO format with quotes
157
+ const beforeDate = this.formatDateForOData(criteria.before);
158
+ filters.push(`receivedDateTime le '${beforeDate}'`);
115
159
  }
116
160
  if (criteria.hasAttachment !== undefined) {
117
161
  filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
@@ -944,7 +988,7 @@ ${originalBodyContent}
944
988
  }
945
989
  }
946
990
  /**
947
- * Advanced email search using Microsoft Graph Search API with KQL - Persistent search until results found
991
+ * Advanced email search using Microsoft Graph Search API with KQL - Returns ALL matching results
948
992
  */
949
993
  async searchEmails(criteria = {}) {
950
994
  return await this.executeWithAuth(async () => {
@@ -957,52 +1001,60 @@ ${originalBodyContent}
957
1001
  logger.log('šŸ“¦ Returning cached results');
958
1002
  return cachedResults;
959
1003
  }
960
- const maxResults = criteria.maxResults || 50;
1004
+ // If maxResults is specified, use it; otherwise get ALL results (with safety limit)
1005
+ const maxResults = criteria.maxResults || 0; // 0 means no limit
1006
+ const safetyLimit = 1000; // Maximum results to return even when maxResults is 0
1007
+ const effectiveMaxResults = maxResults > 0 ? maxResults : safetyLimit;
961
1008
  let allMessages = [];
962
1009
  let searchAttempts = 0;
963
1010
  const maxAttempts = 6; // Try multiple strategies
1011
+ // Add timeout mechanism (5 minutes max)
1012
+ const searchTimeout = 5 * 60 * 1000; // 5 minutes
1013
+ const startTime = Date.now();
964
1014
  logger.log(`šŸ” Starting persistent search with criteria: ${JSON.stringify(criteria)}`);
1015
+ logger.log(`šŸ” Max results requested: ${maxResults === 0 ? 'ALL (limited to ' + safetyLimit + ' for safety)' : maxResults}`);
1016
+ logger.log(`šŸ” Search timeout: ${searchTimeout / 1000} seconds`);
965
1017
  try {
966
- // Strategy 1: Use reliable OData filter search first (has proper IDs)
967
- if (searchAttempts < maxAttempts) {
1018
+ // Strategy 1: Use reliable OData filter search first (has proper IDs) - Get ALL results
1019
+ if (searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
968
1020
  searchAttempts++;
969
- logger.log(`šŸ” Attempt ${searchAttempts}: Using reliable OData filter search first`);
970
- const filterResults = await this.performFilteredSearch(criteria, maxResults * 2);
1021
+ logger.log(`šŸ” Attempt ${searchAttempts}: Using reliable OData filter search to get ALL results`);
1022
+ const filterResults = await this.performFilteredSearchAll(criteria);
971
1023
  allMessages.push(...filterResults);
972
1024
  if (allMessages.length > 0) {
973
1025
  logger.log(`āœ… Found ${allMessages.length} results with OData filter search`);
974
1026
  }
975
1027
  }
976
- // Strategy 2: Use Microsoft Graph Search API for text-based queries (backup)
977
- if (allMessages.length === 0 && criteria.query && searchAttempts < maxAttempts) {
1028
+ // Strategy 2: Use Microsoft Graph Search API for text-based queries (backup) - Get ALL results
1029
+ if (allMessages.length === 0 && criteria.query && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
978
1030
  searchAttempts++;
979
- logger.log(`šŸ” Attempt ${searchAttempts}: Using Graph Search API for query: "${criteria.query}"`);
980
- const searchResults = await this.performGraphSearch(criteria.query, maxResults * 2); // Search for more to increase chances
1031
+ logger.log(`šŸ” Attempt ${searchAttempts}: Using Graph Search API for query: "${criteria.query}" to get ALL results`);
1032
+ const searchResults = await this.performGraphSearchAll(criteria.query);
981
1033
  allMessages.push(...searchResults);
982
1034
  if (allMessages.length > 0) {
983
1035
  logger.log(`āœ… Found ${allMessages.length} results with Graph Search API`);
984
1036
  }
985
1037
  }
986
- // Strategy 3: Use KQL (Keyword Query Language) for advanced searches
987
- if (allMessages.length === 0 && searchAttempts < maxAttempts) {
1038
+ // Strategy 3: Use KQL (Keyword Query Language) for advanced searches - Get ALL results
1039
+ if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
988
1040
  const kqlQuery = this.buildKQLQuery(criteria);
989
1041
  if (kqlQuery) {
990
1042
  searchAttempts++;
991
- logger.log(`šŸ” Attempt ${searchAttempts}: Using KQL search: "${kqlQuery}"`);
992
- const kqlResults = await this.performKQLSearch(kqlQuery, maxResults * 2);
1043
+ logger.log(`šŸ” Attempt ${searchAttempts}: Using KQL search: "${kqlQuery}" to get ALL results`);
1044
+ const kqlResults = await this.performKQLSearchAll(kqlQuery);
993
1045
  allMessages.push(...kqlResults);
994
1046
  if (allMessages.length > 0) {
995
1047
  logger.log(`āœ… Found ${allMessages.length} results with KQL search`);
996
1048
  }
997
1049
  }
998
1050
  }
999
- // Strategy 4: Try relaxed KQL search (remove some constraints)
1000
- if (allMessages.length === 0 && searchAttempts < maxAttempts) {
1051
+ // Strategy 4: Try relaxed KQL search (remove some constraints) - Get ALL results
1052
+ if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
1001
1053
  const relaxedKQL = this.buildRelaxedKQLQuery(criteria);
1002
1054
  if (relaxedKQL && relaxedKQL !== this.buildKQLQuery(criteria)) {
1003
1055
  searchAttempts++;
1004
- logger.log(`šŸ” Attempt ${searchAttempts}: Using relaxed KQL search: "${relaxedKQL}"`);
1005
- const relaxedResults = await this.performKQLSearch(relaxedKQL, maxResults * 2);
1056
+ logger.log(`šŸ” Attempt ${searchAttempts}: Using relaxed KQL search: "${relaxedKQL}" to get ALL results`);
1057
+ const relaxedResults = await this.performKQLSearchAll(relaxedKQL);
1006
1058
  allMessages.push(...relaxedResults);
1007
1059
  if (allMessages.length > 0) {
1008
1060
  logger.log(`āœ… Found ${allMessages.length} results with relaxed KQL search`);
@@ -1010,33 +1062,34 @@ ${originalBodyContent}
1010
1062
  }
1011
1063
  }
1012
1064
  // Strategy 5: If we found results but they have UNKNOWN IDs, try to resolve them
1013
- if (allMessages.length > 0 && allMessages.some(msg => msg.id === 'UNKNOWN')) {
1065
+ if (allMessages.length > 0 && allMessages.some(msg => msg.id === 'UNKNOWN') && (Date.now() - startTime) < searchTimeout) {
1014
1066
  logger.log(`šŸ” Attempting to resolve UNKNOWN message IDs using direct message queries`);
1015
1067
  allMessages = await this.resolveUnknownMessageIds(allMessages, criteria);
1016
1068
  }
1017
- // Strategy 6: Partial text search across recent emails (expanded scope)
1018
- if (allMessages.length === 0 && searchAttempts < maxAttempts) {
1069
+ // Strategy 6: Partial text search across ALL recent emails (expanded scope)
1070
+ if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
1019
1071
  searchAttempts++;
1020
- logger.log(`šŸ” Attempt ${searchAttempts}: Using expanded partial text search`);
1021
- const partialResults = await this.performPartialTextSearch(criteria, maxResults * 4); // Very broad search
1072
+ logger.log(`šŸ” Attempt ${searchAttempts}: Using expanded partial text search across ALL emails`);
1073
+ const partialResults = await this.performPartialTextSearchAll(criteria);
1022
1074
  allMessages.push(...partialResults);
1023
1075
  if (allMessages.length > 0) {
1024
1076
  logger.log(`āœ… Found ${allMessages.length} results with partial text search`);
1025
1077
  }
1026
1078
  }
1027
- // Strategy 7: Fallback to basic search with maximum scope
1028
- if (allMessages.length === 0 && searchAttempts < maxAttempts) {
1079
+ // Strategy 7: Fallback to basic search with ALL results
1080
+ if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
1029
1081
  searchAttempts++;
1030
- logger.log(`šŸ” Attempt ${searchAttempts}: Using fallback basic search with maximum scope`);
1031
- const basicResult = await this.performBasicSearch({
1032
- ...criteria,
1033
- maxResults: Math.max(maxResults * 5, 500) // Very large scope
1034
- });
1082
+ logger.log(`šŸ” Attempt ${searchAttempts}: Using fallback basic search to get ALL results`);
1083
+ const basicResult = await this.performBasicSearchAll(criteria);
1035
1084
  allMessages.push(...basicResult.messages);
1036
1085
  if (allMessages.length > 0) {
1037
1086
  logger.log(`āœ… Found ${allMessages.length} results with basic search fallback`);
1038
1087
  }
1039
1088
  }
1089
+ // Check if we hit the timeout
1090
+ if ((Date.now() - startTime) >= searchTimeout) {
1091
+ logger.log(`āš ļø Search timeout reached after ${(Date.now() - startTime) / 1000} seconds. Returning partial results.`);
1092
+ }
1040
1093
  // Remove duplicates and apply advanced filtering
1041
1094
  const uniqueMessages = this.removeDuplicateMessages(allMessages);
1042
1095
  logger.log(`šŸ“§ After deduplication: ${uniqueMessages.length} unique messages`);
@@ -1045,30 +1098,31 @@ ${originalBodyContent}
1045
1098
  // Sort by relevance and date
1046
1099
  const sortedMessages = this.sortSearchResults(filteredMessages, criteria);
1047
1100
  // If still no results, try one more time with very relaxed criteria
1048
- if (sortedMessages.length === 0 && (criteria.query || criteria.from || criteria.subject)) {
1049
- logger.log(`šŸ” Final attempt: Ultra-relaxed search for any partial matches`);
1050
- const ultraRelaxedResults = await this.performUltraRelaxedSearch(criteria, maxResults * 3);
1101
+ if (sortedMessages.length === 0 && (criteria.query || criteria.from || criteria.subject) && (Date.now() - startTime) < searchTimeout) {
1102
+ logger.log(`šŸ” Final attempt: Ultra-relaxed search for any partial matches across ALL emails`);
1103
+ const ultraRelaxedResults = await this.performUltraRelaxedSearchAll(criteria);
1051
1104
  const finalFiltered = this.applyUltraRelaxedFiltering(ultraRelaxedResults, criteria);
1052
1105
  if (finalFiltered.length > 0) {
1053
1106
  logger.log(`āœ… Ultra-relaxed search found ${finalFiltered.length} results`);
1054
- const limitedMessages = finalFiltered.slice(0, maxResults);
1107
+ // Apply maxResults limit if specified
1108
+ const finalMessages = maxResults > 0 ? finalFiltered.slice(0, maxResults) : finalFiltered.slice(0, safetyLimit);
1055
1109
  const searchResult = {
1056
- messages: limitedMessages,
1057
- hasMore: finalFiltered.length > maxResults
1110
+ messages: finalMessages,
1111
+ hasMore: maxResults > 0 ? finalFiltered.length > maxResults : finalFiltered.length > safetyLimit
1058
1112
  };
1059
1113
  this.setCachedResults(cacheKey, searchResult);
1060
1114
  return searchResult;
1061
1115
  }
1062
1116
  }
1063
- // Limit results
1064
- const limitedMessages = sortedMessages.slice(0, maxResults);
1117
+ // Apply maxResults limit if specified, otherwise return all results (with safety limit)
1118
+ const finalMessages = maxResults > 0 ? sortedMessages.slice(0, maxResults) : sortedMessages.slice(0, safetyLimit);
1065
1119
  const searchResult = {
1066
- messages: limitedMessages,
1067
- hasMore: sortedMessages.length > maxResults
1120
+ messages: finalMessages,
1121
+ hasMore: maxResults > 0 ? sortedMessages.length > maxResults : sortedMessages.length > safetyLimit
1068
1122
  };
1069
1123
  this.setCachedResults(cacheKey, searchResult);
1070
- logger.log(`šŸ” Search completed after ${searchAttempts} attempts: ${limitedMessages.length} final results`);
1071
- if (limitedMessages.length === 0) {
1124
+ logger.log(`šŸ” Search completed after ${searchAttempts} attempts: ${finalMessages.length} final results (${maxResults > 0 ? 'limited' : 'safety-limited'}) in ${(Date.now() - startTime) / 1000} seconds`);
1125
+ if (finalMessages.length === 0) {
1072
1126
  logger.log(`āŒ No results found despite ${searchAttempts} search strategies. Consider broadening search criteria.`);
1073
1127
  }
1074
1128
  return searchResult;
@@ -1077,7 +1131,7 @@ ${originalBodyContent}
1077
1131
  logger.error('Error in persistent email search:', error);
1078
1132
  // Final fallback - get some recent emails and filter them
1079
1133
  logger.log('šŸ”„ Final fallback: getting recent emails to filter manually');
1080
- return await this.performBasicSearch(criteria);
1134
+ return await this.performBasicSearchAll(criteria);
1081
1135
  }
1082
1136
  }, 'searchEmails');
1083
1137
  }
@@ -1184,21 +1238,33 @@ ${originalBodyContent}
1184
1238
  // Smart sender search - handles partial names, emails, display names
1185
1239
  const fromTerm = criteria.from.trim();
1186
1240
  if (fromTerm.includes('@')) {
1187
- kqlParts.push(`from:${fromTerm}`);
1241
+ // For email addresses, escape special characters
1242
+ const escapedFrom = fromTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1243
+ kqlParts.push(`from:${escapedFrom}`);
1188
1244
  }
1189
1245
  else {
1190
- // For names, search in both from field and sender
1191
- kqlParts.push(`(from:"${fromTerm}" OR sender:"${fromTerm}" OR from:*${fromTerm}*)`);
1246
+ // For names, search in both from field and sender with proper escaping
1247
+ const escapedFrom = fromTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1248
+ kqlParts.push(`(from:"${escapedFrom}" OR sender:"${escapedFrom}" OR from:*${escapedFrom}*)`);
1192
1249
  }
1193
1250
  }
1194
1251
  if (criteria.to) {
1195
- kqlParts.push(`to:${criteria.to}`);
1252
+ const escapedTo = criteria.to.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1253
+ kqlParts.push(`to:${escapedTo}`);
1196
1254
  }
1197
1255
  if (criteria.cc) {
1198
- kqlParts.push(`cc:${criteria.cc}`);
1256
+ const escapedCc = criteria.cc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1257
+ kqlParts.push(`cc:${escapedCc}`);
1199
1258
  }
1200
1259
  if (criteria.subject) {
1201
- kqlParts.push(`subject:"${criteria.subject}"`);
1260
+ // Properly escape subject terms
1261
+ const escapedSubject = criteria.subject.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1262
+ kqlParts.push(`subject:"${escapedSubject}"`);
1263
+ }
1264
+ if (criteria.query) {
1265
+ // Escape general query terms
1266
+ const escapedQuery = criteria.query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1267
+ kqlParts.push(`"${escapedQuery}"`);
1202
1268
  }
1203
1269
  if (criteria.hasAttachment === true) {
1204
1270
  kqlParts.push('hasattachment:true');
@@ -1224,7 +1290,8 @@ ${originalBodyContent}
1224
1290
  kqlParts.push(`received<=${beforeDate}`);
1225
1291
  }
1226
1292
  if (criteria.folder) {
1227
- kqlParts.push(`foldernames:"${criteria.folder}"`);
1293
+ const escapedFolder = criteria.folder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1294
+ kqlParts.push(`foldernames:"${escapedFolder}"`);
1228
1295
  }
1229
1296
  return kqlParts.join(' AND ');
1230
1297
  }
@@ -1278,11 +1345,42 @@ ${originalBodyContent}
1278
1345
  .top(Math.min(maxResults, 999));
1279
1346
  // Apply OData filters where possible
1280
1347
  const filters = [];
1281
- if (criteria.from && criteria.from.includes('@')) {
1282
- filters.push(`from/emailAddress/address eq '${criteria.from}'`);
1348
+ if (criteria.from) {
1349
+ // Properly escape single quotes in email addresses and names
1350
+ const escapedFrom = this.escapeODataValue(criteria.from);
1351
+ if (criteria.from.includes('@')) {
1352
+ // For email addresses, use exact match
1353
+ filters.push(`from/emailAddress/address eq '${escapedFrom}'`);
1354
+ }
1355
+ else {
1356
+ // For names, use contains for partial matching
1357
+ filters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
1358
+ }
1283
1359
  }
1284
1360
  if (criteria.to && criteria.to.includes('@')) {
1285
- filters.push(`toRecipients/any(r: r/emailAddress/address eq '${criteria.to}')`);
1361
+ // Properly escape single quotes in email addresses
1362
+ const escapedTo = this.escapeODataValue(criteria.to);
1363
+ filters.push(`toRecipients/any(r: r/emailAddress/address eq '${escapedTo}')`);
1364
+ }
1365
+ if (criteria.cc && criteria.cc.includes('@')) {
1366
+ // Properly escape single quotes in email addresses
1367
+ const escapedCc = this.escapeODataValue(criteria.cc);
1368
+ filters.push(`ccRecipients/any(r: r/emailAddress/address eq '${escapedCc}')`);
1369
+ }
1370
+ if (criteria.subject) {
1371
+ // Properly escape single quotes in subject
1372
+ const escapedSubject = this.escapeODataValue(criteria.subject);
1373
+ filters.push(`contains(subject,'${escapedSubject}')`);
1374
+ }
1375
+ if (criteria.after) {
1376
+ // Fix date formatting - use proper ISO format with quotes
1377
+ const afterDate = this.formatDateForOData(criteria.after);
1378
+ filters.push(`receivedDateTime ge '${afterDate}'`);
1379
+ }
1380
+ if (criteria.before) {
1381
+ // Fix date formatting - use proper ISO format with quotes
1382
+ const beforeDate = this.formatDateForOData(criteria.before);
1383
+ filters.push(`receivedDateTime le '${beforeDate}'`);
1286
1384
  }
1287
1385
  if (criteria.isUnread !== undefined) {
1288
1386
  filters.push(`isRead eq ${!criteria.isUnread}`);
@@ -1294,17 +1392,26 @@ ${originalBodyContent}
1294
1392
  filters.push(`importance eq '${criteria.importance}'`);
1295
1393
  }
1296
1394
  if (filters.length > 0) {
1297
- apiCall = apiCall.filter(filters.join(' and '));
1395
+ const filterString = filters.join(' and ');
1396
+ logger.log(`šŸ” Applying OData filter: ${filterString}`);
1397
+ apiCall = apiCall.filter(filterString);
1298
1398
  }
1299
1399
  // Apply folder filter using specific folder API
1300
1400
  if (criteria.folder) {
1301
1401
  const folders = await this.findFolderByName(criteria.folder);
1302
1402
  if (folders.length > 0) {
1403
+ logger.log(`šŸ” Switching to folder-specific search: ${folders[0].displayName} (${folders[0].id})`);
1303
1404
  apiCall = graphClient
1304
1405
  .api(`/me/mailFolders/${folders[0].id}/messages`)
1305
1406
  .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
1306
1407
  .orderby('receivedDateTime desc')
1307
1408
  .top(Math.min(maxResults, 999));
1409
+ // Re-apply filters for folder-specific search
1410
+ if (filters.length > 0) {
1411
+ const filterString = filters.join(' and ');
1412
+ logger.log(`šŸ” Applying OData filter to folder search: ${filterString}`);
1413
+ apiCall = apiCall.filter(filterString);
1414
+ }
1308
1415
  }
1309
1416
  }
1310
1417
  const result = await apiCall.get();
@@ -1314,6 +1421,7 @@ ${originalBodyContent}
1314
1421
  }
1315
1422
  catch (error) {
1316
1423
  logger.log(`āŒ Filtered Search failed: ${error}`);
1424
+ logger.error(`āŒ Filtered Search error details:`, JSON.stringify(error, null, 2));
1317
1425
  return [];
1318
1426
  }
1319
1427
  }
@@ -1891,7 +1999,7 @@ ${originalBodyContent}
1891
1999
  }
1892
2000
  }
1893
2001
  /**
1894
- * Search for emails addressed to the current user (both TO and CC recipients)
2002
+ * Search for ALL emails addressed to the current user (both TO and CC recipients)
1895
2003
  */
1896
2004
  async searchEmailsToMe(additionalCriteria = {}) {
1897
2005
  try {
@@ -1904,96 +2012,171 @@ ${originalBodyContent}
1904
2012
  return cachedResults;
1905
2013
  }
1906
2014
  try {
1907
- // Start with a basic query to get emails
1908
- const apiCall = graphClient.api('/me/messages')
1909
- .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink');
1910
- // Add search query for user's email in TO or CC using proper syntax
1911
- apiCall.search(`"${userEmail}"`);
1912
- // Add simple filters that are supported by the API
1913
- if (additionalCriteria.after) {
1914
- apiCall.filter(`receivedDateTime ge ${new Date(additionalCriteria.after).toISOString()}`);
1915
- }
1916
- if (additionalCriteria.before) {
1917
- apiCall.filter(`receivedDateTime le ${new Date(additionalCriteria.before).toISOString()}`);
1918
- }
1919
- if (additionalCriteria.isUnread !== undefined) {
1920
- apiCall.filter(`isRead eq ${!additionalCriteria.isUnread}`);
1921
- }
1922
- if (additionalCriteria.importance) {
1923
- apiCall.filter(`importance eq '${additionalCriteria.importance}'`);
1924
- }
1925
- // Set page size
1926
- const pageSize = Math.min(additionalCriteria.maxResults || 100, 100);
1927
- apiCall.top(pageSize);
1928
- const result = await apiCall.get();
1929
- const messages = result.value?.map((email) => ({
1930
- id: email.id,
1931
- subject: email.subject || '',
1932
- from: {
1933
- name: email.from?.emailAddress?.name || '',
1934
- address: email.from?.emailAddress?.address || ''
1935
- },
1936
- toRecipients: email.toRecipients?.map((recipient) => ({
1937
- name: recipient.emailAddress?.name || '',
1938
- address: recipient.emailAddress?.address || ''
1939
- })) || [],
1940
- ccRecipients: email.ccRecipients?.map((recipient) => ({
1941
- name: recipient.emailAddress?.name || '',
1942
- address: recipient.emailAddress?.address || ''
1943
- })) || [],
1944
- receivedDateTime: email.receivedDateTime,
1945
- sentDateTime: email.sentDateTime,
1946
- bodyPreview: email.bodyPreview || '',
1947
- isRead: email.isRead || false,
1948
- hasAttachments: email.hasAttachments || false,
1949
- importance: email.importance || 'normal',
1950
- conversationId: email.conversationId || '',
1951
- parentFolderId: email.parentFolderId || '',
1952
- webLink: email.webLink || '',
1953
- attachments: []
1954
- })) || [];
2015
+ const allMessages = [];
2016
+ let nextLink = null;
2017
+ let pageCount = 0;
2018
+ const maxPages = 50; // Safety limit
2019
+ const maxResults = additionalCriteria.maxResults || 0; // 0 means no limit
2020
+ logger.log(`šŸ” Starting searchEmailsToMe to get ALL results for user: ${userEmail}`);
2021
+ do {
2022
+ pageCount++;
2023
+ logger.log(`šŸ” searchEmailsToMe - Fetching page ${pageCount}`);
2024
+ // Start with a basic query to get emails
2025
+ const apiCall = graphClient.api('/me/messages')
2026
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink');
2027
+ // Add search query for user's email in TO or CC using proper syntax
2028
+ apiCall.search(`"${userEmail}"`);
2029
+ // Add simple filters that are supported by the API
2030
+ const filters = [];
2031
+ if (additionalCriteria.after) {
2032
+ const afterDate = this.formatDateForOData(additionalCriteria.after);
2033
+ filters.push(`receivedDateTime ge '${afterDate}'`);
2034
+ }
2035
+ if (additionalCriteria.before) {
2036
+ const beforeDate = this.formatDateForOData(additionalCriteria.before);
2037
+ filters.push(`receivedDateTime le '${beforeDate}'`);
2038
+ }
2039
+ if (additionalCriteria.isUnread !== undefined) {
2040
+ filters.push(`isRead eq ${!additionalCriteria.isUnread}`);
2041
+ }
2042
+ if (additionalCriteria.importance) {
2043
+ filters.push(`importance eq '${additionalCriteria.importance}'`);
2044
+ }
2045
+ if (additionalCriteria.hasAttachment !== undefined) {
2046
+ filters.push(`hasAttachments eq ${additionalCriteria.hasAttachment}`);
2047
+ }
2048
+ // Apply filters if any
2049
+ if (filters.length > 0) {
2050
+ const filterString = filters.join(' and ');
2051
+ logger.log(`šŸ” Applying filters to searchEmailsToMe: ${filterString}`);
2052
+ apiCall.filter(filterString);
2053
+ }
2054
+ // Set page size
2055
+ const pageSize = Math.min(999, maxResults > 0 ? maxResults : 999);
2056
+ apiCall.top(pageSize);
2057
+ // Use nextLink if available for pagination
2058
+ if (nextLink) {
2059
+ const nextApiCall = graphClient.api(nextLink);
2060
+ const result = await nextApiCall.get();
2061
+ const messages = result.value?.map((email) => ({
2062
+ id: email.id,
2063
+ subject: email.subject || '',
2064
+ from: {
2065
+ name: email.from?.emailAddress?.name || '',
2066
+ address: email.from?.emailAddress?.address || ''
2067
+ },
2068
+ toRecipients: email.toRecipients?.map((recipient) => ({
2069
+ name: recipient.emailAddress?.name || '',
2070
+ address: recipient.emailAddress?.address || ''
2071
+ })) || [],
2072
+ ccRecipients: email.ccRecipients?.map((recipient) => ({
2073
+ name: recipient.emailAddress?.name || '',
2074
+ address: recipient.emailAddress?.address || ''
2075
+ })) || [],
2076
+ receivedDateTime: email.receivedDateTime,
2077
+ sentDateTime: email.sentDateTime,
2078
+ bodyPreview: email.bodyPreview || '',
2079
+ isRead: email.isRead || false,
2080
+ hasAttachments: email.hasAttachments || false,
2081
+ importance: email.importance || 'normal',
2082
+ conversationId: email.conversationId || '',
2083
+ parentFolderId: email.parentFolderId || '',
2084
+ webLink: email.webLink || '',
2085
+ attachments: []
2086
+ })) || [];
2087
+ allMessages.push(...messages);
2088
+ logger.log(`šŸ“§ searchEmailsToMe page ${pageCount}: ${messages.length} results (total: ${allMessages.length})`);
2089
+ // Get next page link
2090
+ nextLink = result['@odata.nextLink'] || null;
2091
+ }
2092
+ else {
2093
+ const result = await apiCall.get();
2094
+ const messages = result.value?.map((email) => ({
2095
+ id: email.id,
2096
+ subject: email.subject || '',
2097
+ from: {
2098
+ name: email.from?.emailAddress?.name || '',
2099
+ address: email.from?.emailAddress?.address || ''
2100
+ },
2101
+ toRecipients: email.toRecipients?.map((recipient) => ({
2102
+ name: recipient.emailAddress?.name || '',
2103
+ address: recipient.emailAddress?.address || ''
2104
+ })) || [],
2105
+ ccRecipients: email.ccRecipients?.map((recipient) => ({
2106
+ name: recipient.emailAddress?.name || '',
2107
+ address: recipient.emailAddress?.address || ''
2108
+ })) || [],
2109
+ receivedDateTime: email.receivedDateTime,
2110
+ sentDateTime: email.sentDateTime,
2111
+ bodyPreview: email.bodyPreview || '',
2112
+ isRead: email.isRead || false,
2113
+ hasAttachments: email.hasAttachments || false,
2114
+ importance: email.importance || 'normal',
2115
+ conversationId: email.conversationId || '',
2116
+ parentFolderId: email.parentFolderId || '',
2117
+ webLink: email.webLink || '',
2118
+ attachments: []
2119
+ })) || [];
2120
+ allMessages.push(...messages);
2121
+ logger.log(`šŸ“§ searchEmailsToMe page ${pageCount}: ${messages.length} results (total: ${allMessages.length})`);
2122
+ // Get next page link
2123
+ nextLink = result['@odata.nextLink'] || null;
2124
+ }
2125
+ // Safety check to prevent infinite loops
2126
+ if (pageCount >= maxPages) {
2127
+ logger.log(`āš ļø Reached maximum page limit (${maxPages}). Stopping pagination.`);
2128
+ break;
2129
+ }
2130
+ // Stop if we've reached the maxResults limit
2131
+ if (maxResults > 0 && allMessages.length >= maxResults) {
2132
+ logger.log(`āœ… Reached maxResults limit (${maxResults}). Stopping pagination.`);
2133
+ break;
2134
+ }
2135
+ } while (nextLink);
1955
2136
  // Filter messages to only include those where the user is in TO or CC
1956
- let filteredMessages = messages.filter(message => {
2137
+ let filteredMessages = allMessages.filter(message => {
1957
2138
  const isInTo = message.toRecipients.some(recipient => recipient.address.toLowerCase() === userEmail.toLowerCase());
1958
2139
  const isInCc = message.ccRecipients.some(recipient => recipient.address.toLowerCase() === userEmail.toLowerCase());
1959
2140
  return isInTo || isInCc;
1960
2141
  });
1961
- // Apply hasAttachment filter manually if specified
1962
- if (additionalCriteria.hasAttachment !== undefined) {
1963
- filteredMessages = filteredMessages.filter(message => message.hasAttachments === additionalCriteria.hasAttachment);
1964
- }
1965
- // Sort messages by receivedDateTime in descending order
1966
- filteredMessages.sort((a, b) => new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime());
1967
- // For emails with attachments, get attachment counts
1968
- for (const message of filteredMessages) {
1969
- if (message.hasAttachments) {
1970
- try {
1971
- const attachments = await graphClient
1972
- .api(`/me/messages/${message.id}/attachments`)
1973
- .select('id')
1974
- .get();
1975
- message.attachments = new Array(attachments.value?.length || 0);
1976
- }
1977
- catch (error) {
1978
- logger.error(`Error getting attachment count for message ${message.id}:`, error);
1979
- message.attachments = [];
1980
- }
1981
- }
2142
+ // Apply additional manual filtering for criteria that can't be handled by API
2143
+ if (additionalCriteria.from) {
2144
+ const fromTerm = additionalCriteria.from.toLowerCase();
2145
+ filteredMessages = filteredMessages.filter(message => {
2146
+ const fromName = message.from.name.toLowerCase();
2147
+ const fromAddress = message.from.address.toLowerCase();
2148
+ return fromName.includes(fromTerm) || fromAddress.includes(fromTerm);
2149
+ });
2150
+ }
2151
+ if (additionalCriteria.subject) {
2152
+ const subjectTerm = additionalCriteria.subject.toLowerCase();
2153
+ filteredMessages = filteredMessages.filter(message => message.subject.toLowerCase().includes(subjectTerm));
1982
2154
  }
2155
+ if (additionalCriteria.query) {
2156
+ const queryTerm = additionalCriteria.query.toLowerCase();
2157
+ filteredMessages = filteredMessages.filter(message => {
2158
+ const searchableText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
2159
+ return searchableText.includes(queryTerm);
2160
+ });
2161
+ }
2162
+ // Apply maxResults limit if specified
2163
+ const finalMessages = maxResults > 0 ? filteredMessages.slice(0, maxResults) : filteredMessages;
1983
2164
  const searchResult = {
1984
- messages: filteredMessages,
1985
- hasMore: !!result['@odata.nextLink']
2165
+ messages: finalMessages,
2166
+ hasMore: maxResults > 0 ? filteredMessages.length > maxResults : false
1986
2167
  };
1987
2168
  this.setCachedResults(cacheKey, searchResult);
2169
+ logger.log(`šŸ“§ searchEmailsToMe completed: ${finalMessages.length} final results from ${filteredMessages.length} filtered (${maxResults > 0 ? 'limited' : 'ALL'})`);
1988
2170
  return searchResult;
1989
2171
  }
1990
2172
  catch (error) {
1991
- logger.error('Error in email search:', error);
1992
- throw error;
2173
+ logger.error('Error in searchEmailsToMe API call:', error);
2174
+ // Fallback to basic search with manual filtering
2175
+ return await this.performBasicSearchAll(additionalCriteria);
1993
2176
  }
1994
2177
  }
1995
2178
  catch (error) {
1996
- logger.error('Error searching emails addressed to me:', error);
2179
+ logger.error('Error in searchEmailsToMe:', error);
1997
2180
  throw error;
1998
2181
  }
1999
2182
  }
@@ -2365,5 +2548,464 @@ ${originalBodyContent}
2365
2548
  const matches = shorter.split('').filter(char => longer.includes(char)).length;
2366
2549
  return matches / longer.length;
2367
2550
  }
2551
+ /**
2552
+ * Get ALL results using OData filter-based search with pagination
2553
+ */
2554
+ async performFilteredSearchAll(criteria) {
2555
+ try {
2556
+ const graphClient = await this.getGraphClient();
2557
+ const allMessages = [];
2558
+ let nextLink = null;
2559
+ let pageCount = 0;
2560
+ const maxPages = 50; // Safety limit to prevent infinite loops
2561
+ do {
2562
+ pageCount++;
2563
+ logger.log(`šŸ” Filtered Search - Fetching page ${pageCount}`);
2564
+ let apiCall = graphClient
2565
+ .api('/me/messages')
2566
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
2567
+ .orderby('receivedDateTime desc')
2568
+ .top(999); // Maximum allowed per page
2569
+ // Apply OData filters where possible
2570
+ const filters = [];
2571
+ if (criteria.from) {
2572
+ // Properly escape single quotes in email addresses and names
2573
+ const escapedFrom = this.escapeODataValue(criteria.from);
2574
+ if (criteria.from.includes('@')) {
2575
+ // For email addresses, use exact match
2576
+ filters.push(`from/emailAddress/address eq '${escapedFrom}'`);
2577
+ }
2578
+ else {
2579
+ // For names, use contains for partial matching
2580
+ filters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
2581
+ }
2582
+ }
2583
+ if (criteria.to && criteria.to.includes('@')) {
2584
+ // Properly escape single quotes in email addresses
2585
+ const escapedTo = this.escapeODataValue(criteria.to);
2586
+ filters.push(`toRecipients/any(r: r/emailAddress/address eq '${escapedTo}')`);
2587
+ }
2588
+ if (criteria.cc && criteria.cc.includes('@')) {
2589
+ // Properly escape single quotes in email addresses
2590
+ const escapedCc = this.escapeODataValue(criteria.cc);
2591
+ filters.push(`ccRecipients/any(r: r/emailAddress/address eq '${escapedCc}')`);
2592
+ }
2593
+ if (criteria.subject) {
2594
+ // Properly escape single quotes in subject
2595
+ const escapedSubject = this.escapeODataValue(criteria.subject);
2596
+ filters.push(`contains(subject,'${escapedSubject}')`);
2597
+ }
2598
+ if (criteria.after) {
2599
+ // Fix date formatting - use proper ISO format with quotes
2600
+ const afterDate = this.formatDateForOData(criteria.after);
2601
+ filters.push(`receivedDateTime ge '${afterDate}'`);
2602
+ }
2603
+ if (criteria.before) {
2604
+ // Fix date formatting - use proper ISO format with quotes
2605
+ const beforeDate = this.formatDateForOData(criteria.before);
2606
+ filters.push(`receivedDateTime le '${beforeDate}'`);
2607
+ }
2608
+ if (criteria.isUnread !== undefined) {
2609
+ filters.push(`isRead eq ${!criteria.isUnread}`);
2610
+ }
2611
+ if (criteria.hasAttachment !== undefined) {
2612
+ filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
2613
+ }
2614
+ if (criteria.importance) {
2615
+ filters.push(`importance eq '${criteria.importance}'`);
2616
+ }
2617
+ if (filters.length > 0) {
2618
+ const filterString = filters.join(' and ');
2619
+ logger.log(`šŸ” Applying OData filter: ${filterString}`);
2620
+ apiCall = apiCall.filter(filterString);
2621
+ }
2622
+ // Apply folder filter using specific folder API
2623
+ if (criteria.folder) {
2624
+ const folders = await this.findFolderByName(criteria.folder);
2625
+ if (folders.length > 0) {
2626
+ logger.log(`šŸ” Switching to folder-specific search: ${folders[0].displayName} (${folders[0].id})`);
2627
+ apiCall = graphClient
2628
+ .api(`/me/mailFolders/${folders[0].id}/messages`)
2629
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
2630
+ .orderby('receivedDateTime desc')
2631
+ .top(999);
2632
+ // Re-apply filters for folder-specific search
2633
+ if (filters.length > 0) {
2634
+ const filterString = filters.join(' and ');
2635
+ logger.log(`šŸ” Applying OData filter to folder search: ${filterString}`);
2636
+ apiCall = apiCall.filter(filterString);
2637
+ }
2638
+ }
2639
+ }
2640
+ // Use nextLink if available for pagination
2641
+ if (nextLink) {
2642
+ apiCall = graphClient.api(nextLink);
2643
+ }
2644
+ const result = await apiCall.get();
2645
+ const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
2646
+ allMessages.push(...messages);
2647
+ logger.log(`šŸ“§ Filtered Search page ${pageCount}: ${messages.length} results (total: ${allMessages.length})`);
2648
+ // Get next page link
2649
+ nextLink = result['@odata.nextLink'] || null;
2650
+ // Safety check to prevent infinite loops
2651
+ if (pageCount >= maxPages) {
2652
+ logger.log(`āš ļø Reached maximum page limit (${maxPages}). Stopping pagination.`);
2653
+ break;
2654
+ }
2655
+ } while (nextLink);
2656
+ logger.log(`šŸ“§ Filtered Search completed: ${allMessages.length} total results from ${pageCount} pages`);
2657
+ return allMessages;
2658
+ }
2659
+ catch (error) {
2660
+ logger.log(`āŒ Filtered Search failed: ${error}`);
2661
+ logger.error(`āŒ Filtered Search error details:`, JSON.stringify(error, null, 2));
2662
+ return [];
2663
+ }
2664
+ }
2665
+ /**
2666
+ * Get ALL results using Microsoft Graph Search API with pagination
2667
+ */
2668
+ async performGraphSearchAll(query) {
2669
+ try {
2670
+ const graphClient = await this.getGraphClient();
2671
+ const allMessages = [];
2672
+ let from = 0;
2673
+ const pageSize = 1000; // Graph Search max is 1000
2674
+ let pageCount = 0;
2675
+ const maxPages = 20; // Safety limit
2676
+ do {
2677
+ pageCount++;
2678
+ logger.log(`šŸ” Graph Search - Fetching page ${pageCount} (from: ${from})`);
2679
+ const searchRequest = {
2680
+ requests: [
2681
+ {
2682
+ entityTypes: ['message'],
2683
+ query: {
2684
+ queryString: query
2685
+ },
2686
+ from: from,
2687
+ size: pageSize,
2688
+ fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
2689
+ }
2690
+ ]
2691
+ };
2692
+ const result = await graphClient
2693
+ .api('/search/query')
2694
+ .post(searchRequest);
2695
+ const messages = [];
2696
+ if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
2697
+ for (const hit of result.value[0].hitsContainers[0].hits) {
2698
+ const email = hit.resource;
2699
+ messages.push(this.mapEmailResult(email));
2700
+ }
2701
+ }
2702
+ allMessages.push(...messages);
2703
+ logger.log(`šŸ“§ Graph Search page ${pageCount}: ${messages.length} results (total: ${allMessages.length})`);
2704
+ // Check if there are more results
2705
+ const totalHits = result.value?.[0]?.hitsContainers?.[0]?.total || 0;
2706
+ from += pageSize;
2707
+ // Safety check to prevent infinite loops
2708
+ if (pageCount >= maxPages || messages.length < pageSize) {
2709
+ logger.log(`āš ļø Reached end of results or maximum page limit. Stopping pagination.`);
2710
+ break;
2711
+ }
2712
+ } while (true);
2713
+ logger.log(`šŸ“§ Graph Search completed: ${allMessages.length} total results from ${pageCount} pages`);
2714
+ return allMessages;
2715
+ }
2716
+ catch (error) {
2717
+ logger.log(`āŒ Graph Search failed: ${error}. Falling back to alternative search.`);
2718
+ logger.error(`āŒ Graph Search error details:`, JSON.stringify(error, null, 2));
2719
+ return [];
2720
+ }
2721
+ }
2722
+ /**
2723
+ * Get ALL results using KQL search with pagination
2724
+ */
2725
+ async performKQLSearchAll(kqlQuery) {
2726
+ try {
2727
+ const graphClient = await this.getGraphClient();
2728
+ const allMessages = [];
2729
+ let from = 0;
2730
+ const pageSize = 1000;
2731
+ let pageCount = 0;
2732
+ const maxPages = 20; // Safety limit
2733
+ do {
2734
+ pageCount++;
2735
+ logger.log(`šŸ” KQL Search - Fetching page ${pageCount} (from: ${from})`);
2736
+ const searchRequest = {
2737
+ requests: [
2738
+ {
2739
+ entityTypes: ['message'],
2740
+ query: {
2741
+ queryString: kqlQuery
2742
+ },
2743
+ from: from,
2744
+ size: pageSize,
2745
+ fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
2746
+ }
2747
+ ]
2748
+ };
2749
+ const result = await graphClient
2750
+ .api('/search/query')
2751
+ .post(searchRequest);
2752
+ const messages = [];
2753
+ if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
2754
+ for (const hit of result.value[0].hitsContainers[0].hits) {
2755
+ const email = hit.resource;
2756
+ messages.push(this.mapEmailResult(email));
2757
+ }
2758
+ }
2759
+ allMessages.push(...messages);
2760
+ logger.log(`šŸ“§ KQL Search page ${pageCount}: ${messages.length} results (total: ${allMessages.length})`);
2761
+ // Check if there are more results
2762
+ const totalHits = result.value?.[0]?.hitsContainers?.[0]?.total || 0;
2763
+ from += pageSize;
2764
+ // Safety check to prevent infinite loops
2765
+ if (pageCount >= maxPages || messages.length < pageSize) {
2766
+ logger.log(`āš ļø Reached end of results or maximum page limit. Stopping pagination.`);
2767
+ break;
2768
+ }
2769
+ } while (true);
2770
+ logger.log(`šŸ“§ KQL Search completed: ${allMessages.length} total results from ${pageCount} pages`);
2771
+ return allMessages;
2772
+ }
2773
+ catch (error) {
2774
+ logger.log(`āŒ KQL Search failed: ${error}. Falling back to filter search.`);
2775
+ return [];
2776
+ }
2777
+ }
2778
+ /**
2779
+ * Get ALL results using basic search with pagination
2780
+ */
2781
+ async performBasicSearchAll(criteria) {
2782
+ logger.log('šŸ”„ Using fallback basic search to get ALL results');
2783
+ const graphClient = await this.getGraphClient();
2784
+ const allMessages = [];
2785
+ let nextLink = null;
2786
+ let pageCount = 0;
2787
+ const maxPages = 50; // Safety limit
2788
+ do {
2789
+ pageCount++;
2790
+ logger.log(`šŸ” Basic Search - Fetching page ${pageCount}`);
2791
+ let apiCall = graphClient
2792
+ .api('/me/messages')
2793
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
2794
+ .orderby('receivedDateTime desc')
2795
+ .top(999);
2796
+ // Use nextLink if available for pagination
2797
+ if (nextLink) {
2798
+ apiCall = graphClient.api(nextLink);
2799
+ }
2800
+ const result = await apiCall.get();
2801
+ const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
2802
+ allMessages.push(...messages);
2803
+ logger.log(`šŸ“§ Basic Search page ${pageCount}: ${messages.length} results (total: ${allMessages.length})`);
2804
+ // Get next page link
2805
+ nextLink = result['@odata.nextLink'] || null;
2806
+ // Safety check to prevent infinite loops
2807
+ if (pageCount >= maxPages) {
2808
+ logger.log(`āš ļø Reached maximum page limit (${maxPages}). Stopping pagination.`);
2809
+ break;
2810
+ }
2811
+ } while (nextLink);
2812
+ // Apply manual filtering to all results
2813
+ const filteredMessages = this.applyManualFiltering(allMessages, criteria);
2814
+ logger.log(`šŸ“§ Basic Search completed: ${filteredMessages.length} filtered results from ${allMessages.length} total emails`);
2815
+ return {
2816
+ messages: filteredMessages,
2817
+ hasMore: false // We got all results
2818
+ };
2819
+ }
2820
+ /**
2821
+ * Get ALL results using partial text search with pagination
2822
+ */
2823
+ async performPartialTextSearchAll(criteria) {
2824
+ try {
2825
+ const graphClient = await this.getGraphClient();
2826
+ const allMessages = [];
2827
+ let nextLink = null;
2828
+ let pageCount = 0;
2829
+ const maxPages = 20; // Safety limit
2830
+ do {
2831
+ pageCount++;
2832
+ logger.log(`šŸ” Partial Text Search - Fetching page ${pageCount}`);
2833
+ let apiCall = graphClient
2834
+ .api('/me/messages')
2835
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
2836
+ .orderby('receivedDateTime desc')
2837
+ .top(999);
2838
+ // Use nextLink if available for pagination
2839
+ if (nextLink) {
2840
+ apiCall = graphClient.api(nextLink);
2841
+ }
2842
+ const result = await apiCall.get();
2843
+ const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
2844
+ allMessages.push(...messages);
2845
+ logger.log(`šŸ“§ Partial Text Search page ${pageCount}: ${messages.length} results (total: ${allMessages.length})`);
2846
+ // Get next page link
2847
+ nextLink = result['@odata.nextLink'] || null;
2848
+ // Safety check to prevent infinite loops
2849
+ if (pageCount >= maxPages) {
2850
+ logger.log(`āš ļø Reached maximum page limit (${maxPages}). Stopping pagination.`);
2851
+ break;
2852
+ }
2853
+ } while (nextLink);
2854
+ // Apply very broad partial matching to all results
2855
+ const partialMatches = allMessages.filter(message => {
2856
+ let matches = true;
2857
+ // Very flexible text search
2858
+ if (criteria.query) {
2859
+ const searchTerms = criteria.query.toLowerCase().split(/\s+/);
2860
+ const searchableText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
2861
+ // At least 50% of search terms should match
2862
+ const matchingTerms = searchTerms.filter(term => searchableText.includes(term));
2863
+ matches = matchingTerms.length >= Math.ceil(searchTerms.length * 0.5);
2864
+ }
2865
+ // Very flexible sender search
2866
+ if (criteria.from && matches) {
2867
+ const fromTerm = criteria.from.toLowerCase();
2868
+ const fromText = `${message.from.name} ${message.from.address}`.toLowerCase();
2869
+ // Partial matching with character-level similarity
2870
+ matches = fromText.includes(fromTerm) ||
2871
+ fromTerm.split('').some(char => fromText.includes(char)) ||
2872
+ this.calculateStringSimilarity(fromTerm, fromText) > 0.3;
2873
+ }
2874
+ return matches;
2875
+ });
2876
+ logger.log(`šŸ“§ Partial text search completed: ${partialMatches.length} matches from ${allMessages.length} total emails`);
2877
+ return partialMatches;
2878
+ }
2879
+ catch (error) {
2880
+ logger.log(`āŒ Partial text search failed: ${error}`);
2881
+ return [];
2882
+ }
2883
+ }
2884
+ /**
2885
+ * Get ALL results using ultra-relaxed search with pagination
2886
+ */
2887
+ async performUltraRelaxedSearchAll(criteria) {
2888
+ try {
2889
+ const graphClient = await this.getGraphClient();
2890
+ const allEmails = [];
2891
+ // Search across multiple folders and time ranges
2892
+ const searches = [];
2893
+ // Search recent emails with pagination
2894
+ searches.push(this.getAllEmailsFromFolder(graphClient, '/me/messages'));
2895
+ // Search sent items if looking for specific people
2896
+ if (criteria.from || criteria.to) {
2897
+ searches.push(this.getAllEmailsFromFolder(graphClient, '/me/mailFolders/sentitems/messages'));
2898
+ }
2899
+ const results = await Promise.allSettled(searches);
2900
+ results.forEach((result, index) => {
2901
+ if (result.status === 'fulfilled') {
2902
+ allEmails.push(...result.value);
2903
+ logger.log(`šŸ“§ Ultra-relaxed search ${index + 1} found ${result.value.length} emails`);
2904
+ }
2905
+ });
2906
+ const uniqueEmails = this.removeDuplicateMessages(allEmails);
2907
+ logger.log(`šŸ“§ Ultra-relaxed search completed: ${uniqueEmails.length} unique emails`);
2908
+ return uniqueEmails;
2909
+ }
2910
+ catch (error) {
2911
+ logger.log(`āŒ Ultra-relaxed search failed: ${error}`);
2912
+ return [];
2913
+ }
2914
+ }
2915
+ /**
2916
+ * Helper method to get all emails from a specific folder with pagination
2917
+ */
2918
+ async getAllEmailsFromFolder(graphClient, folderPath) {
2919
+ const allMessages = [];
2920
+ let nextLink = null;
2921
+ let pageCount = 0;
2922
+ const maxPages = 20; // Safety limit
2923
+ do {
2924
+ pageCount++;
2925
+ logger.log(`šŸ” Getting emails from ${folderPath} - page ${pageCount}`);
2926
+ let apiCall = graphClient
2927
+ .api(folderPath)
2928
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
2929
+ .orderby('receivedDateTime desc')
2930
+ .top(999);
2931
+ // Use nextLink if available for pagination
2932
+ if (nextLink) {
2933
+ apiCall = graphClient.api(nextLink);
2934
+ }
2935
+ const result = await apiCall.get();
2936
+ const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
2937
+ allMessages.push(...messages);
2938
+ // Get next page link
2939
+ nextLink = result['@odata.nextLink'] || null;
2940
+ // Safety check to prevent infinite loops
2941
+ if (pageCount >= maxPages) {
2942
+ logger.log(`āš ļø Reached maximum page limit (${maxPages}). Stopping pagination.`);
2943
+ break;
2944
+ }
2945
+ } while (nextLink);
2946
+ return allMessages;
2947
+ }
2948
+ /**
2949
+ * Search emails in batches of 50, looping up to 5 times to get more results efficiently
2950
+ */
2951
+ async searchEmailsBatched(criteria = {}, batchSize = 50, maxBatches = 5) {
2952
+ return await this.executeWithAuth(async () => {
2953
+ const graphClient = await this.getGraphClient();
2954
+ logger.log(`šŸ” Starting batched email search: ${batchSize} per batch, max ${maxBatches} batches`);
2955
+ logger.log(`šŸ” Search criteria:`, JSON.stringify(criteria, null, 2));
2956
+ const allMessages = [];
2957
+ let currentBatch = 0;
2958
+ let hasMoreResults = true;
2959
+ const startTime = Date.now();
2960
+ const batchTimeout = 2 * 60 * 1000; // 2 minutes per batch
2961
+ try {
2962
+ while (currentBatch < maxBatches && hasMoreResults && (Date.now() - startTime) < batchTimeout) {
2963
+ currentBatch++;
2964
+ logger.log(`šŸ” Processing batch ${currentBatch}/${maxBatches}`);
2965
+ // Create criteria for this batch
2966
+ const batchCriteria = {
2967
+ ...criteria,
2968
+ maxResults: batchSize
2969
+ };
2970
+ // Search for this batch
2971
+ const batchResult = await this.searchEmails(batchCriteria);
2972
+ const batchMessages = batchResult.messages;
2973
+ logger.log(`šŸ“§ Batch ${currentBatch}: Found ${batchMessages.length} results`);
2974
+ if (batchMessages.length > 0) {
2975
+ // Add new messages (avoid duplicates)
2976
+ const newMessages = batchMessages.filter(msg => !allMessages.some(existing => existing.id === msg.id));
2977
+ allMessages.push(...newMessages);
2978
+ logger.log(`šŸ“§ Batch ${currentBatch}: Added ${newMessages.length} new unique messages (total: ${allMessages.length})`);
2979
+ // Check if we have more results
2980
+ hasMoreResults = batchResult.hasMore && batchMessages.length === batchSize;
2981
+ }
2982
+ else {
2983
+ // No more results found
2984
+ hasMoreResults = false;
2985
+ logger.log(`šŸ“§ Batch ${currentBatch}: No more results found`);
2986
+ }
2987
+ // Small delay between batches to avoid rate limiting
2988
+ if (hasMoreResults && currentBatch < maxBatches) {
2989
+ await new Promise(resolve => setTimeout(resolve, 1000));
2990
+ }
2991
+ }
2992
+ const totalTime = (Date.now() - startTime) / 1000;
2993
+ logger.log(`šŸ” Batched search completed: ${allMessages.length} total results from ${currentBatch} batches in ${totalTime} seconds`);
2994
+ return {
2995
+ messages: allMessages,
2996
+ hasMore: hasMoreResults,
2997
+ totalBatches: currentBatch
2998
+ };
2999
+ }
3000
+ catch (error) {
3001
+ logger.error('Error in batched email search:', error);
3002
+ return {
3003
+ messages: allMessages,
3004
+ hasMore: false,
3005
+ totalBatches: currentBatch
3006
+ };
3007
+ }
3008
+ }, 'searchEmailsBatched');
3009
+ }
2368
3010
  }
2369
3011
  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.14",
3
+ "version": "1.1.15",
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",