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 +86 -8
- package/dist/utils/ms365-operations.js +780 -138
- package/package.json +1 -1
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.
|
|
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 (
|
|
277
|
-
minimum:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|
|
970
|
-
const filterResults = await this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
1031
|
-
const basicResult = await this.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
1064
|
-
const
|
|
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:
|
|
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: ${
|
|
1071
|
-
if (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1252
|
+
const escapedTo = criteria.to.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1253
|
+
kqlParts.push(`to:${escapedTo}`);
|
|
1196
1254
|
}
|
|
1197
1255
|
if (criteria.cc) {
|
|
1198
|
-
|
|
1256
|
+
const escapedCc = criteria.cc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1257
|
+
kqlParts.push(`cc:${escapedCc}`);
|
|
1199
1258
|
}
|
|
1200
1259
|
if (criteria.subject) {
|
|
1201
|
-
|
|
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
|
-
|
|
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
|
|
1282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
apiCall
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
apiCall.
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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 =
|
|
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
|
|
1962
|
-
if (additionalCriteria.
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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:
|
|
1985
|
-
hasMore:
|
|
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
|
|
1992
|
-
|
|
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
|
|
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.
|
|
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",
|