ms365-mcp-server 1.1.9 → 1.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/dist/index.js +141 -7
- package/dist/utils/ms365-operations.js +1056 -109
- package/package.json +1 -1
|
@@ -290,6 +290,19 @@ export class MS365Operations {
|
|
|
290
290
|
importance: message.importance || 'normal',
|
|
291
291
|
attachments: attachments.length > 0 ? attachments : undefined
|
|
292
292
|
};
|
|
293
|
+
// Handle threading - set conversationId for replies/forwards
|
|
294
|
+
if (message.conversationId) {
|
|
295
|
+
draftBody.conversationId = message.conversationId;
|
|
296
|
+
}
|
|
297
|
+
// Handle in-reply-to for proper threading
|
|
298
|
+
if (message.inReplyTo) {
|
|
299
|
+
draftBody.internetMessageHeaders = [
|
|
300
|
+
{
|
|
301
|
+
name: 'In-Reply-To',
|
|
302
|
+
value: message.inReplyTo
|
|
303
|
+
}
|
|
304
|
+
];
|
|
305
|
+
}
|
|
293
306
|
if (message.replyTo) {
|
|
294
307
|
draftBody.replyTo = [{
|
|
295
308
|
emailAddress: {
|
|
@@ -313,6 +326,211 @@ export class MS365Operations {
|
|
|
313
326
|
throw error;
|
|
314
327
|
}
|
|
315
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Update a draft email
|
|
331
|
+
*/
|
|
332
|
+
async updateDraftEmail(draftId, updates) {
|
|
333
|
+
try {
|
|
334
|
+
const graphClient = await this.getGraphClient();
|
|
335
|
+
// Prepare update payload
|
|
336
|
+
const updateBody = {};
|
|
337
|
+
if (updates.subject) {
|
|
338
|
+
updateBody.subject = updates.subject;
|
|
339
|
+
}
|
|
340
|
+
if (updates.body) {
|
|
341
|
+
updateBody.body = {
|
|
342
|
+
contentType: updates.bodyType === 'html' ? 'html' : 'text',
|
|
343
|
+
content: updates.body
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (updates.to) {
|
|
347
|
+
updateBody.toRecipients = updates.to.map(email => ({
|
|
348
|
+
emailAddress: {
|
|
349
|
+
address: email,
|
|
350
|
+
name: email.split('@')[0]
|
|
351
|
+
}
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
if (updates.cc) {
|
|
355
|
+
updateBody.ccRecipients = updates.cc.map(email => ({
|
|
356
|
+
emailAddress: {
|
|
357
|
+
address: email,
|
|
358
|
+
name: email.split('@')[0]
|
|
359
|
+
}
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
if (updates.bcc) {
|
|
363
|
+
updateBody.bccRecipients = updates.bcc.map(email => ({
|
|
364
|
+
emailAddress: {
|
|
365
|
+
address: email,
|
|
366
|
+
name: email.split('@')[0]
|
|
367
|
+
}
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
if (updates.importance) {
|
|
371
|
+
updateBody.importance = updates.importance;
|
|
372
|
+
}
|
|
373
|
+
if (updates.replyTo) {
|
|
374
|
+
updateBody.replyTo = [{
|
|
375
|
+
emailAddress: {
|
|
376
|
+
address: updates.replyTo,
|
|
377
|
+
name: updates.replyTo.split('@')[0]
|
|
378
|
+
}
|
|
379
|
+
}];
|
|
380
|
+
}
|
|
381
|
+
// Update attachments if provided
|
|
382
|
+
if (updates.attachments) {
|
|
383
|
+
updateBody.attachments = updates.attachments.map(att => ({
|
|
384
|
+
'@odata.type': '#microsoft.graph.fileAttachment',
|
|
385
|
+
name: att.name,
|
|
386
|
+
contentBytes: att.contentBytes,
|
|
387
|
+
contentType: att.contentType || 'application/octet-stream'
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
// Update the draft
|
|
391
|
+
const result = await graphClient
|
|
392
|
+
.api(`/me/messages/${draftId}`)
|
|
393
|
+
.patch(updateBody);
|
|
394
|
+
logger.log(`Draft email ${draftId} updated successfully`);
|
|
395
|
+
return {
|
|
396
|
+
id: result.id || draftId,
|
|
397
|
+
status: 'draft_updated'
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
logger.error(`Error updating draft email ${draftId}:`, error);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Send a draft email
|
|
407
|
+
*/
|
|
408
|
+
async sendDraftEmail(draftId) {
|
|
409
|
+
try {
|
|
410
|
+
const graphClient = await this.getGraphClient();
|
|
411
|
+
// Send the draft
|
|
412
|
+
await graphClient
|
|
413
|
+
.api(`/me/messages/${draftId}/send`)
|
|
414
|
+
.post({});
|
|
415
|
+
logger.log(`Draft email ${draftId} sent successfully`);
|
|
416
|
+
return {
|
|
417
|
+
id: draftId,
|
|
418
|
+
status: 'sent'
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
logger.error(`Error sending draft email ${draftId}:`, error);
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* List draft emails
|
|
428
|
+
*/
|
|
429
|
+
async listDrafts(maxResults = 50) {
|
|
430
|
+
try {
|
|
431
|
+
const graphClient = await this.getGraphClient();
|
|
432
|
+
const result = await graphClient
|
|
433
|
+
.api('/me/mailFolders/drafts/messages')
|
|
434
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink,isDraft')
|
|
435
|
+
.orderby('createdDateTime desc')
|
|
436
|
+
.top(maxResults)
|
|
437
|
+
.get();
|
|
438
|
+
const messages = result.value?.map((email) => ({
|
|
439
|
+
id: email.id,
|
|
440
|
+
subject: email.subject || '',
|
|
441
|
+
from: {
|
|
442
|
+
name: email.from?.emailAddress?.name || '',
|
|
443
|
+
address: email.from?.emailAddress?.address || ''
|
|
444
|
+
},
|
|
445
|
+
toRecipients: email.toRecipients?.map((recipient) => ({
|
|
446
|
+
name: recipient.emailAddress?.name || '',
|
|
447
|
+
address: recipient.emailAddress?.address || ''
|
|
448
|
+
})) || [],
|
|
449
|
+
ccRecipients: email.ccRecipients?.map((recipient) => ({
|
|
450
|
+
name: recipient.emailAddress?.name || '',
|
|
451
|
+
address: recipient.emailAddress?.address || ''
|
|
452
|
+
})) || [],
|
|
453
|
+
receivedDateTime: email.receivedDateTime,
|
|
454
|
+
sentDateTime: email.sentDateTime,
|
|
455
|
+
bodyPreview: email.bodyPreview || '',
|
|
456
|
+
isRead: email.isRead || false,
|
|
457
|
+
hasAttachments: email.hasAttachments || false,
|
|
458
|
+
importance: email.importance || 'normal',
|
|
459
|
+
conversationId: email.conversationId || '',
|
|
460
|
+
parentFolderId: email.parentFolderId || '',
|
|
461
|
+
webLink: email.webLink || '',
|
|
462
|
+
attachments: []
|
|
463
|
+
})) || [];
|
|
464
|
+
return {
|
|
465
|
+
messages,
|
|
466
|
+
hasMore: !!result['@odata.nextLink']
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
logger.error('Error listing draft emails:', error);
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Create a threaded reply draft from a specific message
|
|
476
|
+
*/
|
|
477
|
+
async createReplyDraft(originalMessageId, body, replyToAll = false) {
|
|
478
|
+
try {
|
|
479
|
+
const graphClient = await this.getGraphClient();
|
|
480
|
+
// Use Microsoft Graph's createReply endpoint for proper threading
|
|
481
|
+
const endpoint = replyToAll
|
|
482
|
+
? `/me/messages/${originalMessageId}/createReplyAll`
|
|
483
|
+
: `/me/messages/${originalMessageId}/createReply`;
|
|
484
|
+
const requestBody = {};
|
|
485
|
+
// If body is provided, include it as a comment
|
|
486
|
+
if (body) {
|
|
487
|
+
requestBody.comment = body;
|
|
488
|
+
}
|
|
489
|
+
const replyDraft = await graphClient
|
|
490
|
+
.api(endpoint)
|
|
491
|
+
.post(requestBody);
|
|
492
|
+
return {
|
|
493
|
+
id: replyDraft.id,
|
|
494
|
+
subject: replyDraft.subject,
|
|
495
|
+
conversationId: replyDraft.conversationId,
|
|
496
|
+
toRecipients: replyDraft.toRecipients?.map((r) => r.emailAddress.address),
|
|
497
|
+
ccRecipients: replyDraft.ccRecipients?.map((r) => r.emailAddress.address),
|
|
498
|
+
bodyPreview: replyDraft.bodyPreview,
|
|
499
|
+
isDraft: replyDraft.isDraft
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
throw new Error(`Error creating reply draft: ${error}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Create a threaded forward draft from a specific message
|
|
508
|
+
*/
|
|
509
|
+
async createForwardDraft(originalMessageId, comment) {
|
|
510
|
+
try {
|
|
511
|
+
const graphClient = await this.getGraphClient();
|
|
512
|
+
// Use Microsoft Graph's createForward endpoint for proper threading
|
|
513
|
+
const endpoint = `/me/messages/${originalMessageId}/createForward`;
|
|
514
|
+
const requestBody = {};
|
|
515
|
+
// If comment is provided, include it
|
|
516
|
+
if (comment) {
|
|
517
|
+
requestBody.comment = comment;
|
|
518
|
+
}
|
|
519
|
+
const forwardDraft = await graphClient
|
|
520
|
+
.api(endpoint)
|
|
521
|
+
.post(requestBody);
|
|
522
|
+
return {
|
|
523
|
+
id: forwardDraft.id,
|
|
524
|
+
subject: forwardDraft.subject,
|
|
525
|
+
conversationId: forwardDraft.conversationId,
|
|
526
|
+
bodyPreview: forwardDraft.bodyPreview,
|
|
527
|
+
isDraft: forwardDraft.isDraft
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
throw new Error(`Error creating forward draft: ${error}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
316
534
|
/**
|
|
317
535
|
* Get email by ID
|
|
318
536
|
*/
|
|
@@ -425,7 +643,7 @@ export class MS365Operations {
|
|
|
425
643
|
}
|
|
426
644
|
}
|
|
427
645
|
/**
|
|
428
|
-
* Search
|
|
646
|
+
* Advanced email search using Microsoft Graph Search API with KQL - Persistent search until results found
|
|
429
647
|
*/
|
|
430
648
|
async searchEmails(criteria = {}) {
|
|
431
649
|
return await this.executeWithAuth(async () => {
|
|
@@ -436,152 +654,491 @@ export class MS365Operations {
|
|
|
436
654
|
if (cachedResults) {
|
|
437
655
|
return cachedResults;
|
|
438
656
|
}
|
|
439
|
-
|
|
657
|
+
const maxResults = criteria.maxResults || 50;
|
|
658
|
+
let allMessages = [];
|
|
659
|
+
let searchAttempts = 0;
|
|
660
|
+
const maxAttempts = 6; // Try multiple strategies
|
|
661
|
+
logger.log(`🔍 Starting persistent search with criteria: ${JSON.stringify(criteria)}`);
|
|
440
662
|
try {
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
.
|
|
445
|
-
.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
})) || [],
|
|
462
|
-
receivedDateTime: email.receivedDateTime,
|
|
463
|
-
sentDateTime: email.sentDateTime,
|
|
464
|
-
bodyPreview: email.bodyPreview || '',
|
|
465
|
-
isRead: email.isRead || false,
|
|
466
|
-
hasAttachments: email.hasAttachments || false,
|
|
467
|
-
importance: email.importance || 'normal',
|
|
468
|
-
conversationId: email.conversationId || '',
|
|
469
|
-
parentFolderId: email.parentFolderId || '',
|
|
470
|
-
webLink: email.webLink || ''
|
|
471
|
-
})) || [];
|
|
472
|
-
// Apply manual filtering for all criteria
|
|
473
|
-
messages = this.applyManualFiltering(messages, criteria);
|
|
474
|
-
// For emails with attachments, get attachment counts
|
|
475
|
-
for (const message of messages) {
|
|
476
|
-
if (message.hasAttachments) {
|
|
477
|
-
try {
|
|
478
|
-
const attachments = await graphClient
|
|
479
|
-
.api(`/me/messages/${message.id}/attachments`)
|
|
480
|
-
.select('id')
|
|
481
|
-
.get();
|
|
482
|
-
message.attachments = new Array(attachments.value?.length || 0);
|
|
663
|
+
// Strategy 1: Use Microsoft Graph Search API for text-based queries
|
|
664
|
+
if (criteria.query && searchAttempts < maxAttempts) {
|
|
665
|
+
searchAttempts++;
|
|
666
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using Graph Search API for query: "${criteria.query}"`);
|
|
667
|
+
const searchResults = await this.performGraphSearch(criteria.query, maxResults * 2); // Search for more to increase chances
|
|
668
|
+
allMessages.push(...searchResults);
|
|
669
|
+
if (allMessages.length > 0) {
|
|
670
|
+
logger.log(`✅ Found ${allMessages.length} results with Graph Search API`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Strategy 2: Use KQL (Keyword Query Language) for advanced searches
|
|
674
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
675
|
+
const kqlQuery = this.buildKQLQuery(criteria);
|
|
676
|
+
if (kqlQuery) {
|
|
677
|
+
searchAttempts++;
|
|
678
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using KQL search: "${kqlQuery}"`);
|
|
679
|
+
const kqlResults = await this.performKQLSearch(kqlQuery, maxResults * 2);
|
|
680
|
+
allMessages.push(...kqlResults);
|
|
681
|
+
if (allMessages.length > 0) {
|
|
682
|
+
logger.log(`✅ Found ${allMessages.length} results with KQL search`);
|
|
483
683
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Strategy 3: Try relaxed KQL search (remove some constraints)
|
|
687
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
688
|
+
const relaxedKQL = this.buildRelaxedKQLQuery(criteria);
|
|
689
|
+
if (relaxedKQL && relaxedKQL !== this.buildKQLQuery(criteria)) {
|
|
690
|
+
searchAttempts++;
|
|
691
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using relaxed KQL search: "${relaxedKQL}"`);
|
|
692
|
+
const relaxedResults = await this.performKQLSearch(relaxedKQL, maxResults * 2);
|
|
693
|
+
allMessages.push(...relaxedResults);
|
|
694
|
+
if (allMessages.length > 0) {
|
|
695
|
+
logger.log(`✅ Found ${allMessages.length} results with relaxed KQL search`);
|
|
487
696
|
}
|
|
488
697
|
}
|
|
489
698
|
}
|
|
699
|
+
// Strategy 4: OData filter search with broader scope
|
|
700
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
701
|
+
searchAttempts++;
|
|
702
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using OData filter search with broader scope`);
|
|
703
|
+
const filterResults = await this.performFilteredSearch(criteria, maxResults * 3); // Even broader search
|
|
704
|
+
allMessages.push(...filterResults);
|
|
705
|
+
if (allMessages.length > 0) {
|
|
706
|
+
logger.log(`✅ Found ${allMessages.length} results with OData filter search`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Strategy 5: Partial text search across recent emails (expanded scope)
|
|
710
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
711
|
+
searchAttempts++;
|
|
712
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using expanded partial text search`);
|
|
713
|
+
const partialResults = await this.performPartialTextSearch(criteria, maxResults * 4); // Very broad search
|
|
714
|
+
allMessages.push(...partialResults);
|
|
715
|
+
if (allMessages.length > 0) {
|
|
716
|
+
logger.log(`✅ Found ${allMessages.length} results with partial text search`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Strategy 6: Fallback to basic search with maximum scope
|
|
720
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
721
|
+
searchAttempts++;
|
|
722
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using fallback basic search with maximum scope`);
|
|
723
|
+
const basicResult = await this.performBasicSearch({
|
|
724
|
+
...criteria,
|
|
725
|
+
maxResults: Math.max(maxResults * 5, 500) // Very large scope
|
|
726
|
+
});
|
|
727
|
+
allMessages.push(...basicResult.messages);
|
|
728
|
+
if (allMessages.length > 0) {
|
|
729
|
+
logger.log(`✅ Found ${allMessages.length} results with basic search fallback`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Remove duplicates and apply advanced filtering
|
|
733
|
+
const uniqueMessages = this.removeDuplicateMessages(allMessages);
|
|
734
|
+
logger.log(`📧 After deduplication: ${uniqueMessages.length} unique messages`);
|
|
735
|
+
const filteredMessages = this.applyAdvancedFiltering(uniqueMessages, criteria);
|
|
736
|
+
logger.log(`📧 After advanced filtering: ${filteredMessages.length} filtered messages`);
|
|
737
|
+
// Sort by relevance and date
|
|
738
|
+
const sortedMessages = this.sortSearchResults(filteredMessages, criteria);
|
|
739
|
+
// If still no results, try one more time with very relaxed criteria
|
|
740
|
+
if (sortedMessages.length === 0 && (criteria.query || criteria.from || criteria.subject)) {
|
|
741
|
+
logger.log(`🔍 Final attempt: Ultra-relaxed search for any partial matches`);
|
|
742
|
+
const ultraRelaxedResults = await this.performUltraRelaxedSearch(criteria, maxResults * 3);
|
|
743
|
+
const finalFiltered = this.applyUltraRelaxedFiltering(ultraRelaxedResults, criteria);
|
|
744
|
+
if (finalFiltered.length > 0) {
|
|
745
|
+
logger.log(`✅ Ultra-relaxed search found ${finalFiltered.length} results`);
|
|
746
|
+
const limitedMessages = finalFiltered.slice(0, maxResults);
|
|
747
|
+
const searchResult = {
|
|
748
|
+
messages: limitedMessages,
|
|
749
|
+
hasMore: finalFiltered.length > maxResults
|
|
750
|
+
};
|
|
751
|
+
this.setCachedResults(cacheKey, searchResult);
|
|
752
|
+
return searchResult;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// Limit results
|
|
756
|
+
const limitedMessages = sortedMessages.slice(0, maxResults);
|
|
490
757
|
const searchResult = {
|
|
491
|
-
messages,
|
|
492
|
-
hasMore:
|
|
758
|
+
messages: limitedMessages,
|
|
759
|
+
hasMore: sortedMessages.length > maxResults
|
|
493
760
|
};
|
|
494
761
|
this.setCachedResults(cacheKey, searchResult);
|
|
762
|
+
logger.log(`🔍 Search completed after ${searchAttempts} attempts: ${limitedMessages.length} final results`);
|
|
763
|
+
if (limitedMessages.length === 0) {
|
|
764
|
+
logger.log(`❌ No results found despite ${searchAttempts} search strategies. Consider broadening search criteria.`);
|
|
765
|
+
}
|
|
495
766
|
return searchResult;
|
|
496
767
|
}
|
|
497
768
|
catch (error) {
|
|
498
|
-
logger.error('Error in email search:', error);
|
|
499
|
-
|
|
769
|
+
logger.error('Error in persistent email search:', error);
|
|
770
|
+
// Final fallback - get some recent emails and filter them
|
|
771
|
+
logger.log('🔄 Final fallback: getting recent emails to filter manually');
|
|
772
|
+
return await this.performBasicSearch(criteria);
|
|
500
773
|
}
|
|
501
774
|
}, 'searchEmails');
|
|
502
775
|
}
|
|
503
776
|
/**
|
|
504
|
-
*
|
|
777
|
+
* Use Microsoft Graph Search API for full-text search
|
|
505
778
|
*/
|
|
506
|
-
|
|
779
|
+
async performGraphSearch(query, maxResults) {
|
|
780
|
+
try {
|
|
781
|
+
const graphClient = await this.getGraphClient();
|
|
782
|
+
const searchRequest = {
|
|
783
|
+
requests: [
|
|
784
|
+
{
|
|
785
|
+
entityTypes: ['message'],
|
|
786
|
+
query: {
|
|
787
|
+
queryString: query
|
|
788
|
+
},
|
|
789
|
+
from: 0,
|
|
790
|
+
size: Math.min(maxResults, 1000), // Graph Search max is 1000
|
|
791
|
+
fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
|
|
792
|
+
}
|
|
793
|
+
]
|
|
794
|
+
};
|
|
795
|
+
const result = await graphClient
|
|
796
|
+
.api('/search/query')
|
|
797
|
+
.post(searchRequest);
|
|
798
|
+
const messages = [];
|
|
799
|
+
if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
|
|
800
|
+
for (const hit of result.value[0].hitsContainers[0].hits) {
|
|
801
|
+
const email = hit.resource;
|
|
802
|
+
messages.push(this.mapEmailResult(email));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
logger.log(`📧 Graph Search returned ${messages.length} results`);
|
|
806
|
+
return messages;
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
logger.log(`❌ Graph Search failed: ${error}. Falling back to alternative search.`);
|
|
810
|
+
return [];
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Build KQL (Keyword Query Language) for advanced searches
|
|
815
|
+
*/
|
|
816
|
+
buildKQLQuery(criteria) {
|
|
817
|
+
const kqlParts = [];
|
|
818
|
+
if (criteria.from) {
|
|
819
|
+
// Smart sender search - handles partial names, emails, display names
|
|
820
|
+
const fromTerm = criteria.from.trim();
|
|
821
|
+
if (fromTerm.includes('@')) {
|
|
822
|
+
kqlParts.push(`from:${fromTerm}`);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
// For names, search in both from field and sender
|
|
826
|
+
kqlParts.push(`(from:"${fromTerm}" OR sender:"${fromTerm}" OR from:*${fromTerm}*)`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (criteria.to) {
|
|
830
|
+
kqlParts.push(`to:${criteria.to}`);
|
|
831
|
+
}
|
|
832
|
+
if (criteria.cc) {
|
|
833
|
+
kqlParts.push(`cc:${criteria.cc}`);
|
|
834
|
+
}
|
|
835
|
+
if (criteria.subject) {
|
|
836
|
+
kqlParts.push(`subject:"${criteria.subject}"`);
|
|
837
|
+
}
|
|
838
|
+
if (criteria.hasAttachment === true) {
|
|
839
|
+
kqlParts.push('hasattachment:true');
|
|
840
|
+
}
|
|
841
|
+
else if (criteria.hasAttachment === false) {
|
|
842
|
+
kqlParts.push('hasattachment:false');
|
|
843
|
+
}
|
|
844
|
+
if (criteria.isUnread === true) {
|
|
845
|
+
kqlParts.push('isread:false');
|
|
846
|
+
}
|
|
847
|
+
else if (criteria.isUnread === false) {
|
|
848
|
+
kqlParts.push('isread:true');
|
|
849
|
+
}
|
|
850
|
+
if (criteria.importance) {
|
|
851
|
+
kqlParts.push(`importance:${criteria.importance}`);
|
|
852
|
+
}
|
|
853
|
+
if (criteria.after) {
|
|
854
|
+
const afterDate = new Date(criteria.after).toISOString().split('T')[0];
|
|
855
|
+
kqlParts.push(`received>=${afterDate}`);
|
|
856
|
+
}
|
|
857
|
+
if (criteria.before) {
|
|
858
|
+
const beforeDate = new Date(criteria.before).toISOString().split('T')[0];
|
|
859
|
+
kqlParts.push(`received<=${beforeDate}`);
|
|
860
|
+
}
|
|
861
|
+
if (criteria.folder) {
|
|
862
|
+
kqlParts.push(`foldernames:"${criteria.folder}"`);
|
|
863
|
+
}
|
|
864
|
+
return kqlParts.join(' AND ');
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Perform KQL-based search using Graph Search API
|
|
868
|
+
*/
|
|
869
|
+
async performKQLSearch(kqlQuery, maxResults) {
|
|
870
|
+
try {
|
|
871
|
+
const graphClient = await this.getGraphClient();
|
|
872
|
+
const searchRequest = {
|
|
873
|
+
requests: [
|
|
874
|
+
{
|
|
875
|
+
entityTypes: ['message'],
|
|
876
|
+
query: {
|
|
877
|
+
queryString: kqlQuery
|
|
878
|
+
},
|
|
879
|
+
from: 0,
|
|
880
|
+
size: Math.min(maxResults, 1000),
|
|
881
|
+
fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
|
|
882
|
+
}
|
|
883
|
+
]
|
|
884
|
+
};
|
|
885
|
+
const result = await graphClient
|
|
886
|
+
.api('/search/query')
|
|
887
|
+
.post(searchRequest);
|
|
888
|
+
const messages = [];
|
|
889
|
+
if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
|
|
890
|
+
for (const hit of result.value[0].hitsContainers[0].hits) {
|
|
891
|
+
const email = hit.resource;
|
|
892
|
+
messages.push(this.mapEmailResult(email));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
logger.log(`📧 KQL Search returned ${messages.length} results`);
|
|
896
|
+
return messages;
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
logger.log(`❌ KQL Search failed: ${error}. Falling back to filter search.`);
|
|
900
|
+
return [];
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Fallback to OData filter-based search
|
|
905
|
+
*/
|
|
906
|
+
async performFilteredSearch(criteria, maxResults) {
|
|
907
|
+
try {
|
|
908
|
+
const graphClient = await this.getGraphClient();
|
|
909
|
+
let apiCall = graphClient
|
|
910
|
+
.api('/me/messages')
|
|
911
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
912
|
+
.orderby('receivedDateTime desc')
|
|
913
|
+
.top(Math.min(maxResults, 999));
|
|
914
|
+
// Apply OData filters where possible
|
|
915
|
+
const filters = [];
|
|
916
|
+
if (criteria.from && criteria.from.includes('@')) {
|
|
917
|
+
filters.push(`from/emailAddress/address eq '${criteria.from}'`);
|
|
918
|
+
}
|
|
919
|
+
if (criteria.to && criteria.to.includes('@')) {
|
|
920
|
+
filters.push(`toRecipients/any(r: r/emailAddress/address eq '${criteria.to}')`);
|
|
921
|
+
}
|
|
922
|
+
if (criteria.isUnread !== undefined) {
|
|
923
|
+
filters.push(`isRead eq ${!criteria.isUnread}`);
|
|
924
|
+
}
|
|
925
|
+
if (criteria.hasAttachment !== undefined) {
|
|
926
|
+
filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
|
|
927
|
+
}
|
|
928
|
+
if (criteria.importance) {
|
|
929
|
+
filters.push(`importance eq '${criteria.importance}'`);
|
|
930
|
+
}
|
|
931
|
+
if (filters.length > 0) {
|
|
932
|
+
apiCall = apiCall.filter(filters.join(' and '));
|
|
933
|
+
}
|
|
934
|
+
// Apply folder filter using specific folder API
|
|
935
|
+
if (criteria.folder) {
|
|
936
|
+
const folders = await this.findFolderByName(criteria.folder);
|
|
937
|
+
if (folders.length > 0) {
|
|
938
|
+
apiCall = graphClient
|
|
939
|
+
.api(`/me/mailFolders/${folders[0].id}/messages`)
|
|
940
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
941
|
+
.orderby('receivedDateTime desc')
|
|
942
|
+
.top(Math.min(maxResults, 999));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const result = await apiCall.get();
|
|
946
|
+
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
947
|
+
logger.log(`📧 Filtered Search returned ${messages.length} results`);
|
|
948
|
+
return messages;
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
logger.log(`❌ Filtered Search failed: ${error}`);
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Fallback to basic search (original implementation)
|
|
957
|
+
*/
|
|
958
|
+
async performBasicSearch(criteria) {
|
|
959
|
+
logger.log('🔄 Using fallback basic search');
|
|
960
|
+
const graphClient = await this.getGraphClient();
|
|
961
|
+
const result = await graphClient
|
|
962
|
+
.api('/me/messages')
|
|
963
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
964
|
+
.orderby('receivedDateTime desc')
|
|
965
|
+
.top(criteria.maxResults || 50)
|
|
966
|
+
.get();
|
|
967
|
+
let messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
968
|
+
messages = this.applyManualFiltering(messages, criteria);
|
|
969
|
+
return {
|
|
970
|
+
messages,
|
|
971
|
+
hasMore: !!result['@odata.nextLink']
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Map email result to EmailInfo format
|
|
976
|
+
*/
|
|
977
|
+
mapEmailResult(email) {
|
|
978
|
+
return {
|
|
979
|
+
id: email.id,
|
|
980
|
+
subject: email.subject || '',
|
|
981
|
+
from: {
|
|
982
|
+
name: email.from?.emailAddress?.name || '',
|
|
983
|
+
address: email.from?.emailAddress?.address || ''
|
|
984
|
+
},
|
|
985
|
+
toRecipients: email.toRecipients?.map((recipient) => ({
|
|
986
|
+
name: recipient.emailAddress?.name || '',
|
|
987
|
+
address: recipient.emailAddress?.address || ''
|
|
988
|
+
})) || [],
|
|
989
|
+
ccRecipients: email.ccRecipients?.map((recipient) => ({
|
|
990
|
+
name: recipient.emailAddress?.name || '',
|
|
991
|
+
address: recipient.emailAddress?.address || ''
|
|
992
|
+
})) || [],
|
|
993
|
+
receivedDateTime: email.receivedDateTime,
|
|
994
|
+
sentDateTime: email.sentDateTime,
|
|
995
|
+
bodyPreview: email.bodyPreview || '',
|
|
996
|
+
isRead: email.isRead || false,
|
|
997
|
+
hasAttachments: email.hasAttachments || false,
|
|
998
|
+
importance: email.importance || 'normal',
|
|
999
|
+
conversationId: email.conversationId || '',
|
|
1000
|
+
parentFolderId: email.parentFolderId || '',
|
|
1001
|
+
webLink: email.webLink || '',
|
|
1002
|
+
attachments: []
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Remove duplicate messages based on ID
|
|
1007
|
+
*/
|
|
1008
|
+
removeDuplicateMessages(messages) {
|
|
1009
|
+
const seen = new Set();
|
|
507
1010
|
return messages.filter(message => {
|
|
508
|
-
|
|
1011
|
+
if (seen.has(message.id)) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
seen.add(message.id);
|
|
1015
|
+
return true;
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Apply advanced filtering with better logic
|
|
1020
|
+
*/
|
|
1021
|
+
applyAdvancedFiltering(messages, criteria) {
|
|
1022
|
+
return messages.filter(message => {
|
|
1023
|
+
// Enhanced text search across multiple fields
|
|
509
1024
|
if (criteria.query) {
|
|
510
|
-
const searchText = criteria.query.toLowerCase();
|
|
511
|
-
const
|
|
512
|
-
|
|
1025
|
+
const searchText = criteria.query.toLowerCase().trim();
|
|
1026
|
+
const searchableContent = [
|
|
1027
|
+
message.subject,
|
|
1028
|
+
message.bodyPreview,
|
|
1029
|
+
message.from.name,
|
|
1030
|
+
message.from.address,
|
|
1031
|
+
...message.toRecipients.map(r => `${r.name} ${r.address}`),
|
|
1032
|
+
...message.ccRecipients.map(r => `${r.name} ${r.address}`)
|
|
1033
|
+
].join(' ').toLowerCase();
|
|
1034
|
+
// Support multiple search terms (AND logic)
|
|
1035
|
+
const searchTerms = searchText.split(/\s+/).filter(term => term.length > 0);
|
|
1036
|
+
if (!searchTerms.every(term => searchableContent.includes(term))) {
|
|
513
1037
|
return false;
|
|
1038
|
+
}
|
|
514
1039
|
}
|
|
1040
|
+
// Enhanced sender search
|
|
515
1041
|
if (criteria.from) {
|
|
516
1042
|
const searchTerm = criteria.from.toLowerCase().trim();
|
|
517
1043
|
const fromName = message.from.name.toLowerCase();
|
|
518
1044
|
const fromAddress = message.from.address.toLowerCase();
|
|
519
|
-
// Multiple matching strategies for better partial name support
|
|
520
1045
|
const matches = [
|
|
521
|
-
// Direct name or email match
|
|
522
1046
|
fromName.includes(searchTerm),
|
|
523
1047
|
fromAddress.includes(searchTerm),
|
|
524
|
-
// Split search
|
|
1048
|
+
// Split name search
|
|
525
1049
|
searchTerm.split(/\s+/).every(part => fromName.includes(part)),
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
// Handle initials (e.g., "M Kumar" for "Madan Kumar")
|
|
533
|
-
searchTerm.split(/\s+/).length === 2 &&
|
|
534
|
-
fromName.split(/\s+/).length >= 2 &&
|
|
535
|
-
fromName.split(/\s+/)[0].startsWith(searchTerm.split(/\s+/)[0][0]) &&
|
|
536
|
-
fromName.includes(searchTerm.split(/\s+/)[1])
|
|
1050
|
+
// Word boundary search
|
|
1051
|
+
new RegExp(`\\b${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i').test(fromName),
|
|
1052
|
+
// Email domain search
|
|
1053
|
+
searchTerm.includes('@') && fromAddress === searchTerm,
|
|
1054
|
+
// Partial email search
|
|
1055
|
+
!searchTerm.includes('@') && fromAddress.includes(searchTerm)
|
|
537
1056
|
];
|
|
538
1057
|
if (!matches.some(match => match))
|
|
539
1058
|
return false;
|
|
540
1059
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (!toMatch)
|
|
544
|
-
return false;
|
|
545
|
-
}
|
|
546
|
-
if (criteria.cc) {
|
|
547
|
-
// Handle case where ccRecipients might be undefined
|
|
548
|
-
const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
|
|
549
|
-
message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
|
|
550
|
-
if (!ccMatch)
|
|
551
|
-
return false;
|
|
552
|
-
}
|
|
553
|
-
if (criteria.subject) {
|
|
554
|
-
if (!message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
555
|
-
return false;
|
|
556
|
-
}
|
|
557
|
-
// Apply date filters
|
|
558
|
-
if (criteria.after) {
|
|
559
|
-
const afterDate = new Date(criteria.after);
|
|
560
|
-
const messageDate = new Date(message.receivedDateTime);
|
|
561
|
-
if (messageDate < afterDate)
|
|
562
|
-
return false;
|
|
563
|
-
}
|
|
564
|
-
if (criteria.before) {
|
|
565
|
-
const beforeDate = new Date(criteria.before);
|
|
1060
|
+
// Date range filters
|
|
1061
|
+
if (criteria.after || criteria.before) {
|
|
566
1062
|
const messageDate = new Date(message.receivedDateTime);
|
|
567
|
-
if (
|
|
568
|
-
|
|
1063
|
+
if (criteria.after) {
|
|
1064
|
+
const afterDate = new Date(criteria.after);
|
|
1065
|
+
if (messageDate < afterDate)
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
if (criteria.before) {
|
|
1069
|
+
const beforeDate = new Date(criteria.before);
|
|
1070
|
+
if (messageDate > beforeDate)
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
569
1073
|
}
|
|
570
|
-
//
|
|
571
|
-
if (criteria.
|
|
1074
|
+
// Other filters remain the same but are more robust
|
|
1075
|
+
if (criteria.to && !message.toRecipients.some(r => r.address.toLowerCase().includes(criteria.to.toLowerCase())))
|
|
572
1076
|
return false;
|
|
573
|
-
|
|
574
|
-
// Apply read status filter
|
|
575
|
-
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread) {
|
|
1077
|
+
if (criteria.cc && (!message.ccRecipients || !message.ccRecipients.some(r => r.address.toLowerCase().includes(criteria.cc.toLowerCase()))))
|
|
576
1078
|
return false;
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (criteria.
|
|
1079
|
+
if (criteria.subject && !message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
1080
|
+
return false;
|
|
1081
|
+
if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment)
|
|
1082
|
+
return false;
|
|
1083
|
+
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread)
|
|
1084
|
+
return false;
|
|
1085
|
+
if (criteria.importance && message.importance !== criteria.importance)
|
|
580
1086
|
return false;
|
|
581
|
-
}
|
|
582
1087
|
return true;
|
|
583
1088
|
});
|
|
584
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Sort search results by relevance and date
|
|
1092
|
+
*/
|
|
1093
|
+
sortSearchResults(messages, criteria) {
|
|
1094
|
+
return messages.sort((a, b) => {
|
|
1095
|
+
// Calculate relevance score
|
|
1096
|
+
const scoreA = this.calculateRelevanceScore(a, criteria);
|
|
1097
|
+
const scoreB = this.calculateRelevanceScore(b, criteria);
|
|
1098
|
+
if (scoreA !== scoreB) {
|
|
1099
|
+
return scoreB - scoreA; // Higher score first
|
|
1100
|
+
}
|
|
1101
|
+
// If relevance is same, sort by date (newer first)
|
|
1102
|
+
return new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime();
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Calculate relevance score for search results
|
|
1107
|
+
*/
|
|
1108
|
+
calculateRelevanceScore(message, criteria) {
|
|
1109
|
+
let score = 0;
|
|
1110
|
+
if (criteria.query) {
|
|
1111
|
+
const query = criteria.query.toLowerCase();
|
|
1112
|
+
// Subject matches get higher score
|
|
1113
|
+
if (message.subject.toLowerCase().includes(query))
|
|
1114
|
+
score += 10;
|
|
1115
|
+
// Sender name matches
|
|
1116
|
+
if (message.from.name.toLowerCase().includes(query))
|
|
1117
|
+
score += 5;
|
|
1118
|
+
// Body preview matches
|
|
1119
|
+
if (message.bodyPreview.toLowerCase().includes(query))
|
|
1120
|
+
score += 3;
|
|
1121
|
+
// Exact word matches get bonus
|
|
1122
|
+
const words = query.split(/\s+/);
|
|
1123
|
+
words.forEach(word => {
|
|
1124
|
+
if (message.subject.toLowerCase().includes(word))
|
|
1125
|
+
score += 2;
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
// Recent emails get slight boost
|
|
1129
|
+
const daysOld = (Date.now() - new Date(message.receivedDateTime).getTime()) / (1000 * 60 * 60 * 24);
|
|
1130
|
+
if (daysOld < 7)
|
|
1131
|
+
score += 2;
|
|
1132
|
+
else if (daysOld < 30)
|
|
1133
|
+
score += 1;
|
|
1134
|
+
// Unread emails get boost
|
|
1135
|
+
if (!message.isRead)
|
|
1136
|
+
score += 1;
|
|
1137
|
+
// Important emails get boost
|
|
1138
|
+
if (message.importance === 'high')
|
|
1139
|
+
score += 3;
|
|
1140
|
+
return score;
|
|
1141
|
+
}
|
|
585
1142
|
/**
|
|
586
1143
|
* List emails in a folder
|
|
587
1144
|
*/
|
|
@@ -732,14 +1289,108 @@ export class MS365Operations {
|
|
|
732
1289
|
}
|
|
733
1290
|
}
|
|
734
1291
|
/**
|
|
735
|
-
* List mail folders
|
|
1292
|
+
* List mail folders including child folders recursively
|
|
736
1293
|
*/
|
|
737
1294
|
async listFolders() {
|
|
738
1295
|
try {
|
|
739
1296
|
const graphClient = await this.getGraphClient();
|
|
1297
|
+
const allFolders = [];
|
|
1298
|
+
// Get top-level folders
|
|
1299
|
+
logger.log('📁 Fetching top-level mail folders...');
|
|
740
1300
|
const result = await graphClient
|
|
741
1301
|
.api('/me/mailFolders')
|
|
742
1302
|
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1303
|
+
.top(999) // Request up to 999 folders to avoid pagination
|
|
1304
|
+
.get();
|
|
1305
|
+
logger.log(`📁 Found ${result.value?.length || 0} top-level folders from API`);
|
|
1306
|
+
// Process top-level folders
|
|
1307
|
+
const topLevelFolders = result.value?.map((folder) => ({
|
|
1308
|
+
id: folder.id,
|
|
1309
|
+
displayName: folder.displayName || '',
|
|
1310
|
+
totalItemCount: folder.totalItemCount || 0,
|
|
1311
|
+
unreadItemCount: folder.unreadItemCount || 0,
|
|
1312
|
+
parentFolderId: folder.parentFolderId,
|
|
1313
|
+
depth: 0,
|
|
1314
|
+
fullPath: folder.displayName || ''
|
|
1315
|
+
})) || [];
|
|
1316
|
+
logger.log(`📁 Processed ${topLevelFolders.length} top-level folders: ${topLevelFolders.map((f) => f.displayName).join(', ')}`);
|
|
1317
|
+
allFolders.push(...topLevelFolders);
|
|
1318
|
+
// Recursively get child folders for each top-level folder
|
|
1319
|
+
logger.log('📂 Starting recursive child folder discovery...');
|
|
1320
|
+
for (const folder of topLevelFolders) {
|
|
1321
|
+
logger.log(`📂 Checking children of: ${folder.displayName} (${folder.id})`);
|
|
1322
|
+
const childFolders = await this.getChildFolders(folder.id, folder.fullPath || folder.displayName, 1);
|
|
1323
|
+
logger.log(`📂 Found ${childFolders.length} child folders for ${folder.displayName}`);
|
|
1324
|
+
allFolders.push(...childFolders);
|
|
1325
|
+
}
|
|
1326
|
+
logger.log(`📁 Total folders discovered: ${allFolders.length}`);
|
|
1327
|
+
// Sort folders by full path for better organization
|
|
1328
|
+
allFolders.sort((a, b) => (a.fullPath || '').localeCompare(b.fullPath || ''));
|
|
1329
|
+
return allFolders;
|
|
1330
|
+
}
|
|
1331
|
+
catch (error) {
|
|
1332
|
+
logger.error('Error listing folders:', error);
|
|
1333
|
+
throw error;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Recursively get child folders of a parent folder
|
|
1338
|
+
*/
|
|
1339
|
+
async getChildFolders(parentFolderId, parentPath, depth) {
|
|
1340
|
+
try {
|
|
1341
|
+
const graphClient = await this.getGraphClient();
|
|
1342
|
+
const childFolders = [];
|
|
1343
|
+
logger.log(`📂 [Depth ${depth}] Getting child folders for: ${parentPath} (${parentFolderId})`);
|
|
1344
|
+
// Get child folders of the specified parent
|
|
1345
|
+
const result = await graphClient
|
|
1346
|
+
.api(`/me/mailFolders/${parentFolderId}/childFolders`)
|
|
1347
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1348
|
+
.top(999) // Request up to 999 child folders to avoid pagination
|
|
1349
|
+
.get();
|
|
1350
|
+
logger.log(`📂 [Depth ${depth}] API returned ${result.value?.length || 0} child folders for ${parentPath}`);
|
|
1351
|
+
if (result.value && result.value.length > 0) {
|
|
1352
|
+
const folders = result.value.map((folder) => ({
|
|
1353
|
+
id: folder.id,
|
|
1354
|
+
displayName: folder.displayName || '',
|
|
1355
|
+
totalItemCount: folder.totalItemCount || 0,
|
|
1356
|
+
unreadItemCount: folder.unreadItemCount || 0,
|
|
1357
|
+
parentFolderId: folder.parentFolderId,
|
|
1358
|
+
depth,
|
|
1359
|
+
fullPath: `${parentPath}/${folder.displayName || ''}`
|
|
1360
|
+
}));
|
|
1361
|
+
logger.log(`📂 [Depth ${depth}] Mapped child folders: ${folders.map((f) => f.displayName).join(', ')}`);
|
|
1362
|
+
childFolders.push(...folders);
|
|
1363
|
+
// Recursively get child folders (limit depth to prevent infinite recursion)
|
|
1364
|
+
if (depth < 10) { // Max depth of 10 levels
|
|
1365
|
+
logger.log(`📂 [Depth ${depth}] Recursing into ${folders.length} child folders...`);
|
|
1366
|
+
for (const folder of folders) {
|
|
1367
|
+
const subChildFolders = await this.getChildFolders(folder.id, folder.fullPath || folder.displayName, depth + 1);
|
|
1368
|
+
childFolders.push(...subChildFolders);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
logger.log(`📂 [Depth ${depth}] Maximum depth reached, stopping recursion`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
logger.log(`📂 [Depth ${depth}] Returning ${childFolders.length} total child folders for ${parentPath}`);
|
|
1376
|
+
return childFolders;
|
|
1377
|
+
}
|
|
1378
|
+
catch (error) {
|
|
1379
|
+
// Log the error but don't throw - some folders might not have children or access might be restricted
|
|
1380
|
+
logger.log(`❌ [Depth ${depth}] Could not access child folders for ${parentFolderId} (${parentPath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
1381
|
+
return [];
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* List child folders of a specific parent folder
|
|
1386
|
+
*/
|
|
1387
|
+
async listChildFolders(parentFolderId) {
|
|
1388
|
+
try {
|
|
1389
|
+
const graphClient = await this.getGraphClient();
|
|
1390
|
+
const result = await graphClient
|
|
1391
|
+
.api(`/me/mailFolders/${parentFolderId}/childFolders`)
|
|
1392
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1393
|
+
.top(999) // Request up to 999 child folders to avoid pagination
|
|
743
1394
|
.get();
|
|
744
1395
|
return result.value?.map((folder) => ({
|
|
745
1396
|
id: folder.id,
|
|
@@ -750,7 +1401,22 @@ export class MS365Operations {
|
|
|
750
1401
|
})) || [];
|
|
751
1402
|
}
|
|
752
1403
|
catch (error) {
|
|
753
|
-
logger.error(
|
|
1404
|
+
logger.error(`Error listing child folders for ${parentFolderId}:`, error);
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Find folder by name (case-insensitive search across all folders)
|
|
1410
|
+
*/
|
|
1411
|
+
async findFolderByName(folderName) {
|
|
1412
|
+
try {
|
|
1413
|
+
const allFolders = await this.listFolders();
|
|
1414
|
+
const searchName = folderName.toLowerCase();
|
|
1415
|
+
return allFolders.filter(folder => folder.displayName.toLowerCase().includes(searchName) ||
|
|
1416
|
+
(folder.fullPath && folder.fullPath.toLowerCase().includes(searchName)));
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
logger.error(`Error finding folder by name ${folderName}:`, error);
|
|
754
1420
|
throw error;
|
|
755
1421
|
}
|
|
756
1422
|
}
|
|
@@ -1028,5 +1694,286 @@ export class MS365Operations {
|
|
|
1028
1694
|
throw error;
|
|
1029
1695
|
}
|
|
1030
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Debug method to check raw folder API response
|
|
1699
|
+
*/
|
|
1700
|
+
async debugFolders() {
|
|
1701
|
+
try {
|
|
1702
|
+
const graphClient = await this.getGraphClient();
|
|
1703
|
+
logger.log('🔍 DEBUG: Testing folder API calls...');
|
|
1704
|
+
// Test basic folder listing
|
|
1705
|
+
const basicResult = await graphClient
|
|
1706
|
+
.api('/me/mailFolders')
|
|
1707
|
+
.get();
|
|
1708
|
+
logger.log(`🔍 DEBUG: Raw /me/mailFolders response: ${JSON.stringify(basicResult, null, 2)}`);
|
|
1709
|
+
// Test with specific selection
|
|
1710
|
+
const selectResult = await graphClient
|
|
1711
|
+
.api('/me/mailFolders')
|
|
1712
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1713
|
+
.get();
|
|
1714
|
+
logger.log(`🔍 DEBUG: Selected fields response: ${JSON.stringify(selectResult, null, 2)}`);
|
|
1715
|
+
// Test well-known folder access
|
|
1716
|
+
try {
|
|
1717
|
+
const inboxResult = await graphClient
|
|
1718
|
+
.api('/me/mailFolders/inbox/childFolders')
|
|
1719
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1720
|
+
.get();
|
|
1721
|
+
logger.log(`🔍 DEBUG: Inbox children: ${JSON.stringify(inboxResult, null, 2)}`);
|
|
1722
|
+
}
|
|
1723
|
+
catch (inboxError) {
|
|
1724
|
+
logger.log(`🔍 DEBUG: Inbox children error: ${inboxError}`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
catch (error) {
|
|
1728
|
+
logger.error('🔍 DEBUG: Error in debugFolders:', error);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Apply manual filtering to search results (used when $filter can't be used with $search)
|
|
1733
|
+
*/
|
|
1734
|
+
applyManualFiltering(messages, criteria) {
|
|
1735
|
+
return messages.filter(message => {
|
|
1736
|
+
// Apply text search filters manually
|
|
1737
|
+
if (criteria.query) {
|
|
1738
|
+
const searchText = criteria.query.toLowerCase();
|
|
1739
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
1740
|
+
if (!messageText.includes(searchText))
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
if (criteria.from) {
|
|
1744
|
+
const searchTerm = criteria.from.toLowerCase().trim();
|
|
1745
|
+
const fromName = message.from.name.toLowerCase();
|
|
1746
|
+
const fromAddress = message.from.address.toLowerCase();
|
|
1747
|
+
const matches = [
|
|
1748
|
+
fromName.includes(searchTerm),
|
|
1749
|
+
fromAddress.includes(searchTerm),
|
|
1750
|
+
searchTerm.split(/\s+/).every(part => fromName.includes(part)),
|
|
1751
|
+
fromName.split(/\s+/).some(namePart => namePart.startsWith(searchTerm))
|
|
1752
|
+
];
|
|
1753
|
+
if (!matches.some(match => match))
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
if (criteria.to) {
|
|
1757
|
+
const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
|
|
1758
|
+
if (!toMatch)
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
if (criteria.cc) {
|
|
1762
|
+
const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
|
|
1763
|
+
message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
|
|
1764
|
+
if (!ccMatch)
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
if (criteria.subject) {
|
|
1768
|
+
if (!message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
if (criteria.after) {
|
|
1772
|
+
const afterDate = new Date(criteria.after);
|
|
1773
|
+
const messageDate = new Date(message.receivedDateTime);
|
|
1774
|
+
if (messageDate < afterDate)
|
|
1775
|
+
return false;
|
|
1776
|
+
}
|
|
1777
|
+
if (criteria.before) {
|
|
1778
|
+
const beforeDate = new Date(criteria.before);
|
|
1779
|
+
const messageDate = new Date(message.receivedDateTime);
|
|
1780
|
+
if (messageDate > beforeDate)
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment) {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread) {
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
if (criteria.importance && message.importance !== criteria.importance) {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
return true;
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Build relaxed KQL query with fewer constraints
|
|
1797
|
+
*/
|
|
1798
|
+
buildRelaxedKQLQuery(criteria) {
|
|
1799
|
+
const kqlParts = [];
|
|
1800
|
+
// Only include the most important criteria for relaxed search
|
|
1801
|
+
if (criteria.from) {
|
|
1802
|
+
const fromTerm = criteria.from.trim();
|
|
1803
|
+
if (fromTerm.includes('@')) {
|
|
1804
|
+
// For email addresses, try domain search too
|
|
1805
|
+
const domain = fromTerm.split('@')[1];
|
|
1806
|
+
if (domain) {
|
|
1807
|
+
kqlParts.push(`(from:${fromTerm} OR from:*@${domain})`);
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
kqlParts.push(`from:${fromTerm}`);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
// For names, use wildcard search
|
|
1815
|
+
kqlParts.push(`from:*${fromTerm}*`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (criteria.subject) {
|
|
1819
|
+
// Use partial subject matching
|
|
1820
|
+
kqlParts.push(`subject:*${criteria.subject}*`);
|
|
1821
|
+
}
|
|
1822
|
+
// Skip attachment and read status filters in relaxed mode
|
|
1823
|
+
// Keep only critical filters
|
|
1824
|
+
if (criteria.importance === 'high') {
|
|
1825
|
+
kqlParts.push(`importance:high`);
|
|
1826
|
+
}
|
|
1827
|
+
// Expand date range for relaxed search
|
|
1828
|
+
if (criteria.after) {
|
|
1829
|
+
const afterDate = new Date(criteria.after);
|
|
1830
|
+
afterDate.setDate(afterDate.getDate() - 7); // Expand by a week
|
|
1831
|
+
const expandedDate = afterDate.toISOString().split('T')[0];
|
|
1832
|
+
kqlParts.push(`received>=${expandedDate}`);
|
|
1833
|
+
}
|
|
1834
|
+
return kqlParts.join(' AND ');
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Perform partial text search across recent emails with broader matching
|
|
1838
|
+
*/
|
|
1839
|
+
async performPartialTextSearch(criteria, maxResults) {
|
|
1840
|
+
try {
|
|
1841
|
+
const graphClient = await this.getGraphClient();
|
|
1842
|
+
// Get a large set of recent emails to search through
|
|
1843
|
+
const result = await graphClient
|
|
1844
|
+
.api('/me/messages')
|
|
1845
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1846
|
+
.orderby('receivedDateTime desc')
|
|
1847
|
+
.top(Math.min(maxResults, 1000))
|
|
1848
|
+
.get();
|
|
1849
|
+
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
1850
|
+
// Apply very broad partial matching
|
|
1851
|
+
const partialMatches = messages.filter(message => {
|
|
1852
|
+
let matches = true;
|
|
1853
|
+
// Very flexible text search
|
|
1854
|
+
if (criteria.query) {
|
|
1855
|
+
const searchTerms = criteria.query.toLowerCase().split(/\s+/);
|
|
1856
|
+
const searchableText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
1857
|
+
// At least 50% of search terms should match
|
|
1858
|
+
const matchingTerms = searchTerms.filter(term => searchableText.includes(term));
|
|
1859
|
+
matches = matchingTerms.length >= Math.ceil(searchTerms.length * 0.5);
|
|
1860
|
+
}
|
|
1861
|
+
// Very flexible sender search
|
|
1862
|
+
if (criteria.from && matches) {
|
|
1863
|
+
const fromTerm = criteria.from.toLowerCase();
|
|
1864
|
+
const fromText = `${message.from.name} ${message.from.address}`.toLowerCase();
|
|
1865
|
+
// Partial matching with character-level similarity
|
|
1866
|
+
matches = fromText.includes(fromTerm) ||
|
|
1867
|
+
fromTerm.split('').some(char => fromText.includes(char)) ||
|
|
1868
|
+
this.calculateStringSimilarity(fromTerm, fromText) > 0.3;
|
|
1869
|
+
}
|
|
1870
|
+
return matches;
|
|
1871
|
+
});
|
|
1872
|
+
logger.log(`📧 Partial text search found ${partialMatches.length} potential matches`);
|
|
1873
|
+
return partialMatches;
|
|
1874
|
+
}
|
|
1875
|
+
catch (error) {
|
|
1876
|
+
logger.log(`❌ Partial text search failed: ${error}`);
|
|
1877
|
+
return [];
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Ultra-relaxed search that casts a very wide net
|
|
1882
|
+
*/
|
|
1883
|
+
async performUltraRelaxedSearch(criteria, maxResults) {
|
|
1884
|
+
try {
|
|
1885
|
+
const graphClient = await this.getGraphClient();
|
|
1886
|
+
// Search across multiple folders and time ranges
|
|
1887
|
+
const searches = [];
|
|
1888
|
+
// Search recent emails
|
|
1889
|
+
searches.push(graphClient
|
|
1890
|
+
.api('/me/messages')
|
|
1891
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1892
|
+
.orderby('receivedDateTime desc')
|
|
1893
|
+
.top(500)
|
|
1894
|
+
.get());
|
|
1895
|
+
// Search sent items if looking for specific people
|
|
1896
|
+
if (criteria.from || criteria.to) {
|
|
1897
|
+
searches.push(graphClient
|
|
1898
|
+
.api('/me/mailFolders/sentitems/messages')
|
|
1899
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1900
|
+
.orderby('receivedDateTime desc')
|
|
1901
|
+
.top(200)
|
|
1902
|
+
.get());
|
|
1903
|
+
}
|
|
1904
|
+
const results = await Promise.allSettled(searches);
|
|
1905
|
+
const allEmails = [];
|
|
1906
|
+
results.forEach((result, index) => {
|
|
1907
|
+
if (result.status === 'fulfilled') {
|
|
1908
|
+
const emails = result.value.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
1909
|
+
allEmails.push(...emails);
|
|
1910
|
+
logger.log(`📧 Ultra-relaxed search ${index + 1} found ${emails.length} emails`);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
return this.removeDuplicateMessages(allEmails);
|
|
1914
|
+
}
|
|
1915
|
+
catch (error) {
|
|
1916
|
+
logger.log(`❌ Ultra-relaxed search failed: ${error}`);
|
|
1917
|
+
return [];
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Ultra-relaxed filtering with very permissive matching
|
|
1922
|
+
*/
|
|
1923
|
+
applyUltraRelaxedFiltering(messages, criteria) {
|
|
1924
|
+
return messages.filter(message => {
|
|
1925
|
+
let score = 0;
|
|
1926
|
+
let hasAnyMatch = false;
|
|
1927
|
+
// Any partial query match
|
|
1928
|
+
if (criteria.query) {
|
|
1929
|
+
const searchText = criteria.query.toLowerCase();
|
|
1930
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name}`.toLowerCase();
|
|
1931
|
+
// Check for any word matches
|
|
1932
|
+
const queryWords = searchText.split(/\s+/);
|
|
1933
|
+
const matchingWords = queryWords.filter(word => messageText.includes(word));
|
|
1934
|
+
if (matchingWords.length > 0) {
|
|
1935
|
+
hasAnyMatch = true;
|
|
1936
|
+
score += matchingWords.length;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
// Any sender similarity
|
|
1940
|
+
if (criteria.from) {
|
|
1941
|
+
const fromTerm = criteria.from.toLowerCase();
|
|
1942
|
+
const fromText = `${message.from.name} ${message.from.address}`.toLowerCase();
|
|
1943
|
+
if (fromText.includes(fromTerm) ||
|
|
1944
|
+
this.calculateStringSimilarity(fromTerm, fromText) > 0.2) {
|
|
1945
|
+
hasAnyMatch = true;
|
|
1946
|
+
score += 2;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
// Any subject similarity
|
|
1950
|
+
if (criteria.subject) {
|
|
1951
|
+
const subjectTerm = criteria.subject.toLowerCase();
|
|
1952
|
+
if (message.subject.toLowerCase().includes(subjectTerm)) {
|
|
1953
|
+
hasAnyMatch = true;
|
|
1954
|
+
score += 3;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
// If no specific criteria, return recent emails
|
|
1958
|
+
if (!criteria.query && !criteria.from && !criteria.subject) {
|
|
1959
|
+
hasAnyMatch = true;
|
|
1960
|
+
}
|
|
1961
|
+
return hasAnyMatch;
|
|
1962
|
+
}).sort((a, b) => {
|
|
1963
|
+
// Sort by date for ultra-relaxed results
|
|
1964
|
+
return new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime();
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Calculate string similarity (simple version)
|
|
1969
|
+
*/
|
|
1970
|
+
calculateStringSimilarity(str1, str2) {
|
|
1971
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
1972
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
1973
|
+
if (longer.length === 0)
|
|
1974
|
+
return 1.0;
|
|
1975
|
+
const matches = shorter.split('').filter(char => longer.includes(char)).length;
|
|
1976
|
+
return matches / longer.length;
|
|
1977
|
+
}
|
|
1031
1978
|
}
|
|
1032
1979
|
export const ms365Operations = new MS365Operations();
|