ms365-mcp-server 1.1.16 → 1.1.17
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 +1122 -9
- package/dist/utils/batch-performance-monitor.js +106 -0
- package/dist/utils/batch-test-scenarios.js +277 -0
- package/dist/utils/context-aware-search.js +499 -0
- package/dist/utils/cross-reference-detector.js +352 -0
- package/dist/utils/document-workflow.js +433 -0
- package/dist/utils/enhanced-fuzzy-search.js +514 -0
- package/dist/utils/error-handler.js +337 -0
- package/dist/utils/intelligence-engine.js +71 -0
- package/dist/utils/intelligent-cache.js +379 -0
- package/dist/utils/large-mailbox-search.js +599 -0
- package/dist/utils/ms365-operations.js +657 -181
- package/dist/utils/performance-monitor.js +395 -0
- package/dist/utils/proactive-intelligence.js +390 -0
- package/dist/utils/rate-limiter.js +284 -0
- package/dist/utils/search-batch-pipeline.js +222 -0
- package/dist/utils/thread-reconstruction.js +700 -0
- package/package.json +1 -1
|
@@ -116,47 +116,36 @@ export class MS365Operations {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
|
-
* Build filter query
|
|
119
|
+
* Build OData filter query (STRICT - only filterable fields)
|
|
120
|
+
* CRITICAL: Cannot mix with $search operations per Graph API limitations
|
|
120
121
|
*/
|
|
121
122
|
buildFilterQuery(criteria) {
|
|
122
123
|
const filters = [];
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
// ✅ SAFE: from/emailAddress fields are filterable
|
|
125
|
+
if (criteria.from && criteria.from.includes('@')) {
|
|
126
|
+
// Only exact email matches in filters - names require $search
|
|
125
127
|
const escapedFrom = this.escapeODataValue(criteria.from);
|
|
126
|
-
|
|
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
|
-
}
|
|
128
|
+
filters.push(`from/emailAddress/address eq '${escapedFrom}'`);
|
|
134
129
|
}
|
|
130
|
+
// ✅ SAFE: toRecipients/ccRecipients with proper any() syntax
|
|
135
131
|
if (criteria.to && criteria.to.includes('@')) {
|
|
136
|
-
// Properly escape single quotes in email addresses
|
|
137
132
|
const escapedTo = this.escapeODataValue(criteria.to);
|
|
138
133
|
filters.push(`toRecipients/any(r: r/emailAddress/address eq '${escapedTo}')`);
|
|
139
134
|
}
|
|
140
135
|
if (criteria.cc && criteria.cc.includes('@')) {
|
|
141
|
-
// Properly escape single quotes in email addresses
|
|
142
136
|
const escapedCc = this.escapeODataValue(criteria.cc);
|
|
143
137
|
filters.push(`ccRecipients/any(r: r/emailAddress/address eq '${escapedCc}')`);
|
|
144
138
|
}
|
|
145
|
-
|
|
146
|
-
// Properly escape single quotes in subject
|
|
147
|
-
const escapedSubject = this.escapeODataValue(criteria.subject);
|
|
148
|
-
filters.push(`contains(subject,'${escapedSubject}')`);
|
|
149
|
-
}
|
|
139
|
+
// ✅ SAFE: Date filters with proper ISO 8601 format
|
|
150
140
|
if (criteria.after) {
|
|
151
|
-
// Fix date formatting - use proper ISO format with quotes
|
|
152
141
|
const afterDate = this.formatDateForOData(criteria.after);
|
|
153
142
|
filters.push(`receivedDateTime ge '${afterDate}'`);
|
|
154
143
|
}
|
|
155
144
|
if (criteria.before) {
|
|
156
|
-
// Fix date formatting - use proper ISO format with quotes
|
|
157
145
|
const beforeDate = this.formatDateForOData(criteria.before);
|
|
158
146
|
filters.push(`receivedDateTime le '${beforeDate}'`);
|
|
159
147
|
}
|
|
148
|
+
// ✅ SAFE: Boolean filters
|
|
160
149
|
if (criteria.hasAttachment !== undefined) {
|
|
161
150
|
filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
|
|
162
151
|
}
|
|
@@ -166,57 +155,57 @@ export class MS365Operations {
|
|
|
166
155
|
if (criteria.importance) {
|
|
167
156
|
filters.push(`importance eq '${criteria.importance}'`);
|
|
168
157
|
}
|
|
158
|
+
// ❌ REMOVED: subject filtering - must use $search for subject content
|
|
159
|
+
// ❌ REMOVED: from name filtering - must use $search for partial names
|
|
169
160
|
return filters.join(' and ');
|
|
170
161
|
}
|
|
171
162
|
/**
|
|
172
|
-
* Build search query for Microsoft Graph API
|
|
163
|
+
* Build search query for Microsoft Graph API (STRICT - only searchable fields)
|
|
164
|
+
* CRITICAL: Cannot mix with $filter operations per Graph API limitations
|
|
165
|
+
* $search ONLY works on: subject, body, from (not to, cc, categories, etc.)
|
|
173
166
|
*/
|
|
174
167
|
buildSearchQuery(criteria) {
|
|
175
168
|
const searchTerms = [];
|
|
169
|
+
// ✅ SAFE: General text search (searches subject, body, from automatically)
|
|
176
170
|
if (criteria.query) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (criteria.from) {
|
|
182
|
-
// Check if it looks like an email address for exact matching
|
|
183
|
-
if (criteria.from.includes('@')) {
|
|
184
|
-
emailSearchTerms.push(`from:${criteria.from}`);
|
|
171
|
+
// Escape quotes and use proper search syntax
|
|
172
|
+
const escapedQuery = criteria.query.replace(/"/g, '\\"');
|
|
173
|
+
if (escapedQuery.includes(' ')) {
|
|
174
|
+
searchTerms.push(`"${escapedQuery}"`);
|
|
185
175
|
}
|
|
186
176
|
else {
|
|
187
|
-
|
|
188
|
-
// This will trigger manual filtering which supports partial names better
|
|
189
|
-
if (criteria.query) {
|
|
190
|
-
searchTerms.push(`${criteria.query} AND ${criteria.from}`);
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
searchTerms.push(criteria.from);
|
|
194
|
-
}
|
|
177
|
+
searchTerms.push(escapedQuery);
|
|
195
178
|
}
|
|
196
179
|
}
|
|
197
|
-
|
|
198
|
-
emailSearchTerms.push(`to:${criteria.to}`);
|
|
199
|
-
}
|
|
200
|
-
if (criteria.cc) {
|
|
201
|
-
emailSearchTerms.push(`cc:${criteria.cc}`);
|
|
202
|
-
}
|
|
203
|
-
// Combine email search terms with OR logic
|
|
204
|
-
if (emailSearchTerms.length > 0) {
|
|
205
|
-
searchTerms.push(emailSearchTerms.join(' OR '));
|
|
206
|
-
}
|
|
180
|
+
// ✅ SAFE: Subject search (explicit field supported by $search)
|
|
207
181
|
if (criteria.subject) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (subjectTerm.includes(' ') || subjectTerm.includes('"')) {
|
|
211
|
-
// If contains spaces or quotes, wrap in quotes and escape any internal quotes
|
|
212
|
-
const escapedSubject = subjectTerm.replace(/"/g, '\\"');
|
|
182
|
+
const escapedSubject = criteria.subject.replace(/"/g, '\\"');
|
|
183
|
+
if (escapedSubject.includes(' ')) {
|
|
213
184
|
searchTerms.push(`subject:"${escapedSubject}"`);
|
|
214
185
|
}
|
|
215
186
|
else {
|
|
216
|
-
|
|
217
|
-
searchTerms.push(`subject:${subjectTerm}`);
|
|
187
|
+
searchTerms.push(`subject:${escapedSubject}`);
|
|
218
188
|
}
|
|
219
189
|
}
|
|
190
|
+
// ✅ SAFE: From search (supported by $search for names and emails)
|
|
191
|
+
if (criteria.from) {
|
|
192
|
+
const escapedFrom = criteria.from.replace(/"/g, '\\"');
|
|
193
|
+
if (criteria.from.includes('@')) {
|
|
194
|
+
// Exact email search
|
|
195
|
+
searchTerms.push(`from:${escapedFrom}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Name search - use quotes for multi-word names
|
|
199
|
+
if (escapedFrom.includes(' ')) {
|
|
200
|
+
searchTerms.push(`from:"${escapedFrom}"`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
searchTerms.push(`from:${escapedFrom}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ❌ REMOVED: to/cc searches - NOT supported by $search
|
|
208
|
+
// These will be handled by manual filtering after retrieval
|
|
220
209
|
return searchTerms.join(' AND ');
|
|
221
210
|
}
|
|
222
211
|
/**
|
|
@@ -988,12 +977,12 @@ ${originalBodyContent}
|
|
|
988
977
|
}
|
|
989
978
|
}
|
|
990
979
|
/**
|
|
991
|
-
*
|
|
980
|
+
* GRAPH API COMPLIANT SEARCH - Respects Microsoft's strict limitations
|
|
981
|
+
* Strategy: Use EITHER $filter OR $search, never both
|
|
992
982
|
*/
|
|
993
983
|
async searchEmails(criteria = {}) {
|
|
994
984
|
return await this.executeWithAuth(async () => {
|
|
995
|
-
|
|
996
|
-
logger.log(`🔍 Starting email search with criteria:`, JSON.stringify(criteria, null, 2));
|
|
985
|
+
logger.log(`🔍 GRAPH API COMPLIANT SEARCH with criteria:`, JSON.stringify(criteria, null, 2));
|
|
997
986
|
// Create cache key from criteria
|
|
998
987
|
const cacheKey = JSON.stringify(criteria);
|
|
999
988
|
const cachedResults = this.getCachedResults(cacheKey);
|
|
@@ -1001,99 +990,214 @@ ${originalBodyContent}
|
|
|
1001
990
|
logger.log('📦 Returning cached results');
|
|
1002
991
|
return cachedResults;
|
|
1003
992
|
}
|
|
1004
|
-
|
|
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;
|
|
993
|
+
const maxResults = criteria.maxResults || 50;
|
|
1008
994
|
let allMessages = [];
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
995
|
+
// STRATEGY 1: Use $filter for structured queries (exact matches, dates, booleans)
|
|
996
|
+
const hasFilterableFields = !!((criteria.from && criteria.from.includes('@')) ||
|
|
997
|
+
(criteria.to && criteria.to.includes('@')) ||
|
|
998
|
+
(criteria.cc && criteria.cc.includes('@')) ||
|
|
999
|
+
criteria.after ||
|
|
1000
|
+
criteria.before ||
|
|
1001
|
+
criteria.hasAttachment !== undefined ||
|
|
1002
|
+
criteria.isUnread !== undefined ||
|
|
1003
|
+
criteria.importance);
|
|
1004
|
+
// STRATEGY 2: Use $search for text searches (subject, body content, from names)
|
|
1005
|
+
const hasSearchableFields = !!(criteria.query ||
|
|
1006
|
+
criteria.subject ||
|
|
1007
|
+
(criteria.from && !criteria.from.includes('@')) // name searches
|
|
1008
|
+
);
|
|
1017
1009
|
try {
|
|
1018
|
-
//
|
|
1019
|
-
if (
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
}
|
|
1028
|
-
//
|
|
1029
|
-
if (
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
// Strategy 3: Quick partial search if still no results
|
|
1039
|
-
if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
|
|
1040
|
-
searchAttempts++;
|
|
1041
|
-
logger.log(`🔍 Attempt ${searchAttempts}: Using quick partial search`);
|
|
1042
|
-
const partialResults = await this.performPartialTextSearch(criteria, criteria.maxResults || 50);
|
|
1043
|
-
allMessages.push(...partialResults);
|
|
1044
|
-
if (allMessages.length > 0) {
|
|
1045
|
-
logger.log(`✅ Found ${allMessages.length} results with quick partial search`);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
// Check if we hit the timeout
|
|
1049
|
-
if ((Date.now() - startTime) >= searchTimeout) {
|
|
1050
|
-
logger.log(`⚠️ Search timeout reached after ${(Date.now() - startTime) / 1000} seconds. Returning partial results.`);
|
|
1010
|
+
// STRATEGY A: Pure Filter Strategy (when no search fields present)
|
|
1011
|
+
if (hasFilterableFields && !hasSearchableFields) {
|
|
1012
|
+
logger.log('🔍 Using PURE FILTER strategy (structured queries only)');
|
|
1013
|
+
allMessages = await this.performPureFilterSearch(criteria, maxResults);
|
|
1014
|
+
}
|
|
1015
|
+
// STRATEGY B: Pure Search Strategy (when no filter fields present)
|
|
1016
|
+
else if (hasSearchableFields && !hasFilterableFields) {
|
|
1017
|
+
logger.log('🔍 Using PURE SEARCH strategy (text search only)');
|
|
1018
|
+
allMessages = await this.performPureSearchQuery(criteria, maxResults);
|
|
1019
|
+
}
|
|
1020
|
+
// STRATEGY C: Hybrid Strategy (filter first, then search within results)
|
|
1021
|
+
else if (hasFilterableFields && hasSearchableFields) {
|
|
1022
|
+
logger.log('🔍 Using HYBRID strategy (filter first, then search within results)');
|
|
1023
|
+
allMessages = await this.performHybridFilterThenSearch(criteria, maxResults);
|
|
1024
|
+
}
|
|
1025
|
+
// STRATEGY D: Fallback to basic list with manual filtering
|
|
1026
|
+
else {
|
|
1027
|
+
logger.log('🔍 Using FALLBACK strategy (basic list with manual filtering)');
|
|
1028
|
+
const basicResult = await this.performBasicSearch(criteria);
|
|
1029
|
+
allMessages = basicResult.messages;
|
|
1051
1030
|
}
|
|
1052
|
-
//
|
|
1053
|
-
const
|
|
1054
|
-
logger.log(`📧 After deduplication: ${uniqueMessages.length} unique messages`);
|
|
1055
|
-
const filteredMessages = this.applyAdvancedFiltering(uniqueMessages, criteria);
|
|
1056
|
-
logger.log(`📧 After advanced filtering: ${filteredMessages.length} filtered messages`);
|
|
1031
|
+
// Apply manual filtering for unsupported fields (to/cc names, complex logic)
|
|
1032
|
+
const filteredMessages = this.applyManualFiltering(allMessages, criteria);
|
|
1057
1033
|
// Sort by relevance and date
|
|
1058
1034
|
const sortedMessages = this.sortSearchResults(filteredMessages, criteria);
|
|
1059
|
-
//
|
|
1060
|
-
|
|
1061
|
-
logger.log(`🔍 Final attempt: Ultra-relaxed search for any partial matches across ALL emails`);
|
|
1062
|
-
const ultraRelaxedResults = await this.performUltraRelaxedSearchAll(criteria);
|
|
1063
|
-
const finalFiltered = this.applyUltraRelaxedFiltering(ultraRelaxedResults, criteria);
|
|
1064
|
-
if (finalFiltered.length > 0) {
|
|
1065
|
-
logger.log(`✅ Ultra-relaxed search found ${finalFiltered.length} results`);
|
|
1066
|
-
// Apply maxResults limit if specified
|
|
1067
|
-
const finalMessages = maxResults > 0 ? finalFiltered.slice(0, maxResults) : finalFiltered.slice(0, safetyLimit);
|
|
1068
|
-
const searchResult = {
|
|
1069
|
-
messages: finalMessages,
|
|
1070
|
-
hasMore: maxResults > 0 ? finalFiltered.length > maxResults : finalFiltered.length > safetyLimit
|
|
1071
|
-
};
|
|
1072
|
-
this.setCachedResults(cacheKey, searchResult);
|
|
1073
|
-
return searchResult;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
// Apply maxResults limit if specified, otherwise return all results (with safety limit)
|
|
1077
|
-
const finalMessages = maxResults > 0 ? sortedMessages.slice(0, maxResults) : sortedMessages.slice(0, safetyLimit);
|
|
1035
|
+
// Apply maxResults limit
|
|
1036
|
+
const finalMessages = sortedMessages.slice(0, maxResults);
|
|
1078
1037
|
const searchResult = {
|
|
1079
1038
|
messages: finalMessages,
|
|
1080
|
-
hasMore:
|
|
1039
|
+
hasMore: sortedMessages.length > maxResults
|
|
1081
1040
|
};
|
|
1082
1041
|
this.setCachedResults(cacheKey, searchResult);
|
|
1083
|
-
logger.log(`🔍
|
|
1084
|
-
if (finalMessages.length === 0) {
|
|
1085
|
-
logger.log(`❌ No results found despite ${searchAttempts} search strategies. Consider broadening search criteria.`);
|
|
1086
|
-
}
|
|
1042
|
+
logger.log(`🔍 COMPLIANT SEARCH completed: ${finalMessages.length} results`);
|
|
1087
1043
|
return searchResult;
|
|
1088
1044
|
}
|
|
1089
1045
|
catch (error) {
|
|
1090
|
-
logger.error('Error in
|
|
1091
|
-
// Final fallback
|
|
1092
|
-
|
|
1093
|
-
return await this.performBasicSearchAll(criteria);
|
|
1046
|
+
logger.error('Error in compliant email search:', error);
|
|
1047
|
+
// Final fallback
|
|
1048
|
+
return await this.performBasicSearch(criteria);
|
|
1094
1049
|
}
|
|
1095
1050
|
}, 'searchEmails');
|
|
1096
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* PURE FILTER SEARCH - Only uses $filter, never $search
|
|
1054
|
+
* Use for: exact email matches, dates, booleans, importance
|
|
1055
|
+
*/
|
|
1056
|
+
async performPureFilterSearch(criteria, maxResults) {
|
|
1057
|
+
try {
|
|
1058
|
+
const graphClient = await this.getGraphClient();
|
|
1059
|
+
const filterString = this.buildFilterQuery(criteria);
|
|
1060
|
+
if (!filterString) {
|
|
1061
|
+
logger.log('⚠️ No valid filters for pure filter search');
|
|
1062
|
+
return [];
|
|
1063
|
+
}
|
|
1064
|
+
logger.log(`🔍 Pure Filter Query: ${filterString}`);
|
|
1065
|
+
let apiCall = graphClient
|
|
1066
|
+
.api('/me/messages')
|
|
1067
|
+
.filter(filterString)
|
|
1068
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1069
|
+
.orderby('receivedDateTime desc')
|
|
1070
|
+
.top(Math.min(maxResults, 999));
|
|
1071
|
+
const result = await apiCall.get();
|
|
1072
|
+
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
1073
|
+
logger.log(`📧 Pure Filter Search returned ${messages.length} results`);
|
|
1074
|
+
return messages;
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
logger.log(`❌ Pure Filter Search failed: ${error}`);
|
|
1078
|
+
return [];
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* PURE SEARCH QUERY - Only uses $search, never $filter
|
|
1083
|
+
* Use for: text searches in subject, body, from names
|
|
1084
|
+
*/
|
|
1085
|
+
async performPureSearchQuery(criteria, maxResults) {
|
|
1086
|
+
try {
|
|
1087
|
+
const graphClient = await this.getGraphClient();
|
|
1088
|
+
const searchString = this.buildSearchQuery(criteria);
|
|
1089
|
+
if (!searchString) {
|
|
1090
|
+
logger.log('⚠️ No valid search terms for pure search query');
|
|
1091
|
+
return [];
|
|
1092
|
+
}
|
|
1093
|
+
logger.log(`🔍 Pure Search Query: ${searchString}`);
|
|
1094
|
+
// Use Graph Search API for pure text searches
|
|
1095
|
+
const searchRequest = {
|
|
1096
|
+
requests: [
|
|
1097
|
+
{
|
|
1098
|
+
entityTypes: ['message'],
|
|
1099
|
+
query: {
|
|
1100
|
+
queryString: searchString
|
|
1101
|
+
},
|
|
1102
|
+
from: 0,
|
|
1103
|
+
size: Math.min(maxResults, 1000),
|
|
1104
|
+
fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
|
|
1105
|
+
}
|
|
1106
|
+
]
|
|
1107
|
+
};
|
|
1108
|
+
const result = await graphClient
|
|
1109
|
+
.api('/search/query')
|
|
1110
|
+
.post(searchRequest);
|
|
1111
|
+
const messages = [];
|
|
1112
|
+
if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
|
|
1113
|
+
for (const hit of result.value[0].hitsContainers[0].hits) {
|
|
1114
|
+
messages.push(this.mapEmailResult(hit.resource));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
logger.log(`📧 Pure Search Query returned ${messages.length} results`);
|
|
1118
|
+
return messages;
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
logger.log(`❌ Pure Search Query failed: ${error}`);
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* HYBRID STRATEGY - Filter first, then search within results
|
|
1127
|
+
* Step 1: Use $filter to narrow down results structurally
|
|
1128
|
+
* Step 2: Apply manual text filtering to remaining results
|
|
1129
|
+
*/
|
|
1130
|
+
async performHybridFilterThenSearch(criteria, maxResults) {
|
|
1131
|
+
try {
|
|
1132
|
+
// Step 1: Create filter-only criteria
|
|
1133
|
+
const filterCriteria = {
|
|
1134
|
+
from: criteria.from?.includes('@') ? criteria.from : undefined,
|
|
1135
|
+
to: criteria.to?.includes('@') ? criteria.to : undefined,
|
|
1136
|
+
cc: criteria.cc?.includes('@') ? criteria.cc : undefined,
|
|
1137
|
+
after: criteria.after,
|
|
1138
|
+
before: criteria.before,
|
|
1139
|
+
hasAttachment: criteria.hasAttachment,
|
|
1140
|
+
isUnread: criteria.isUnread,
|
|
1141
|
+
importance: criteria.importance,
|
|
1142
|
+
maxResults: maxResults * 3 // Get more results for subsequent filtering
|
|
1143
|
+
};
|
|
1144
|
+
// Get filtered results
|
|
1145
|
+
const filteredResults = await this.performPureFilterSearch(filterCriteria, maxResults * 3);
|
|
1146
|
+
if (filteredResults.length === 0) {
|
|
1147
|
+
logger.log('🔍 Hybrid: No results from filter step, trying pure search');
|
|
1148
|
+
return await this.performPureSearchQuery(criteria, maxResults);
|
|
1149
|
+
}
|
|
1150
|
+
logger.log(`🔍 Hybrid: Filter step returned ${filteredResults.length} results, now applying search`);
|
|
1151
|
+
// Step 2: Apply search criteria manually to filtered results
|
|
1152
|
+
const searchCriteria = {
|
|
1153
|
+
query: criteria.query,
|
|
1154
|
+
subject: criteria.subject,
|
|
1155
|
+
from: criteria.from?.includes('@') ? undefined : criteria.from, // Only names for search
|
|
1156
|
+
};
|
|
1157
|
+
const finalResults = this.applyManualSearchFiltering(filteredResults, searchCriteria);
|
|
1158
|
+
logger.log(`🔍 Hybrid: Final results after search filtering: ${finalResults.length}`);
|
|
1159
|
+
return finalResults.slice(0, maxResults);
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
logger.log(`❌ Hybrid search failed: ${error}`);
|
|
1163
|
+
return [];
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Apply manual search filtering for text-based criteria
|
|
1168
|
+
* Used in hybrid strategy to search within already filtered results
|
|
1169
|
+
*/
|
|
1170
|
+
applyManualSearchFiltering(messages, criteria) {
|
|
1171
|
+
return messages.filter(message => {
|
|
1172
|
+
// General query search across multiple fields
|
|
1173
|
+
if (criteria.query) {
|
|
1174
|
+
const searchText = criteria.query.toLowerCase().trim();
|
|
1175
|
+
const searchableContent = [
|
|
1176
|
+
message.subject,
|
|
1177
|
+
message.bodyPreview,
|
|
1178
|
+
message.from.name,
|
|
1179
|
+
message.from.address
|
|
1180
|
+
].join(' ').toLowerCase();
|
|
1181
|
+
const searchTerms = searchText.split(/\s+/).filter(term => term.length > 0);
|
|
1182
|
+
if (!searchTerms.every(term => searchableContent.includes(term))) {
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
// Subject search
|
|
1187
|
+
if (criteria.subject) {
|
|
1188
|
+
const subjectMatch = message.subject.toLowerCase().includes(criteria.subject.toLowerCase());
|
|
1189
|
+
if (!subjectMatch)
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
// From name search (not email address)
|
|
1193
|
+
if (criteria.from && !criteria.from.includes('@')) {
|
|
1194
|
+
const fromNameMatch = message.from.name.toLowerCase().includes(criteria.from.toLowerCase());
|
|
1195
|
+
if (!fromNameMatch)
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
return true;
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1097
1201
|
/**
|
|
1098
1202
|
* Resolve UNKNOWN message IDs by finding messages using alternative search criteria
|
|
1099
1203
|
*/
|
|
@@ -1355,23 +1459,10 @@ ${originalBodyContent}
|
|
|
1355
1459
|
logger.log(`🔍 Applying OData filter: ${filterString}`);
|
|
1356
1460
|
apiCall = apiCall.filter(filterString);
|
|
1357
1461
|
}
|
|
1358
|
-
// Apply folder filter using
|
|
1462
|
+
// Apply folder filter using optimized folder search
|
|
1359
1463
|
if (criteria.folder) {
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
logger.log(`🔍 Switching to folder-specific search: ${folders[0].displayName} (${folders[0].id})`);
|
|
1363
|
-
apiCall = graphClient
|
|
1364
|
-
.api(`/me/mailFolders/${folders[0].id}/messages`)
|
|
1365
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1366
|
-
.orderby('receivedDateTime desc')
|
|
1367
|
-
.top(Math.min(maxResults, 999));
|
|
1368
|
-
// Re-apply filters for folder-specific search
|
|
1369
|
-
if (filters.length > 0) {
|
|
1370
|
-
const filterString = filters.join(' and ');
|
|
1371
|
-
logger.log(`🔍 Applying OData filter to folder search: ${filterString}`);
|
|
1372
|
-
apiCall = apiCall.filter(filterString);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1464
|
+
logger.log(`🔍 Using optimized folder search for: ${criteria.folder}`);
|
|
1465
|
+
return await this.performOptimizedFolderSearch(criteria, maxResults);
|
|
1375
1466
|
}
|
|
1376
1467
|
const result = await apiCall.get();
|
|
1377
1468
|
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
@@ -2578,23 +2669,16 @@ ${originalBodyContent}
|
|
|
2578
2669
|
logger.log(`🔍 Applying OData filter: ${filterString}`);
|
|
2579
2670
|
apiCall = apiCall.filter(filterString);
|
|
2580
2671
|
}
|
|
2581
|
-
//
|
|
2672
|
+
// For folder searches, use optimized approach
|
|
2582
2673
|
if (criteria.folder) {
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
// Re-apply filters for folder-specific search
|
|
2592
|
-
if (filters.length > 0) {
|
|
2593
|
-
const filterString = filters.join(' and ');
|
|
2594
|
-
logger.log(`🔍 Applying OData filter to folder search: ${filterString}`);
|
|
2595
|
-
apiCall = apiCall.filter(filterString);
|
|
2596
|
-
}
|
|
2597
|
-
}
|
|
2674
|
+
logger.log(`🔍 Using optimized folder search (ALL mode) for: ${criteria.folder}`);
|
|
2675
|
+
// Use optimized search but allow unlimited results
|
|
2676
|
+
const originalMaxResults = criteria.maxResults;
|
|
2677
|
+
criteria.maxResults = 9999; // Set high limit for "ALL" mode
|
|
2678
|
+
const result = await this.performOptimizedFolderSearch(criteria, 9999);
|
|
2679
|
+
criteria.maxResults = originalMaxResults; // Restore original
|
|
2680
|
+
allMessages.push(...result);
|
|
2681
|
+
break; // Exit the pagination loop since we got the results
|
|
2598
2682
|
}
|
|
2599
2683
|
// Use nextLink if available for pagination
|
|
2600
2684
|
if (nextLink) {
|
|
@@ -2957,21 +3041,10 @@ ${originalBodyContent}
|
|
|
2957
3041
|
logger.log(`🔍 Fast OData filter: ${filterString}`);
|
|
2958
3042
|
apiCall = apiCall.filter(filterString);
|
|
2959
3043
|
}
|
|
2960
|
-
// Apply folder filter
|
|
3044
|
+
// Apply folder filter using optimized search
|
|
2961
3045
|
if (criteria.folder) {
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
logger.log(`🔍 Fast folder search: ${folders[0].displayName}`);
|
|
2965
|
-
apiCall = graphClient
|
|
2966
|
-
.api(`/me/mailFolders/${folders[0].id}/messages`)
|
|
2967
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
2968
|
-
.orderby('receivedDateTime desc')
|
|
2969
|
-
.top(criteria.maxResults || 50);
|
|
2970
|
-
if (filters.length > 0) {
|
|
2971
|
-
const filterString = filters.join(' and ');
|
|
2972
|
-
apiCall = apiCall.filter(filterString);
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
3046
|
+
logger.log(`🔍 Using optimized folder search (FAST mode) for: ${criteria.folder}`);
|
|
3047
|
+
return await this.performOptimizedFolderSearch(criteria, criteria.maxResults || 50);
|
|
2975
3048
|
}
|
|
2976
3049
|
const result = await apiCall.get();
|
|
2977
3050
|
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
@@ -3031,6 +3104,10 @@ ${originalBodyContent}
|
|
|
3031
3104
|
/**
|
|
3032
3105
|
* Search emails in batches of 50, looping up to 5 times to get more results efficiently
|
|
3033
3106
|
*/
|
|
3107
|
+
/**
|
|
3108
|
+
* GRAPH API COMPLIANT BATCH SEARCH - Respects Microsoft's strict limitations
|
|
3109
|
+
* Uses the same compliant strategy as regular search but in batches
|
|
3110
|
+
*/
|
|
3034
3111
|
async searchEmailsBatched(criteria = {}, batchSize = 50, maxBatches = 5) {
|
|
3035
3112
|
return await this.executeWithAuth(async () => {
|
|
3036
3113
|
const graphClient = await this.getGraphClient();
|
|
@@ -3111,5 +3188,404 @@ ${originalBodyContent}
|
|
|
3111
3188
|
}
|
|
3112
3189
|
}, 'searchEmailsBatched');
|
|
3113
3190
|
}
|
|
3191
|
+
/**
|
|
3192
|
+
* NATIVE MICROSOFT GRAPH JSON BATCHING
|
|
3193
|
+
* Uses the official $batch endpoint to combine up to 20 requests into a single HTTP call
|
|
3194
|
+
* Based on: https://learn.microsoft.com/en-us/graph/json-batching
|
|
3195
|
+
*/
|
|
3196
|
+
async batchRequests(requests) {
|
|
3197
|
+
return await this.executeWithAuth(async () => {
|
|
3198
|
+
if (requests.length === 0) {
|
|
3199
|
+
return new Map();
|
|
3200
|
+
}
|
|
3201
|
+
if (requests.length > 20) {
|
|
3202
|
+
throw new Error('Microsoft Graph batch limit is 20 requests. Use multiple batches for more requests.');
|
|
3203
|
+
}
|
|
3204
|
+
const graphClient = await this.getGraphClient();
|
|
3205
|
+
logger.log(`🔄 NATIVE BATCH: Processing ${requests.length} requests in single HTTP call`);
|
|
3206
|
+
const batchRequest = {
|
|
3207
|
+
requests: requests.map(req => ({
|
|
3208
|
+
id: req.id,
|
|
3209
|
+
method: req.method.toUpperCase(),
|
|
3210
|
+
url: req.url.startsWith('/') ? req.url : `/${req.url}`,
|
|
3211
|
+
headers: req.headers || {},
|
|
3212
|
+
body: req.body,
|
|
3213
|
+
dependsOn: req.dependsOn || []
|
|
3214
|
+
}))
|
|
3215
|
+
};
|
|
3216
|
+
const startTime = Date.now();
|
|
3217
|
+
const batchResponse = await graphClient
|
|
3218
|
+
.api('/$batch')
|
|
3219
|
+
.post(batchRequest);
|
|
3220
|
+
const processingTime = Date.now() - startTime;
|
|
3221
|
+
logger.log(`📊 NATIVE BATCH: Completed ${requests.length} requests in ${processingTime}ms (avg: ${(processingTime / requests.length).toFixed(2)}ms per request)`);
|
|
3222
|
+
// Process responses into a Map for easy access
|
|
3223
|
+
const results = new Map();
|
|
3224
|
+
if (batchResponse.responses) {
|
|
3225
|
+
for (const response of batchResponse.responses) {
|
|
3226
|
+
if (response.status >= 200 && response.status < 300) {
|
|
3227
|
+
results.set(response.id, {
|
|
3228
|
+
success: true,
|
|
3229
|
+
data: response.body,
|
|
3230
|
+
status: response.status,
|
|
3231
|
+
headers: response.headers
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
else {
|
|
3235
|
+
results.set(response.id, {
|
|
3236
|
+
success: false,
|
|
3237
|
+
error: response.body,
|
|
3238
|
+
status: response.status,
|
|
3239
|
+
headers: response.headers
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
return results;
|
|
3245
|
+
}, 'batchRequests');
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Get multiple emails efficiently using native JSON batching
|
|
3249
|
+
* Replaces the current manual batching approach
|
|
3250
|
+
*/
|
|
3251
|
+
async getEmailsBatch(messageIds, includeAttachments = false) {
|
|
3252
|
+
if (messageIds.length === 0) {
|
|
3253
|
+
return [];
|
|
3254
|
+
}
|
|
3255
|
+
const emails = [];
|
|
3256
|
+
const selectFields = 'id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink';
|
|
3257
|
+
// Process in batches of 20 (Graph API limit)
|
|
3258
|
+
for (let i = 0; i < messageIds.length; i += 20) {
|
|
3259
|
+
const batchIds = messageIds.slice(i, i + 20);
|
|
3260
|
+
// Create batch requests
|
|
3261
|
+
const requests = batchIds.map((id, index) => ({
|
|
3262
|
+
id: `email_${i + index}`,
|
|
3263
|
+
method: 'GET',
|
|
3264
|
+
url: `/me/messages/${id}?$select=${selectFields}`
|
|
3265
|
+
}));
|
|
3266
|
+
// Add attachment requests if needed
|
|
3267
|
+
if (includeAttachments) {
|
|
3268
|
+
const attachmentRequests = batchIds.map((id, index) => ({
|
|
3269
|
+
id: `attachments_${i + index}`,
|
|
3270
|
+
method: 'GET',
|
|
3271
|
+
url: `/me/messages/${id}/attachments`,
|
|
3272
|
+
dependsOn: [`email_${i + index}`] // Only run if email request succeeds
|
|
3273
|
+
}));
|
|
3274
|
+
requests.push(...attachmentRequests);
|
|
3275
|
+
}
|
|
3276
|
+
// Execute batch
|
|
3277
|
+
const batchResults = await this.batchRequests(requests);
|
|
3278
|
+
// Process email results
|
|
3279
|
+
for (let j = 0; j < batchIds.length; j++) {
|
|
3280
|
+
const emailResult = batchResults.get(`email_${i + j}`);
|
|
3281
|
+
if (emailResult?.success) {
|
|
3282
|
+
const emailInfo = this.mapEmailResult(emailResult.data);
|
|
3283
|
+
// Add attachments if requested
|
|
3284
|
+
if (includeAttachments) {
|
|
3285
|
+
const attachmentResult = batchResults.get(`attachments_${i + j}`);
|
|
3286
|
+
if (attachmentResult?.success) {
|
|
3287
|
+
emailInfo.attachments = attachmentResult.data.value?.map((att) => ({
|
|
3288
|
+
id: att.id,
|
|
3289
|
+
name: att.name,
|
|
3290
|
+
size: att.size,
|
|
3291
|
+
contentType: att.contentType,
|
|
3292
|
+
isInline: att.isInline || false
|
|
3293
|
+
})) || [];
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
emails.push(emailInfo);
|
|
3297
|
+
}
|
|
3298
|
+
else {
|
|
3299
|
+
logger.log(`❌ Failed to get email ${batchIds[j]}: ${emailResult?.error?.message || 'Unknown error'}`);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
return emails;
|
|
3304
|
+
}
|
|
3305
|
+
/**
|
|
3306
|
+
* Perform multiple email operations in a single batch
|
|
3307
|
+
* Examples: mark multiple emails as read, move multiple emails, etc.
|
|
3308
|
+
*/
|
|
3309
|
+
async batchEmailOperations(operations) {
|
|
3310
|
+
const requests = operations.map(op => {
|
|
3311
|
+
switch (op.operation) {
|
|
3312
|
+
case 'mark':
|
|
3313
|
+
return {
|
|
3314
|
+
id: op.id,
|
|
3315
|
+
method: 'PATCH',
|
|
3316
|
+
url: `/me/messages/${op.messageId}`,
|
|
3317
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3318
|
+
body: { isRead: op.params?.isRead ?? true }
|
|
3319
|
+
};
|
|
3320
|
+
case 'move':
|
|
3321
|
+
return {
|
|
3322
|
+
id: op.id,
|
|
3323
|
+
method: 'POST',
|
|
3324
|
+
url: `/me/messages/${op.messageId}/move`,
|
|
3325
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3326
|
+
body: { destinationId: op.params?.destinationFolderId }
|
|
3327
|
+
};
|
|
3328
|
+
case 'delete':
|
|
3329
|
+
return {
|
|
3330
|
+
id: op.id,
|
|
3331
|
+
method: 'DELETE',
|
|
3332
|
+
url: `/me/messages/${op.messageId}`
|
|
3333
|
+
};
|
|
3334
|
+
default:
|
|
3335
|
+
throw new Error(`Unknown operation: ${op.operation}`);
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
const batchResults = await this.batchRequests(requests);
|
|
3339
|
+
// Convert to operation results
|
|
3340
|
+
const results = new Map();
|
|
3341
|
+
for (const [id, result] of batchResults) {
|
|
3342
|
+
results.set(id, {
|
|
3343
|
+
success: result.success,
|
|
3344
|
+
error: result.success ? undefined : result.error?.message || 'Unknown error'
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
return results;
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* Enhanced folder and email info fetching using batching
|
|
3351
|
+
* Gets folder info and recent emails in parallel
|
|
3352
|
+
*/
|
|
3353
|
+
async getFolderWithRecentEmails(folderId, emailCount = 10) {
|
|
3354
|
+
const requests = [
|
|
3355
|
+
{
|
|
3356
|
+
id: 'folder_info',
|
|
3357
|
+
method: 'GET',
|
|
3358
|
+
url: `/me/mailFolders/${folderId}?$select=id,displayName,totalItemCount,unreadItemCount,parentFolderId`
|
|
3359
|
+
},
|
|
3360
|
+
{
|
|
3361
|
+
id: 'recent_emails',
|
|
3362
|
+
method: 'GET',
|
|
3363
|
+
url: `/me/mailFolders/${folderId}/messages?$select=id,subject,from,receivedDateTime,isRead,hasAttachments&$orderby=receivedDateTime desc&$top=${emailCount}`
|
|
3364
|
+
}
|
|
3365
|
+
];
|
|
3366
|
+
const batchResults = await this.batchRequests(requests);
|
|
3367
|
+
const folderResult = batchResults.get('folder_info');
|
|
3368
|
+
const emailsResult = batchResults.get('recent_emails');
|
|
3369
|
+
return {
|
|
3370
|
+
folder: folderResult?.success ? folderResult.data : null,
|
|
3371
|
+
emails: emailsResult?.success ?
|
|
3372
|
+
(emailsResult.data.value?.map((email) => this.mapEmailResult(email)) || []) :
|
|
3373
|
+
[]
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Perform optimized folder search for large folders
|
|
3378
|
+
*/
|
|
3379
|
+
async performOptimizedFolderSearch(criteria, maxResults) {
|
|
3380
|
+
try {
|
|
3381
|
+
const graphClient = await this.getGraphClient();
|
|
3382
|
+
// First, find the folder
|
|
3383
|
+
const folders = await this.findFolderByName(criteria.folder);
|
|
3384
|
+
if (folders.length === 0) {
|
|
3385
|
+
logger.log(`❌ Folder not found: ${criteria.folder}`);
|
|
3386
|
+
throw new Error(`Folder "${criteria.folder}" not found. Available folders can be checked with list_folders tool.`);
|
|
3387
|
+
}
|
|
3388
|
+
const targetFolder = folders[0];
|
|
3389
|
+
logger.log(`🔍 Searching in folder: ${targetFolder.displayName} (${targetFolder.totalItemCount} total emails)`);
|
|
3390
|
+
// Check if it's a large folder and adjust strategy
|
|
3391
|
+
const isLargeFolder = targetFolder.totalItemCount > 10000;
|
|
3392
|
+
if (isLargeFolder) {
|
|
3393
|
+
logger.log(`⚡ Large folder detected (${targetFolder.totalItemCount} emails). Using optimized search strategy.`);
|
|
3394
|
+
// For large folders, use recent-first strategy with filters
|
|
3395
|
+
return await this.performLargeFolderOptimizedSearch(targetFolder, criteria, maxResults);
|
|
3396
|
+
}
|
|
3397
|
+
else {
|
|
3398
|
+
// For smaller folders, use standard folder search
|
|
3399
|
+
return await this.performStandardFolderSearch(targetFolder, criteria, maxResults);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
catch (error) {
|
|
3403
|
+
logger.error(`Error in optimized folder search:`, error);
|
|
3404
|
+
throw error;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3408
|
+
* Optimized search for large folders (>10k emails)
|
|
3409
|
+
*/
|
|
3410
|
+
async performLargeFolderOptimizedSearch(folder, criteria, maxResults) {
|
|
3411
|
+
const graphClient = await this.getGraphClient();
|
|
3412
|
+
// Strategy 1: Search recent emails first (last 90 days)
|
|
3413
|
+
const recentDate = new Date();
|
|
3414
|
+
recentDate.setDate(recentDate.getDate() - 90);
|
|
3415
|
+
const recentDateStr = recentDate.toISOString().split('T')[0];
|
|
3416
|
+
logger.log(`🔍 Phase 1: Searching recent emails (since ${recentDateStr}) in large folder`);
|
|
3417
|
+
let apiCall = graphClient
|
|
3418
|
+
.api(`/me/mailFolders/${folder.id}/messages`)
|
|
3419
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
3420
|
+
.orderby('receivedDateTime desc')
|
|
3421
|
+
.top(Math.min(maxResults * 2, 500)); // Get more for filtering
|
|
3422
|
+
// Build filters for better performance
|
|
3423
|
+
const filters = [`receivedDateTime ge '${recentDateStr}T00:00:00Z'`];
|
|
3424
|
+
if (criteria.from) {
|
|
3425
|
+
const escapedFrom = this.escapeODataValue(criteria.from);
|
|
3426
|
+
if (criteria.from.includes('@')) {
|
|
3427
|
+
filters.push(`from/emailAddress/address eq '${escapedFrom}'`);
|
|
3428
|
+
}
|
|
3429
|
+
else {
|
|
3430
|
+
filters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
if (criteria.subject) {
|
|
3434
|
+
const escapedSubject = this.escapeODataValue(criteria.subject);
|
|
3435
|
+
filters.push(`contains(subject,'${escapedSubject}')`);
|
|
3436
|
+
}
|
|
3437
|
+
if (criteria.isUnread !== undefined) {
|
|
3438
|
+
filters.push(`isRead eq ${!criteria.isUnread}`);
|
|
3439
|
+
}
|
|
3440
|
+
if (criteria.hasAttachment !== undefined) {
|
|
3441
|
+
filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
|
|
3442
|
+
}
|
|
3443
|
+
const filterString = filters.join(' and ');
|
|
3444
|
+
logger.log(`🔍 Large folder filter: ${filterString}`);
|
|
3445
|
+
apiCall = apiCall.filter(filterString);
|
|
3446
|
+
const result = await apiCall.get();
|
|
3447
|
+
let messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
3448
|
+
// Apply additional text filtering
|
|
3449
|
+
if (criteria.query) {
|
|
3450
|
+
messages = messages.filter(message => {
|
|
3451
|
+
const searchText = criteria.query.toLowerCase();
|
|
3452
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
3453
|
+
return messageText.includes(searchText);
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
logger.log(`📧 Large folder search Phase 1 result: ${messages.length} emails found`);
|
|
3457
|
+
// If we have enough results or found good matches, return
|
|
3458
|
+
if (messages.length >= maxResults || messages.length > 5) {
|
|
3459
|
+
return messages.slice(0, maxResults);
|
|
3460
|
+
}
|
|
3461
|
+
// Strategy 2: If not enough results, search older emails (last 365 days) but with stricter filters
|
|
3462
|
+
logger.log(`🔍 Phase 2: Expanding search to older emails with stricter criteria`);
|
|
3463
|
+
const olderDate = new Date();
|
|
3464
|
+
olderDate.setDate(olderDate.getDate() - 365);
|
|
3465
|
+
const olderDateStr = olderDate.toISOString().split('T')[0];
|
|
3466
|
+
let olderApiCall = graphClient
|
|
3467
|
+
.api(`/me/mailFolders/${folder.id}/messages`)
|
|
3468
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
3469
|
+
.orderby('receivedDateTime desc')
|
|
3470
|
+
.top(300);
|
|
3471
|
+
// More restrictive filters for older search
|
|
3472
|
+
const olderFilters = [
|
|
3473
|
+
`receivedDateTime ge '${olderDateStr}T00:00:00Z'`,
|
|
3474
|
+
`receivedDateTime lt '${recentDateStr}T00:00:00Z'`
|
|
3475
|
+
];
|
|
3476
|
+
// Must have at least one specific criteria for older search
|
|
3477
|
+
if (criteria.from) {
|
|
3478
|
+
const escapedFrom = this.escapeODataValue(criteria.from);
|
|
3479
|
+
if (criteria.from.includes('@')) {
|
|
3480
|
+
olderFilters.push(`from/emailAddress/address eq '${escapedFrom}'`);
|
|
3481
|
+
}
|
|
3482
|
+
else {
|
|
3483
|
+
olderFilters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
else if (criteria.subject) {
|
|
3487
|
+
const escapedSubject = this.escapeODataValue(criteria.subject);
|
|
3488
|
+
olderFilters.push(`contains(subject,'${escapedSubject}')`);
|
|
3489
|
+
}
|
|
3490
|
+
else {
|
|
3491
|
+
// If no specific sender/subject, skip older search to avoid timeout
|
|
3492
|
+
logger.log(`⚠️ No specific sender/subject for older search. Returning recent results only.`);
|
|
3493
|
+
return messages.slice(0, maxResults);
|
|
3494
|
+
}
|
|
3495
|
+
if (criteria.importance) {
|
|
3496
|
+
olderFilters.push(`importance eq '${criteria.importance}'`);
|
|
3497
|
+
}
|
|
3498
|
+
const olderFilterString = olderFilters.join(' and ');
|
|
3499
|
+
logger.log(`🔍 Large folder older filter: ${olderFilterString}`);
|
|
3500
|
+
olderApiCall = olderApiCall.filter(olderFilterString);
|
|
3501
|
+
const olderResult = await olderApiCall.get();
|
|
3502
|
+
const olderMessages = olderResult.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
3503
|
+
// Apply text filtering to older results
|
|
3504
|
+
let filteredOlderMessages = olderMessages;
|
|
3505
|
+
if (criteria.query) {
|
|
3506
|
+
filteredOlderMessages = olderMessages.filter(message => {
|
|
3507
|
+
const searchText = criteria.query.toLowerCase();
|
|
3508
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
3509
|
+
return messageText.includes(searchText);
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
logger.log(`📧 Large folder search Phase 2 result: ${filteredOlderMessages.length} additional emails found`);
|
|
3513
|
+
// Combine results and avoid duplicates
|
|
3514
|
+
const allMessages = [...messages];
|
|
3515
|
+
for (const olderMessage of filteredOlderMessages) {
|
|
3516
|
+
if (!allMessages.some(existing => existing.id === olderMessage.id)) {
|
|
3517
|
+
allMessages.push(olderMessage);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
return allMessages.slice(0, maxResults);
|
|
3521
|
+
}
|
|
3522
|
+
/**
|
|
3523
|
+
* Standard search for smaller folders (<10k emails)
|
|
3524
|
+
*/
|
|
3525
|
+
async performStandardFolderSearch(folder, criteria, maxResults) {
|
|
3526
|
+
const graphClient = await this.getGraphClient();
|
|
3527
|
+
let apiCall = graphClient
|
|
3528
|
+
.api(`/me/mailFolders/${folder.id}/messages`)
|
|
3529
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
3530
|
+
.orderby('receivedDateTime desc')
|
|
3531
|
+
.top(Math.min(maxResults * 2, 999));
|
|
3532
|
+
// Build filters
|
|
3533
|
+
const filters = [];
|
|
3534
|
+
if (criteria.from) {
|
|
3535
|
+
const escapedFrom = this.escapeODataValue(criteria.from);
|
|
3536
|
+
if (criteria.from.includes('@')) {
|
|
3537
|
+
filters.push(`from/emailAddress/address eq '${escapedFrom}'`);
|
|
3538
|
+
}
|
|
3539
|
+
else {
|
|
3540
|
+
filters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
if (criteria.to && criteria.to.includes('@')) {
|
|
3544
|
+
const escapedTo = this.escapeODataValue(criteria.to);
|
|
3545
|
+
filters.push(`toRecipients/any(r: r/emailAddress/address eq '${escapedTo}')`);
|
|
3546
|
+
}
|
|
3547
|
+
if (criteria.cc && criteria.cc.includes('@')) {
|
|
3548
|
+
const escapedCc = this.escapeODataValue(criteria.cc);
|
|
3549
|
+
filters.push(`ccRecipients/any(r: r/emailAddress/address eq '${escapedCc}')`);
|
|
3550
|
+
}
|
|
3551
|
+
if (criteria.subject) {
|
|
3552
|
+
const escapedSubject = this.escapeODataValue(criteria.subject);
|
|
3553
|
+
filters.push(`contains(subject,'${escapedSubject}')`);
|
|
3554
|
+
}
|
|
3555
|
+
if (criteria.after) {
|
|
3556
|
+
const afterDate = this.formatDateForOData(criteria.after);
|
|
3557
|
+
filters.push(`receivedDateTime ge '${afterDate}'`);
|
|
3558
|
+
}
|
|
3559
|
+
if (criteria.before) {
|
|
3560
|
+
const beforeDate = this.formatDateForOData(criteria.before);
|
|
3561
|
+
filters.push(`receivedDateTime le '${beforeDate}'`);
|
|
3562
|
+
}
|
|
3563
|
+
if (criteria.isUnread !== undefined) {
|
|
3564
|
+
filters.push(`isRead eq ${!criteria.isUnread}`);
|
|
3565
|
+
}
|
|
3566
|
+
if (criteria.hasAttachment !== undefined) {
|
|
3567
|
+
filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
|
|
3568
|
+
}
|
|
3569
|
+
if (criteria.importance) {
|
|
3570
|
+
filters.push(`importance eq '${criteria.importance}'`);
|
|
3571
|
+
}
|
|
3572
|
+
if (filters.length > 0) {
|
|
3573
|
+
const filterString = filters.join(' and ');
|
|
3574
|
+
logger.log(`🔍 Standard folder filter: ${filterString}`);
|
|
3575
|
+
apiCall = apiCall.filter(filterString);
|
|
3576
|
+
}
|
|
3577
|
+
const result = await apiCall.get();
|
|
3578
|
+
let messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
3579
|
+
// Apply additional text filtering
|
|
3580
|
+
if (criteria.query) {
|
|
3581
|
+
messages = messages.filter(message => {
|
|
3582
|
+
const searchText = criteria.query.toLowerCase();
|
|
3583
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
3584
|
+
return messageText.includes(searchText);
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
logger.log(`📧 Standard folder search result: ${messages.length} emails found`);
|
|
3588
|
+
return messages.slice(0, maxResults);
|
|
3589
|
+
}
|
|
3114
3590
|
}
|
|
3115
3591
|
export const ms365Operations = new MS365Operations();
|