ms365-mcp-server 1.1.15 → 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 +1123 -10
- 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 +799 -219
- 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
|
-
|
|
187
|
+
searchTerms.push(`subject:${escapedSubject}`);
|
|
188
|
+
}
|
|
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
|
+
}
|
|
218
205
|
}
|
|
219
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,140 +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: Use KQL (Keyword Query Language) for advanced searches - Get ALL results
|
|
1039
|
-
if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
|
|
1040
|
-
const kqlQuery = this.buildKQLQuery(criteria);
|
|
1041
|
-
if (kqlQuery) {
|
|
1042
|
-
searchAttempts++;
|
|
1043
|
-
logger.log(`🔍 Attempt ${searchAttempts}: Using KQL search: "${kqlQuery}" to get ALL results`);
|
|
1044
|
-
const kqlResults = await this.performKQLSearchAll(kqlQuery);
|
|
1045
|
-
allMessages.push(...kqlResults);
|
|
1046
|
-
if (allMessages.length > 0) {
|
|
1047
|
-
logger.log(`✅ Found ${allMessages.length} results with KQL search`);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
// Strategy 4: Try relaxed KQL search (remove some constraints) - Get ALL results
|
|
1052
|
-
if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
|
|
1053
|
-
const relaxedKQL = this.buildRelaxedKQLQuery(criteria);
|
|
1054
|
-
if (relaxedKQL && relaxedKQL !== this.buildKQLQuery(criteria)) {
|
|
1055
|
-
searchAttempts++;
|
|
1056
|
-
logger.log(`🔍 Attempt ${searchAttempts}: Using relaxed KQL search: "${relaxedKQL}" to get ALL results`);
|
|
1057
|
-
const relaxedResults = await this.performKQLSearchAll(relaxedKQL);
|
|
1058
|
-
allMessages.push(...relaxedResults);
|
|
1059
|
-
if (allMessages.length > 0) {
|
|
1060
|
-
logger.log(`✅ Found ${allMessages.length} results with relaxed KQL search`);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
// Strategy 5: If we found results but they have UNKNOWN IDs, try to resolve them
|
|
1065
|
-
if (allMessages.length > 0 && allMessages.some(msg => msg.id === 'UNKNOWN') && (Date.now() - startTime) < searchTimeout) {
|
|
1066
|
-
logger.log(`🔍 Attempting to resolve UNKNOWN message IDs using direct message queries`);
|
|
1067
|
-
allMessages = await this.resolveUnknownMessageIds(allMessages, criteria);
|
|
1068
|
-
}
|
|
1069
|
-
// Strategy 6: Partial text search across ALL recent emails (expanded scope)
|
|
1070
|
-
if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
|
|
1071
|
-
searchAttempts++;
|
|
1072
|
-
logger.log(`🔍 Attempt ${searchAttempts}: Using expanded partial text search across ALL emails`);
|
|
1073
|
-
const partialResults = await this.performPartialTextSearchAll(criteria);
|
|
1074
|
-
allMessages.push(...partialResults);
|
|
1075
|
-
if (allMessages.length > 0) {
|
|
1076
|
-
logger.log(`✅ Found ${allMessages.length} results with partial text search`);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
// Strategy 7: Fallback to basic search with ALL results
|
|
1080
|
-
if (allMessages.length === 0 && searchAttempts < maxAttempts && (Date.now() - startTime) < searchTimeout) {
|
|
1081
|
-
searchAttempts++;
|
|
1082
|
-
logger.log(`🔍 Attempt ${searchAttempts}: Using fallback basic search to get ALL results`);
|
|
1083
|
-
const basicResult = await this.performBasicSearchAll(criteria);
|
|
1084
|
-
allMessages.push(...basicResult.messages);
|
|
1085
|
-
if (allMessages.length > 0) {
|
|
1086
|
-
logger.log(`✅ Found ${allMessages.length} results with basic search fallback`);
|
|
1087
|
-
}
|
|
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.`);
|
|
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;
|
|
1092
1030
|
}
|
|
1093
|
-
//
|
|
1094
|
-
const
|
|
1095
|
-
logger.log(`📧 After deduplication: ${uniqueMessages.length} unique messages`);
|
|
1096
|
-
const filteredMessages = this.applyAdvancedFiltering(uniqueMessages, criteria);
|
|
1097
|
-
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);
|
|
1098
1033
|
// Sort by relevance and date
|
|
1099
1034
|
const sortedMessages = this.sortSearchResults(filteredMessages, criteria);
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
logger.log(`🔍 Final attempt: Ultra-relaxed search for any partial matches across ALL emails`);
|
|
1103
|
-
const ultraRelaxedResults = await this.performUltraRelaxedSearchAll(criteria);
|
|
1104
|
-
const finalFiltered = this.applyUltraRelaxedFiltering(ultraRelaxedResults, criteria);
|
|
1105
|
-
if (finalFiltered.length > 0) {
|
|
1106
|
-
logger.log(`✅ Ultra-relaxed search found ${finalFiltered.length} results`);
|
|
1107
|
-
// Apply maxResults limit if specified
|
|
1108
|
-
const finalMessages = maxResults > 0 ? finalFiltered.slice(0, maxResults) : finalFiltered.slice(0, safetyLimit);
|
|
1109
|
-
const searchResult = {
|
|
1110
|
-
messages: finalMessages,
|
|
1111
|
-
hasMore: maxResults > 0 ? finalFiltered.length > maxResults : finalFiltered.length > safetyLimit
|
|
1112
|
-
};
|
|
1113
|
-
this.setCachedResults(cacheKey, searchResult);
|
|
1114
|
-
return searchResult;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
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);
|
|
1035
|
+
// Apply maxResults limit
|
|
1036
|
+
const finalMessages = sortedMessages.slice(0, maxResults);
|
|
1119
1037
|
const searchResult = {
|
|
1120
1038
|
messages: finalMessages,
|
|
1121
|
-
hasMore:
|
|
1039
|
+
hasMore: sortedMessages.length > maxResults
|
|
1122
1040
|
};
|
|
1123
1041
|
this.setCachedResults(cacheKey, searchResult);
|
|
1124
|
-
logger.log(`🔍
|
|
1125
|
-
if (finalMessages.length === 0) {
|
|
1126
|
-
logger.log(`❌ No results found despite ${searchAttempts} search strategies. Consider broadening search criteria.`);
|
|
1127
|
-
}
|
|
1042
|
+
logger.log(`🔍 COMPLIANT SEARCH completed: ${finalMessages.length} results`);
|
|
1128
1043
|
return searchResult;
|
|
1129
1044
|
}
|
|
1130
1045
|
catch (error) {
|
|
1131
|
-
logger.error('Error in
|
|
1132
|
-
// Final fallback
|
|
1133
|
-
|
|
1134
|
-
return await this.performBasicSearchAll(criteria);
|
|
1046
|
+
logger.error('Error in compliant email search:', error);
|
|
1047
|
+
// Final fallback
|
|
1048
|
+
return await this.performBasicSearch(criteria);
|
|
1135
1049
|
}
|
|
1136
1050
|
}, 'searchEmails');
|
|
1137
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
|
+
}
|
|
1138
1201
|
/**
|
|
1139
1202
|
* Resolve UNKNOWN message IDs by finding messages using alternative search criteria
|
|
1140
1203
|
*/
|
|
@@ -1396,23 +1459,10 @@ ${originalBodyContent}
|
|
|
1396
1459
|
logger.log(`🔍 Applying OData filter: ${filterString}`);
|
|
1397
1460
|
apiCall = apiCall.filter(filterString);
|
|
1398
1461
|
}
|
|
1399
|
-
// Apply folder filter using
|
|
1462
|
+
// Apply folder filter using optimized folder search
|
|
1400
1463
|
if (criteria.folder) {
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
logger.log(`🔍 Switching to folder-specific search: ${folders[0].displayName} (${folders[0].id})`);
|
|
1404
|
-
apiCall = graphClient
|
|
1405
|
-
.api(`/me/mailFolders/${folders[0].id}/messages`)
|
|
1406
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1407
|
-
.orderby('receivedDateTime desc')
|
|
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
|
-
}
|
|
1415
|
-
}
|
|
1464
|
+
logger.log(`🔍 Using optimized folder search for: ${criteria.folder}`);
|
|
1465
|
+
return await this.performOptimizedFolderSearch(criteria, maxResults);
|
|
1416
1466
|
}
|
|
1417
1467
|
const result = await apiCall.get();
|
|
1418
1468
|
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
@@ -2619,23 +2669,16 @@ ${originalBodyContent}
|
|
|
2619
2669
|
logger.log(`🔍 Applying OData filter: ${filterString}`);
|
|
2620
2670
|
apiCall = apiCall.filter(filterString);
|
|
2621
2671
|
}
|
|
2622
|
-
//
|
|
2672
|
+
// For folder searches, use optimized approach
|
|
2623
2673
|
if (criteria.folder) {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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
|
-
}
|
|
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
|
|
2639
2682
|
}
|
|
2640
2683
|
// Use nextLink if available for pagination
|
|
2641
2684
|
if (nextLink) {
|
|
@@ -2945,31 +2988,169 @@ ${originalBodyContent}
|
|
|
2945
2988
|
} while (nextLink);
|
|
2946
2989
|
return allMessages;
|
|
2947
2990
|
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Fast OData filter search - optimized for speed
|
|
2993
|
+
*/
|
|
2994
|
+
async performFilteredSearchFast(criteria) {
|
|
2995
|
+
try {
|
|
2996
|
+
const graphClient = await this.getGraphClient();
|
|
2997
|
+
const filters = [];
|
|
2998
|
+
// Build filters quickly
|
|
2999
|
+
if (criteria.from) {
|
|
3000
|
+
const escapedFrom = this.escapeODataValue(criteria.from);
|
|
3001
|
+
filters.push(`contains(from/emailAddress/address,'${escapedFrom}') or contains(from/emailAddress/name,'${escapedFrom}')`);
|
|
3002
|
+
}
|
|
3003
|
+
if (criteria.to) {
|
|
3004
|
+
const escapedTo = this.escapeODataValue(criteria.to);
|
|
3005
|
+
filters.push(`toRecipients/any(r:contains(r/emailAddress/address,'${escapedTo}'))`);
|
|
3006
|
+
}
|
|
3007
|
+
if (criteria.cc) {
|
|
3008
|
+
const escapedCc = this.escapeODataValue(criteria.cc);
|
|
3009
|
+
filters.push(`ccRecipients/any(r:contains(r/emailAddress/address,'${escapedCc}'))`);
|
|
3010
|
+
}
|
|
3011
|
+
if (criteria.subject) {
|
|
3012
|
+
const escapedSubject = this.escapeODataValue(criteria.subject);
|
|
3013
|
+
filters.push(`contains(subject,'${escapedSubject}')`);
|
|
3014
|
+
}
|
|
3015
|
+
if (criteria.after) {
|
|
3016
|
+
const afterDate = this.formatDateForOData(criteria.after);
|
|
3017
|
+
filters.push(`receivedDateTime ge '${afterDate}'`);
|
|
3018
|
+
}
|
|
3019
|
+
if (criteria.before) {
|
|
3020
|
+
const beforeDate = this.formatDateForOData(criteria.before);
|
|
3021
|
+
filters.push(`receivedDateTime le '${beforeDate}'`);
|
|
3022
|
+
}
|
|
3023
|
+
if (criteria.isUnread !== undefined) {
|
|
3024
|
+
filters.push(`isRead eq ${!criteria.isUnread}`);
|
|
3025
|
+
}
|
|
3026
|
+
if (criteria.importance) {
|
|
3027
|
+
filters.push(`importance eq '${criteria.importance}'`);
|
|
3028
|
+
}
|
|
3029
|
+
if (criteria.hasAttachment !== undefined) {
|
|
3030
|
+
filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
|
|
3031
|
+
}
|
|
3032
|
+
// Build API call
|
|
3033
|
+
let apiCall = graphClient
|
|
3034
|
+
.api('/me/messages')
|
|
3035
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
3036
|
+
.orderby('receivedDateTime desc')
|
|
3037
|
+
.top(criteria.maxResults || 50);
|
|
3038
|
+
// Apply filters
|
|
3039
|
+
if (filters.length > 0) {
|
|
3040
|
+
const filterString = filters.join(' and ');
|
|
3041
|
+
logger.log(`🔍 Fast OData filter: ${filterString}`);
|
|
3042
|
+
apiCall = apiCall.filter(filterString);
|
|
3043
|
+
}
|
|
3044
|
+
// Apply folder filter using optimized search
|
|
3045
|
+
if (criteria.folder) {
|
|
3046
|
+
logger.log(`🔍 Using optimized folder search (FAST mode) for: ${criteria.folder}`);
|
|
3047
|
+
return await this.performOptimizedFolderSearch(criteria, criteria.maxResults || 50);
|
|
3048
|
+
}
|
|
3049
|
+
const result = await apiCall.get();
|
|
3050
|
+
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
3051
|
+
logger.log(`📧 Fast OData search returned ${messages.length} results`);
|
|
3052
|
+
return messages;
|
|
3053
|
+
}
|
|
3054
|
+
catch (error) {
|
|
3055
|
+
logger.log(`❌ Fast OData search failed: ${error}`);
|
|
3056
|
+
return [];
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Fast basic search - optimized for speed
|
|
3061
|
+
*/
|
|
3062
|
+
async performBasicSearchFast(criteria) {
|
|
3063
|
+
logger.log('🔄 Using fast basic search');
|
|
3064
|
+
const graphClient = await this.getGraphClient();
|
|
3065
|
+
const result = await graphClient
|
|
3066
|
+
.api('/me/messages')
|
|
3067
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
3068
|
+
.orderby('receivedDateTime desc')
|
|
3069
|
+
.top(criteria.maxResults || 50)
|
|
3070
|
+
.get();
|
|
3071
|
+
let messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
3072
|
+
// Apply minimal filtering for speed
|
|
3073
|
+
if (criteria.from || criteria.to || criteria.subject) {
|
|
3074
|
+
messages = this.applyBasicFiltering(messages, criteria);
|
|
3075
|
+
}
|
|
3076
|
+
return {
|
|
3077
|
+
messages,
|
|
3078
|
+
hasMore: !!result['@odata.nextLink']
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Basic filtering for fast search
|
|
3083
|
+
*/
|
|
3084
|
+
applyBasicFiltering(messages, criteria) {
|
|
3085
|
+
return messages.filter(message => {
|
|
3086
|
+
// Simple text matching
|
|
3087
|
+
if (criteria.from) {
|
|
3088
|
+
const fromText = `${message.from.name} ${message.from.address}`.toLowerCase();
|
|
3089
|
+
if (!fromText.includes(criteria.from.toLowerCase()))
|
|
3090
|
+
return false;
|
|
3091
|
+
}
|
|
3092
|
+
if (criteria.to) {
|
|
3093
|
+
const toText = message.toRecipients.map(r => `${r.name} ${r.address}`).join(' ').toLowerCase();
|
|
3094
|
+
if (!toText.includes(criteria.to.toLowerCase()))
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3097
|
+
if (criteria.subject) {
|
|
3098
|
+
if (!message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
3099
|
+
return false;
|
|
3100
|
+
}
|
|
3101
|
+
return true;
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
2948
3104
|
/**
|
|
2949
3105
|
* Search emails in batches of 50, looping up to 5 times to get more results efficiently
|
|
2950
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
|
+
*/
|
|
2951
3111
|
async searchEmailsBatched(criteria = {}, batchSize = 50, maxBatches = 5) {
|
|
2952
3112
|
return await this.executeWithAuth(async () => {
|
|
2953
3113
|
const graphClient = await this.getGraphClient();
|
|
2954
|
-
logger.log(`🔍 Starting batched email search: ${batchSize} per batch, max ${maxBatches} batches`);
|
|
3114
|
+
logger.log(`🔍 Starting FAST batched email search: ${batchSize} per batch, max ${maxBatches} batches`);
|
|
2955
3115
|
logger.log(`🔍 Search criteria:`, JSON.stringify(criteria, null, 2));
|
|
2956
3116
|
const allMessages = [];
|
|
2957
3117
|
let currentBatch = 0;
|
|
2958
3118
|
let hasMoreResults = true;
|
|
2959
3119
|
const startTime = Date.now();
|
|
2960
|
-
const batchTimeout =
|
|
3120
|
+
const batchTimeout = 30 * 1000; // 30 seconds per batch (much faster)
|
|
2961
3121
|
try {
|
|
2962
3122
|
while (currentBatch < maxBatches && hasMoreResults && (Date.now() - startTime) < batchTimeout) {
|
|
2963
3123
|
currentBatch++;
|
|
2964
3124
|
logger.log(`🔍 Processing batch ${currentBatch}/${maxBatches}`);
|
|
2965
|
-
// Create criteria for this batch
|
|
3125
|
+
// Create criteria for this batch with FAST search strategy
|
|
2966
3126
|
const batchCriteria = {
|
|
2967
3127
|
...criteria,
|
|
2968
3128
|
maxResults: batchSize
|
|
2969
3129
|
};
|
|
2970
|
-
//
|
|
2971
|
-
|
|
2972
|
-
|
|
3130
|
+
// Use FAST search - only try the most reliable method first
|
|
3131
|
+
let batchMessages = [];
|
|
3132
|
+
try {
|
|
3133
|
+
// Strategy 1: Fast OData filter search (most reliable and fastest)
|
|
3134
|
+
logger.log(`🔍 Batch ${currentBatch}: Using fast OData filter search`);
|
|
3135
|
+
batchMessages = await this.performFilteredSearchFast(batchCriteria);
|
|
3136
|
+
if (batchMessages.length === 0) {
|
|
3137
|
+
// Strategy 2: Quick basic search fallback
|
|
3138
|
+
logger.log(`🔍 Batch ${currentBatch}: Using quick basic search fallback`);
|
|
3139
|
+
const basicResult = await this.performBasicSearchFast(batchCriteria);
|
|
3140
|
+
batchMessages = basicResult.messages;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
catch (error) {
|
|
3144
|
+
logger.log(`⚠️ Batch ${currentBatch} search failed, trying basic search: ${error}`);
|
|
3145
|
+
try {
|
|
3146
|
+
const basicResult = await this.performBasicSearchFast(batchCriteria);
|
|
3147
|
+
batchMessages = basicResult.messages;
|
|
3148
|
+
}
|
|
3149
|
+
catch (basicError) {
|
|
3150
|
+
logger.log(`❌ Batch ${currentBatch} basic search also failed: ${basicError}`);
|
|
3151
|
+
break;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
2973
3154
|
logger.log(`📧 Batch ${currentBatch}: Found ${batchMessages.length} results`);
|
|
2974
3155
|
if (batchMessages.length > 0) {
|
|
2975
3156
|
// Add new messages (avoid duplicates)
|
|
@@ -2977,20 +3158,20 @@ ${originalBodyContent}
|
|
|
2977
3158
|
allMessages.push(...newMessages);
|
|
2978
3159
|
logger.log(`📧 Batch ${currentBatch}: Added ${newMessages.length} new unique messages (total: ${allMessages.length})`);
|
|
2979
3160
|
// Check if we have more results
|
|
2980
|
-
hasMoreResults =
|
|
3161
|
+
hasMoreResults = batchMessages.length === batchSize;
|
|
2981
3162
|
}
|
|
2982
3163
|
else {
|
|
2983
3164
|
// No more results found
|
|
2984
3165
|
hasMoreResults = false;
|
|
2985
3166
|
logger.log(`📧 Batch ${currentBatch}: No more results found`);
|
|
2986
3167
|
}
|
|
2987
|
-
//
|
|
3168
|
+
// Shorter delay between batches for faster performance
|
|
2988
3169
|
if (hasMoreResults && currentBatch < maxBatches) {
|
|
2989
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
3170
|
+
await new Promise(resolve => setTimeout(resolve, 500)); // Reduced to 500ms
|
|
2990
3171
|
}
|
|
2991
3172
|
}
|
|
2992
3173
|
const totalTime = (Date.now() - startTime) / 1000;
|
|
2993
|
-
logger.log(`🔍
|
|
3174
|
+
logger.log(`🔍 FAST batched search completed: ${allMessages.length} total results from ${currentBatch} batches in ${totalTime} seconds`);
|
|
2994
3175
|
return {
|
|
2995
3176
|
messages: allMessages,
|
|
2996
3177
|
hasMore: hasMoreResults,
|
|
@@ -2998,7 +3179,7 @@ ${originalBodyContent}
|
|
|
2998
3179
|
};
|
|
2999
3180
|
}
|
|
3000
3181
|
catch (error) {
|
|
3001
|
-
logger.error('Error in batched email search:', error);
|
|
3182
|
+
logger.error('Error in fast batched email search:', error);
|
|
3002
3183
|
return {
|
|
3003
3184
|
messages: allMessages,
|
|
3004
3185
|
hasMore: false,
|
|
@@ -3007,5 +3188,404 @@ ${originalBodyContent}
|
|
|
3007
3188
|
}
|
|
3008
3189
|
}, 'searchEmailsBatched');
|
|
3009
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
|
+
}
|
|
3010
3590
|
}
|
|
3011
3591
|
export const ms365Operations = new MS365Operations();
|