ms365-mcp-server 1.1.10 → 1.1.12
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 +26 -5
- package/dist/utils/ms365-operations.js +1120 -146
- package/package.json +1 -1
|
@@ -423,6 +423,54 @@ export class MS365Operations {
|
|
|
423
423
|
throw error;
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Verify draft threading by checking if draft appears in conversation
|
|
428
|
+
*/
|
|
429
|
+
async verifyDraftThreading(draftId, originalConversationId) {
|
|
430
|
+
try {
|
|
431
|
+
const graphClient = await this.getGraphClient();
|
|
432
|
+
// Get the draft details
|
|
433
|
+
const draft = await graphClient
|
|
434
|
+
.api(`/me/messages/${draftId}`)
|
|
435
|
+
.select('id,subject,conversationId,parentFolderId,internetMessageHeaders,isDraft')
|
|
436
|
+
.get();
|
|
437
|
+
// Check if conversation IDs match
|
|
438
|
+
const conversationMatch = draft.conversationId === originalConversationId;
|
|
439
|
+
// Get all messages in the conversation to see if draft appears
|
|
440
|
+
let conversationMessages = [];
|
|
441
|
+
try {
|
|
442
|
+
const convResult = await graphClient
|
|
443
|
+
.api('/me/messages')
|
|
444
|
+
.filter(`conversationId eq '${originalConversationId}'`)
|
|
445
|
+
.select('id,subject,isDraft,parentFolderId')
|
|
446
|
+
.get();
|
|
447
|
+
conversationMessages = convResult.value || [];
|
|
448
|
+
}
|
|
449
|
+
catch (convError) {
|
|
450
|
+
logger.log(`Could not fetch conversation messages: ${convError}`);
|
|
451
|
+
}
|
|
452
|
+
const draftInConversation = conversationMessages.some((msg) => msg.id === draftId);
|
|
453
|
+
return {
|
|
454
|
+
isThreaded: conversationMatch && draftInConversation,
|
|
455
|
+
details: {
|
|
456
|
+
draftConversationId: draft.conversationId,
|
|
457
|
+
originalConversationId,
|
|
458
|
+
conversationMatch,
|
|
459
|
+
draftInConversation,
|
|
460
|
+
draftFolder: draft.parentFolderId,
|
|
461
|
+
conversationMessagesCount: conversationMessages.length,
|
|
462
|
+
internetMessageHeaders: draft.internetMessageHeaders
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
logger.error('Error verifying draft threading:', error);
|
|
468
|
+
return {
|
|
469
|
+
isThreaded: false,
|
|
470
|
+
details: { error: String(error) }
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
426
474
|
/**
|
|
427
475
|
* List draft emails
|
|
428
476
|
*/
|
|
@@ -477,27 +525,143 @@ export class MS365Operations {
|
|
|
477
525
|
async createReplyDraft(originalMessageId, body, replyToAll = false) {
|
|
478
526
|
try {
|
|
479
527
|
const graphClient = await this.getGraphClient();
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
528
|
+
logger.log(`Creating reply draft for message: ${originalMessageId}`);
|
|
529
|
+
logger.log(`Reply to all: ${replyToAll}`);
|
|
530
|
+
// First, get the original message to include its content in the reply
|
|
531
|
+
const originalMessage = await graphClient
|
|
532
|
+
.api(`/me/messages/${originalMessageId}`)
|
|
533
|
+
.select('id,subject,from,toRecipients,ccRecipients,conversationId,parentFolderId,internetMessageId,internetMessageHeaders,conversationIndex,body,sentDateTime')
|
|
534
|
+
.get();
|
|
535
|
+
// Build the complete reply body with original content
|
|
536
|
+
const originalBodyContent = originalMessage.body?.content || '';
|
|
537
|
+
const fromDisplay = originalMessage.from?.emailAddress?.name || originalMessage.from?.emailAddress?.address || '';
|
|
538
|
+
const sentDate = new Date(originalMessage.sentDateTime).toLocaleString();
|
|
539
|
+
const completeReplyBody = `${body || ''}
|
|
540
|
+
|
|
541
|
+
<br><br>
|
|
542
|
+
<div style="border-left: 2px solid #ccc; padding-left: 10px; margin-top: 10px;">
|
|
543
|
+
<p><strong>From:</strong> ${fromDisplay}<br>
|
|
544
|
+
<strong>Sent:</strong> ${sentDate}<br>
|
|
545
|
+
<strong>Subject:</strong> ${originalMessage.subject}</p>
|
|
546
|
+
<hr style="border: none; border-top: 1px solid #ccc; margin: 10px 0;">
|
|
547
|
+
${originalBodyContent}
|
|
548
|
+
</div>`;
|
|
549
|
+
// First, try using the official Microsoft Graph createReply endpoint for proper threading
|
|
550
|
+
try {
|
|
551
|
+
const endpoint = replyToAll
|
|
552
|
+
? `/me/messages/${originalMessageId}/createReplyAll`
|
|
553
|
+
: `/me/messages/${originalMessageId}/createReply`;
|
|
554
|
+
logger.log(`Using official Graph API endpoint: ${endpoint}`);
|
|
555
|
+
const replyDraft = await graphClient
|
|
556
|
+
.api(endpoint)
|
|
557
|
+
.post({
|
|
558
|
+
message: {
|
|
559
|
+
body: {
|
|
560
|
+
contentType: 'html',
|
|
561
|
+
content: completeReplyBody
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
logger.log(`Reply draft created successfully with ID: ${replyDraft.id}`);
|
|
566
|
+
logger.log(`Draft conversation ID: ${replyDraft.conversationId}`);
|
|
567
|
+
logger.log(`Draft appears as threaded reply in conversation with original content`);
|
|
568
|
+
return {
|
|
569
|
+
id: replyDraft.id,
|
|
570
|
+
subject: replyDraft.subject,
|
|
571
|
+
conversationId: replyDraft.conversationId,
|
|
572
|
+
toRecipients: replyDraft.toRecipients?.map((r) => r.emailAddress.address),
|
|
573
|
+
ccRecipients: replyDraft.ccRecipients?.map((r) => r.emailAddress.address),
|
|
574
|
+
bodyPreview: replyDraft.bodyPreview,
|
|
575
|
+
isDraft: replyDraft.isDraft,
|
|
576
|
+
parentFolderId: replyDraft.parentFolderId
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
catch (officialApiError) {
|
|
580
|
+
logger.log(`Official API failed: ${officialApiError}, trying fallback approach`);
|
|
581
|
+
logger.log(`Fallback: Creating manual reply draft with enhanced threading`);
|
|
582
|
+
logger.log(`Original message conversation ID: ${originalMessage.conversationId}`);
|
|
583
|
+
logger.log(`Original message folder: ${originalMessage.parentFolderId}`);
|
|
584
|
+
// Build proper References header from existing chain
|
|
585
|
+
let referencesHeader = originalMessage.internetMessageId || originalMessage.id;
|
|
586
|
+
if (originalMessage.internetMessageHeaders) {
|
|
587
|
+
const existingReferences = originalMessage.internetMessageHeaders.find((h) => h.name.toLowerCase() === 'references');
|
|
588
|
+
if (existingReferences && existingReferences.value) {
|
|
589
|
+
referencesHeader = `${existingReferences.value} ${referencesHeader}`;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const currentUserEmail = await this.getCurrentUserEmail();
|
|
593
|
+
const draftBody = {
|
|
594
|
+
subject: originalMessage.subject?.startsWith('Re:') ? originalMessage.subject : `Re: ${originalMessage.subject}`,
|
|
595
|
+
body: {
|
|
596
|
+
contentType: 'html',
|
|
597
|
+
content: completeReplyBody
|
|
598
|
+
},
|
|
599
|
+
conversationId: originalMessage.conversationId,
|
|
600
|
+
internetMessageHeaders: [
|
|
601
|
+
{
|
|
602
|
+
name: 'X-In-Reply-To',
|
|
603
|
+
value: originalMessage.internetMessageId || originalMessage.id
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'X-References',
|
|
607
|
+
value: referencesHeader
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
name: 'X-Thread-Topic',
|
|
611
|
+
value: originalMessage.subject?.replace(/^Re:\s*/i, '') || ''
|
|
612
|
+
}
|
|
613
|
+
]
|
|
614
|
+
};
|
|
615
|
+
// Include conversation index if available for proper Outlook threading
|
|
616
|
+
if (originalMessage.conversationIndex) {
|
|
617
|
+
draftBody.internetMessageHeaders.push({
|
|
618
|
+
name: 'X-Thread-Index',
|
|
619
|
+
value: originalMessage.conversationIndex
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
// Set recipients based on reply type
|
|
623
|
+
if (replyToAll) {
|
|
624
|
+
draftBody.toRecipients = [
|
|
625
|
+
...(originalMessage.from ? [{ emailAddress: originalMessage.from.emailAddress }] : []),
|
|
626
|
+
...(originalMessage.toRecipients || []).filter((r) => r.emailAddress.address !== currentUserEmail)
|
|
627
|
+
];
|
|
628
|
+
draftBody.ccRecipients = originalMessage.ccRecipients || [];
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
draftBody.toRecipients = originalMessage.from ? [{ emailAddress: originalMessage.from.emailAddress }] : [];
|
|
632
|
+
}
|
|
633
|
+
// Create the fallback draft
|
|
634
|
+
const replyDraft = await graphClient
|
|
635
|
+
.api('/me/messages')
|
|
636
|
+
.post(draftBody);
|
|
637
|
+
logger.log(`Fallback reply draft created with ID: ${replyDraft.id}`);
|
|
638
|
+
logger.log(`Draft conversation ID: ${replyDraft.conversationId}`);
|
|
639
|
+
// Try to move the draft to the same folder as the original message for better threading
|
|
640
|
+
if (originalMessage.parentFolderId && originalMessage.parentFolderId !== 'drafts') {
|
|
641
|
+
try {
|
|
642
|
+
await graphClient
|
|
643
|
+
.api(`/me/messages/${replyDraft.id}/move`)
|
|
644
|
+
.post({
|
|
645
|
+
destinationId: originalMessage.parentFolderId
|
|
646
|
+
});
|
|
647
|
+
logger.log(`Draft moved to original message folder: ${originalMessage.parentFolderId}`);
|
|
648
|
+
}
|
|
649
|
+
catch (moveError) {
|
|
650
|
+
logger.log(`Could not move draft to original folder: ${moveError}`);
|
|
651
|
+
// This is not critical, draft will remain in drafts folder
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
id: replyDraft.id,
|
|
656
|
+
subject: replyDraft.subject,
|
|
657
|
+
conversationId: replyDraft.conversationId,
|
|
658
|
+
toRecipients: replyDraft.toRecipients?.map((r) => r.emailAddress.address),
|
|
659
|
+
ccRecipients: replyDraft.ccRecipients?.map((r) => r.emailAddress.address),
|
|
660
|
+
bodyPreview: replyDraft.bodyPreview,
|
|
661
|
+
isDraft: replyDraft.isDraft,
|
|
662
|
+
parentFolderId: originalMessage.parentFolderId
|
|
663
|
+
};
|
|
664
|
+
}
|
|
501
665
|
}
|
|
502
666
|
catch (error) {
|
|
503
667
|
throw new Error(`Error creating reply draft: ${error}`);
|
|
@@ -509,23 +673,106 @@ export class MS365Operations {
|
|
|
509
673
|
async createForwardDraft(originalMessageId, comment) {
|
|
510
674
|
try {
|
|
511
675
|
const graphClient = await this.getGraphClient();
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
676
|
+
logger.log(`Creating forward draft for message: ${originalMessageId}`);
|
|
677
|
+
// First, try using the official Microsoft Graph createForward endpoint for proper threading
|
|
678
|
+
try {
|
|
679
|
+
logger.log(`Using official Graph API endpoint: /me/messages/${originalMessageId}/createForward`);
|
|
680
|
+
const forwardDraft = await graphClient
|
|
681
|
+
.api(`/me/messages/${originalMessageId}/createForward`)
|
|
682
|
+
.post({
|
|
683
|
+
message: {
|
|
684
|
+
body: {
|
|
685
|
+
contentType: 'html',
|
|
686
|
+
content: comment || ''
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
logger.log(`Forward draft created successfully with ID: ${forwardDraft.id}`);
|
|
691
|
+
logger.log(`Draft conversation ID: ${forwardDraft.conversationId}`);
|
|
692
|
+
logger.log(`Draft appears as threaded forward in conversation`);
|
|
693
|
+
return {
|
|
694
|
+
id: forwardDraft.id,
|
|
695
|
+
subject: forwardDraft.subject,
|
|
696
|
+
conversationId: forwardDraft.conversationId,
|
|
697
|
+
bodyPreview: forwardDraft.bodyPreview,
|
|
698
|
+
isDraft: forwardDraft.isDraft,
|
|
699
|
+
parentFolderId: forwardDraft.parentFolderId
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
catch (officialApiError) {
|
|
703
|
+
logger.log(`Official API failed: ${officialApiError}, trying fallback approach`);
|
|
704
|
+
// Fallback to manual creation if the official endpoint fails
|
|
705
|
+
const originalMessage = await graphClient
|
|
706
|
+
.api(`/me/messages/${originalMessageId}`)
|
|
707
|
+
.select('id,subject,from,toRecipients,ccRecipients,conversationId,parentFolderId,internetMessageId,internetMessageHeaders,conversationIndex,body,sentDateTime')
|
|
708
|
+
.get();
|
|
709
|
+
logger.log(`Fallback: Creating manual forward draft with enhanced threading`);
|
|
710
|
+
logger.log(`Original message conversation ID: ${originalMessage.conversationId}`);
|
|
711
|
+
logger.log(`Original message folder: ${originalMessage.parentFolderId}`);
|
|
712
|
+
// Build proper References header from existing chain
|
|
713
|
+
let referencesHeader = originalMessage.internetMessageId || originalMessage.id;
|
|
714
|
+
if (originalMessage.internetMessageHeaders) {
|
|
715
|
+
const existingReferences = originalMessage.internetMessageHeaders.find((h) => h.name.toLowerCase() === 'references');
|
|
716
|
+
if (existingReferences && existingReferences.value) {
|
|
717
|
+
referencesHeader = `${existingReferences.value} ${referencesHeader}`;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const forwardedBody = `${comment ? comment + '\n\n' : ''}---------- Forwarded message ----------\nFrom: ${originalMessage.from?.emailAddress?.name || originalMessage.from?.emailAddress?.address}\nDate: ${originalMessage.sentDateTime}\nSubject: ${originalMessage.subject}\nTo: ${originalMessage.toRecipients?.map((r) => r.emailAddress.address).join(', ')}\n\n${originalMessage.body?.content || ''}`;
|
|
721
|
+
const draftBody = {
|
|
722
|
+
subject: originalMessage.subject?.startsWith('Fwd:') ? originalMessage.subject : `Fwd: ${originalMessage.subject}`,
|
|
723
|
+
body: {
|
|
724
|
+
contentType: originalMessage.body?.contentType || 'html',
|
|
725
|
+
content: forwardedBody
|
|
726
|
+
},
|
|
727
|
+
conversationId: originalMessage.conversationId,
|
|
728
|
+
internetMessageHeaders: [
|
|
729
|
+
{
|
|
730
|
+
name: 'X-References',
|
|
731
|
+
value: referencesHeader
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
name: 'X-Thread-Topic',
|
|
735
|
+
value: originalMessage.subject?.replace(/^(Re:|Fwd?):\s*/i, '') || ''
|
|
736
|
+
}
|
|
737
|
+
]
|
|
738
|
+
};
|
|
739
|
+
// Include conversation index if available for proper Outlook threading
|
|
740
|
+
if (originalMessage.conversationIndex) {
|
|
741
|
+
draftBody.internetMessageHeaders.push({
|
|
742
|
+
name: 'X-Thread-Index',
|
|
743
|
+
value: originalMessage.conversationIndex
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
// Create the fallback draft
|
|
747
|
+
const forwardDraft = await graphClient
|
|
748
|
+
.api('/me/messages')
|
|
749
|
+
.post(draftBody);
|
|
750
|
+
logger.log(`Fallback forward draft created with ID: ${forwardDraft.id}`);
|
|
751
|
+
logger.log(`Draft conversation ID: ${forwardDraft.conversationId}`);
|
|
752
|
+
// Try to move the draft to the same folder as the original message for better threading
|
|
753
|
+
if (originalMessage.parentFolderId && originalMessage.parentFolderId !== 'drafts') {
|
|
754
|
+
try {
|
|
755
|
+
await graphClient
|
|
756
|
+
.api(`/me/messages/${forwardDraft.id}/move`)
|
|
757
|
+
.post({
|
|
758
|
+
destinationId: originalMessage.parentFolderId
|
|
759
|
+
});
|
|
760
|
+
logger.log(`Draft moved to original message folder: ${originalMessage.parentFolderId}`);
|
|
761
|
+
}
|
|
762
|
+
catch (moveError) {
|
|
763
|
+
logger.log(`Could not move draft to original folder: ${moveError}`);
|
|
764
|
+
// This is not critical, draft will remain in drafts folder
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
id: forwardDraft.id,
|
|
769
|
+
subject: forwardDraft.subject,
|
|
770
|
+
conversationId: forwardDraft.conversationId,
|
|
771
|
+
bodyPreview: forwardDraft.bodyPreview,
|
|
772
|
+
isDraft: forwardDraft.isDraft,
|
|
773
|
+
parentFolderId: originalMessage.parentFolderId
|
|
774
|
+
};
|
|
775
|
+
}
|
|
529
776
|
}
|
|
530
777
|
catch (error) {
|
|
531
778
|
throw new Error(`Error creating forward draft: ${error}`);
|
|
@@ -643,163 +890,591 @@ export class MS365Operations {
|
|
|
643
890
|
}
|
|
644
891
|
}
|
|
645
892
|
/**
|
|
646
|
-
* Search
|
|
893
|
+
* Advanced email search using Microsoft Graph Search API with KQL - Persistent search until results found
|
|
647
894
|
*/
|
|
648
895
|
async searchEmails(criteria = {}) {
|
|
649
896
|
return await this.executeWithAuth(async () => {
|
|
650
897
|
const graphClient = await this.getGraphClient();
|
|
898
|
+
logger.log(`🔍 Starting email search with criteria:`, JSON.stringify(criteria, null, 2));
|
|
651
899
|
// Create cache key from criteria
|
|
652
900
|
const cacheKey = JSON.stringify(criteria);
|
|
653
901
|
const cachedResults = this.getCachedResults(cacheKey);
|
|
654
902
|
if (cachedResults) {
|
|
903
|
+
logger.log('📦 Returning cached results');
|
|
655
904
|
return cachedResults;
|
|
656
905
|
}
|
|
657
|
-
|
|
906
|
+
const maxResults = criteria.maxResults || 50;
|
|
907
|
+
let allMessages = [];
|
|
908
|
+
let searchAttempts = 0;
|
|
909
|
+
const maxAttempts = 6; // Try multiple strategies
|
|
910
|
+
logger.log(`🔍 Starting persistent search with criteria: ${JSON.stringify(criteria)}`);
|
|
658
911
|
try {
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
.
|
|
663
|
-
.
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
})) || [];
|
|
690
|
-
// Apply manual filtering for all criteria
|
|
691
|
-
messages = this.applyManualFiltering(messages, criteria);
|
|
692
|
-
// For emails with attachments, get attachment counts
|
|
693
|
-
for (const message of messages) {
|
|
694
|
-
if (message.hasAttachments) {
|
|
695
|
-
try {
|
|
696
|
-
const attachments = await graphClient
|
|
697
|
-
.api(`/me/messages/${message.id}/attachments`)
|
|
698
|
-
.select('id')
|
|
699
|
-
.get();
|
|
700
|
-
message.attachments = new Array(attachments.value?.length || 0);
|
|
912
|
+
// Strategy 1: Use reliable OData filter search first (has proper IDs)
|
|
913
|
+
if (searchAttempts < maxAttempts) {
|
|
914
|
+
searchAttempts++;
|
|
915
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using reliable OData filter search first`);
|
|
916
|
+
const filterResults = await this.performFilteredSearch(criteria, maxResults * 2);
|
|
917
|
+
allMessages.push(...filterResults);
|
|
918
|
+
if (allMessages.length > 0) {
|
|
919
|
+
logger.log(`✅ Found ${allMessages.length} results with OData filter search`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// Strategy 2: Use Microsoft Graph Search API for text-based queries (backup)
|
|
923
|
+
if (allMessages.length === 0 && criteria.query && searchAttempts < maxAttempts) {
|
|
924
|
+
searchAttempts++;
|
|
925
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using Graph Search API for query: "${criteria.query}"`);
|
|
926
|
+
const searchResults = await this.performGraphSearch(criteria.query, maxResults * 2); // Search for more to increase chances
|
|
927
|
+
allMessages.push(...searchResults);
|
|
928
|
+
if (allMessages.length > 0) {
|
|
929
|
+
logger.log(`✅ Found ${allMessages.length} results with Graph Search API`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// Strategy 3: Use KQL (Keyword Query Language) for advanced searches
|
|
933
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
934
|
+
const kqlQuery = this.buildKQLQuery(criteria);
|
|
935
|
+
if (kqlQuery) {
|
|
936
|
+
searchAttempts++;
|
|
937
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using KQL search: "${kqlQuery}"`);
|
|
938
|
+
const kqlResults = await this.performKQLSearch(kqlQuery, maxResults * 2);
|
|
939
|
+
allMessages.push(...kqlResults);
|
|
940
|
+
if (allMessages.length > 0) {
|
|
941
|
+
logger.log(`✅ Found ${allMessages.length} results with KQL search`);
|
|
701
942
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// Strategy 4: Try relaxed KQL search (remove some constraints)
|
|
946
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
947
|
+
const relaxedKQL = this.buildRelaxedKQLQuery(criteria);
|
|
948
|
+
if (relaxedKQL && relaxedKQL !== this.buildKQLQuery(criteria)) {
|
|
949
|
+
searchAttempts++;
|
|
950
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using relaxed KQL search: "${relaxedKQL}"`);
|
|
951
|
+
const relaxedResults = await this.performKQLSearch(relaxedKQL, maxResults * 2);
|
|
952
|
+
allMessages.push(...relaxedResults);
|
|
953
|
+
if (allMessages.length > 0) {
|
|
954
|
+
logger.log(`✅ Found ${allMessages.length} results with relaxed KQL search`);
|
|
705
955
|
}
|
|
706
956
|
}
|
|
707
957
|
}
|
|
958
|
+
// Strategy 5: If we found results but they have UNKNOWN IDs, try to resolve them
|
|
959
|
+
if (allMessages.length > 0 && allMessages.some(msg => msg.id === 'UNKNOWN')) {
|
|
960
|
+
logger.log(`🔍 Attempting to resolve UNKNOWN message IDs using direct message queries`);
|
|
961
|
+
allMessages = await this.resolveUnknownMessageIds(allMessages, criteria);
|
|
962
|
+
}
|
|
963
|
+
// Strategy 6: Partial text search across recent emails (expanded scope)
|
|
964
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
965
|
+
searchAttempts++;
|
|
966
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using expanded partial text search`);
|
|
967
|
+
const partialResults = await this.performPartialTextSearch(criteria, maxResults * 4); // Very broad search
|
|
968
|
+
allMessages.push(...partialResults);
|
|
969
|
+
if (allMessages.length > 0) {
|
|
970
|
+
logger.log(`✅ Found ${allMessages.length} results with partial text search`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// Strategy 7: Fallback to basic search with maximum scope
|
|
974
|
+
if (allMessages.length === 0 && searchAttempts < maxAttempts) {
|
|
975
|
+
searchAttempts++;
|
|
976
|
+
logger.log(`🔍 Attempt ${searchAttempts}: Using fallback basic search with maximum scope`);
|
|
977
|
+
const basicResult = await this.performBasicSearch({
|
|
978
|
+
...criteria,
|
|
979
|
+
maxResults: Math.max(maxResults * 5, 500) // Very large scope
|
|
980
|
+
});
|
|
981
|
+
allMessages.push(...basicResult.messages);
|
|
982
|
+
if (allMessages.length > 0) {
|
|
983
|
+
logger.log(`✅ Found ${allMessages.length} results with basic search fallback`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
// Remove duplicates and apply advanced filtering
|
|
987
|
+
const uniqueMessages = this.removeDuplicateMessages(allMessages);
|
|
988
|
+
logger.log(`📧 After deduplication: ${uniqueMessages.length} unique messages`);
|
|
989
|
+
const filteredMessages = this.applyAdvancedFiltering(uniqueMessages, criteria);
|
|
990
|
+
logger.log(`📧 After advanced filtering: ${filteredMessages.length} filtered messages`);
|
|
991
|
+
// Sort by relevance and date
|
|
992
|
+
const sortedMessages = this.sortSearchResults(filteredMessages, criteria);
|
|
993
|
+
// If still no results, try one more time with very relaxed criteria
|
|
994
|
+
if (sortedMessages.length === 0 && (criteria.query || criteria.from || criteria.subject)) {
|
|
995
|
+
logger.log(`🔍 Final attempt: Ultra-relaxed search for any partial matches`);
|
|
996
|
+
const ultraRelaxedResults = await this.performUltraRelaxedSearch(criteria, maxResults * 3);
|
|
997
|
+
const finalFiltered = this.applyUltraRelaxedFiltering(ultraRelaxedResults, criteria);
|
|
998
|
+
if (finalFiltered.length > 0) {
|
|
999
|
+
logger.log(`✅ Ultra-relaxed search found ${finalFiltered.length} results`);
|
|
1000
|
+
const limitedMessages = finalFiltered.slice(0, maxResults);
|
|
1001
|
+
const searchResult = {
|
|
1002
|
+
messages: limitedMessages,
|
|
1003
|
+
hasMore: finalFiltered.length > maxResults
|
|
1004
|
+
};
|
|
1005
|
+
this.setCachedResults(cacheKey, searchResult);
|
|
1006
|
+
return searchResult;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
// Limit results
|
|
1010
|
+
const limitedMessages = sortedMessages.slice(0, maxResults);
|
|
708
1011
|
const searchResult = {
|
|
709
|
-
messages,
|
|
710
|
-
hasMore:
|
|
1012
|
+
messages: limitedMessages,
|
|
1013
|
+
hasMore: sortedMessages.length > maxResults
|
|
711
1014
|
};
|
|
712
1015
|
this.setCachedResults(cacheKey, searchResult);
|
|
1016
|
+
logger.log(`🔍 Search completed after ${searchAttempts} attempts: ${limitedMessages.length} final results`);
|
|
1017
|
+
if (limitedMessages.length === 0) {
|
|
1018
|
+
logger.log(`❌ No results found despite ${searchAttempts} search strategies. Consider broadening search criteria.`);
|
|
1019
|
+
}
|
|
713
1020
|
return searchResult;
|
|
714
1021
|
}
|
|
715
1022
|
catch (error) {
|
|
716
|
-
logger.error('Error in email search:', error);
|
|
717
|
-
|
|
1023
|
+
logger.error('Error in persistent email search:', error);
|
|
1024
|
+
// Final fallback - get some recent emails and filter them
|
|
1025
|
+
logger.log('🔄 Final fallback: getting recent emails to filter manually');
|
|
1026
|
+
return await this.performBasicSearch(criteria);
|
|
718
1027
|
}
|
|
719
1028
|
}, 'searchEmails');
|
|
720
1029
|
}
|
|
721
1030
|
/**
|
|
722
|
-
*
|
|
1031
|
+
* Resolve UNKNOWN message IDs by finding messages using alternative search criteria
|
|
723
1032
|
*/
|
|
724
|
-
|
|
1033
|
+
async resolveUnknownMessageIds(messages, criteria) {
|
|
1034
|
+
try {
|
|
1035
|
+
const graphClient = await this.getGraphClient();
|
|
1036
|
+
const resolvedMessages = [];
|
|
1037
|
+
for (const message of messages) {
|
|
1038
|
+
if (message.id === 'UNKNOWN') {
|
|
1039
|
+
// Try to find this message using subject and sender
|
|
1040
|
+
try {
|
|
1041
|
+
const searchFilter = [];
|
|
1042
|
+
if (message.subject) {
|
|
1043
|
+
searchFilter.push(`subject eq '${message.subject.replace(/'/g, "''")}'`);
|
|
1044
|
+
}
|
|
1045
|
+
if (message.from?.address) {
|
|
1046
|
+
searchFilter.push(`from/emailAddress/address eq '${message.from.address}'`);
|
|
1047
|
+
}
|
|
1048
|
+
if (searchFilter.length > 0) {
|
|
1049
|
+
const result = await graphClient
|
|
1050
|
+
.api('/me/messages')
|
|
1051
|
+
.filter(searchFilter.join(' and '))
|
|
1052
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1053
|
+
.top(1)
|
|
1054
|
+
.get();
|
|
1055
|
+
if (result.value && result.value.length > 0) {
|
|
1056
|
+
const resolvedMessage = this.mapEmailResult(result.value[0]);
|
|
1057
|
+
logger.log(`✅ Resolved UNKNOWN ID to: ${resolvedMessage.id}`);
|
|
1058
|
+
resolvedMessages.push(resolvedMessage);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
catch (resolveError) {
|
|
1064
|
+
logger.log(`⚠️ Could not resolve message ID: ${resolveError}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// If we couldn't resolve it, keep the original message
|
|
1068
|
+
resolvedMessages.push(message);
|
|
1069
|
+
}
|
|
1070
|
+
return resolvedMessages;
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
logger.log(`⚠️ Error resolving unknown message IDs: ${error}`);
|
|
1074
|
+
return messages; // Return original messages if resolution fails
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Use Microsoft Graph Search API for full-text search
|
|
1079
|
+
*/
|
|
1080
|
+
async performGraphSearch(query, maxResults) {
|
|
1081
|
+
try {
|
|
1082
|
+
const graphClient = await this.getGraphClient();
|
|
1083
|
+
const searchRequest = {
|
|
1084
|
+
requests: [
|
|
1085
|
+
{
|
|
1086
|
+
entityTypes: ['message'],
|
|
1087
|
+
query: {
|
|
1088
|
+
queryString: query
|
|
1089
|
+
},
|
|
1090
|
+
from: 0,
|
|
1091
|
+
size: Math.min(maxResults, 1000), // Graph Search max is 1000
|
|
1092
|
+
fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
|
|
1093
|
+
}
|
|
1094
|
+
]
|
|
1095
|
+
};
|
|
1096
|
+
logger.log(`🔍 Graph Search request:`, JSON.stringify(searchRequest, null, 2));
|
|
1097
|
+
const result = await graphClient
|
|
1098
|
+
.api('/search/query')
|
|
1099
|
+
.post(searchRequest);
|
|
1100
|
+
logger.log(`🔍 Graph Search raw response:`, JSON.stringify(result, null, 2));
|
|
1101
|
+
const messages = [];
|
|
1102
|
+
if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
|
|
1103
|
+
logger.log(`🔍 Processing ${result.value[0].hitsContainers[0].hits.length} hits`);
|
|
1104
|
+
for (const hit of result.value[0].hitsContainers[0].hits) {
|
|
1105
|
+
const email = hit.resource;
|
|
1106
|
+
logger.log(`🔍 Raw email from Graph Search:`, JSON.stringify(email, null, 2));
|
|
1107
|
+
const mappedEmail = this.mapEmailResult(email);
|
|
1108
|
+
logger.log(`🔍 Mapped email:`, JSON.stringify(mappedEmail, null, 2));
|
|
1109
|
+
messages.push(mappedEmail);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
else {
|
|
1113
|
+
logger.log(`🔍 No hits found in Graph Search response structure`);
|
|
1114
|
+
}
|
|
1115
|
+
logger.log(`📧 Graph Search returned ${messages.length} results`);
|
|
1116
|
+
return messages;
|
|
1117
|
+
}
|
|
1118
|
+
catch (error) {
|
|
1119
|
+
logger.log(`❌ Graph Search failed: ${error}. Falling back to alternative search.`);
|
|
1120
|
+
logger.error(`❌ Graph Search error details:`, JSON.stringify(error, null, 2));
|
|
1121
|
+
return [];
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Build KQL (Keyword Query Language) for advanced searches
|
|
1126
|
+
*/
|
|
1127
|
+
buildKQLQuery(criteria) {
|
|
1128
|
+
const kqlParts = [];
|
|
1129
|
+
if (criteria.from) {
|
|
1130
|
+
// Smart sender search - handles partial names, emails, display names
|
|
1131
|
+
const fromTerm = criteria.from.trim();
|
|
1132
|
+
if (fromTerm.includes('@')) {
|
|
1133
|
+
kqlParts.push(`from:${fromTerm}`);
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
// For names, search in both from field and sender
|
|
1137
|
+
kqlParts.push(`(from:"${fromTerm}" OR sender:"${fromTerm}" OR from:*${fromTerm}*)`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (criteria.to) {
|
|
1141
|
+
kqlParts.push(`to:${criteria.to}`);
|
|
1142
|
+
}
|
|
1143
|
+
if (criteria.cc) {
|
|
1144
|
+
kqlParts.push(`cc:${criteria.cc}`);
|
|
1145
|
+
}
|
|
1146
|
+
if (criteria.subject) {
|
|
1147
|
+
kqlParts.push(`subject:"${criteria.subject}"`);
|
|
1148
|
+
}
|
|
1149
|
+
if (criteria.hasAttachment === true) {
|
|
1150
|
+
kqlParts.push('hasattachment:true');
|
|
1151
|
+
}
|
|
1152
|
+
else if (criteria.hasAttachment === false) {
|
|
1153
|
+
kqlParts.push('hasattachment:false');
|
|
1154
|
+
}
|
|
1155
|
+
if (criteria.isUnread === true) {
|
|
1156
|
+
kqlParts.push('isread:false');
|
|
1157
|
+
}
|
|
1158
|
+
else if (criteria.isUnread === false) {
|
|
1159
|
+
kqlParts.push('isread:true');
|
|
1160
|
+
}
|
|
1161
|
+
if (criteria.importance) {
|
|
1162
|
+
kqlParts.push(`importance:${criteria.importance}`);
|
|
1163
|
+
}
|
|
1164
|
+
if (criteria.after) {
|
|
1165
|
+
const afterDate = new Date(criteria.after).toISOString().split('T')[0];
|
|
1166
|
+
kqlParts.push(`received>=${afterDate}`);
|
|
1167
|
+
}
|
|
1168
|
+
if (criteria.before) {
|
|
1169
|
+
const beforeDate = new Date(criteria.before).toISOString().split('T')[0];
|
|
1170
|
+
kqlParts.push(`received<=${beforeDate}`);
|
|
1171
|
+
}
|
|
1172
|
+
if (criteria.folder) {
|
|
1173
|
+
kqlParts.push(`foldernames:"${criteria.folder}"`);
|
|
1174
|
+
}
|
|
1175
|
+
return kqlParts.join(' AND ');
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Perform KQL-based search using Graph Search API
|
|
1179
|
+
*/
|
|
1180
|
+
async performKQLSearch(kqlQuery, maxResults) {
|
|
1181
|
+
try {
|
|
1182
|
+
const graphClient = await this.getGraphClient();
|
|
1183
|
+
const searchRequest = {
|
|
1184
|
+
requests: [
|
|
1185
|
+
{
|
|
1186
|
+
entityTypes: ['message'],
|
|
1187
|
+
query: {
|
|
1188
|
+
queryString: kqlQuery
|
|
1189
|
+
},
|
|
1190
|
+
from: 0,
|
|
1191
|
+
size: Math.min(maxResults, 1000),
|
|
1192
|
+
fields: ['id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'bodyPreview', 'isRead', 'hasAttachments', 'importance', 'conversationId', 'parentFolderId', 'webLink']
|
|
1193
|
+
}
|
|
1194
|
+
]
|
|
1195
|
+
};
|
|
1196
|
+
const result = await graphClient
|
|
1197
|
+
.api('/search/query')
|
|
1198
|
+
.post(searchRequest);
|
|
1199
|
+
const messages = [];
|
|
1200
|
+
if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
|
|
1201
|
+
for (const hit of result.value[0].hitsContainers[0].hits) {
|
|
1202
|
+
const email = hit.resource;
|
|
1203
|
+
messages.push(this.mapEmailResult(email));
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
logger.log(`📧 KQL Search returned ${messages.length} results`);
|
|
1207
|
+
return messages;
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
logger.log(`❌ KQL Search failed: ${error}. Falling back to filter search.`);
|
|
1211
|
+
return [];
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Fallback to OData filter-based search
|
|
1216
|
+
*/
|
|
1217
|
+
async performFilteredSearch(criteria, maxResults) {
|
|
1218
|
+
try {
|
|
1219
|
+
const graphClient = await this.getGraphClient();
|
|
1220
|
+
let apiCall = graphClient
|
|
1221
|
+
.api('/me/messages')
|
|
1222
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1223
|
+
.orderby('receivedDateTime desc')
|
|
1224
|
+
.top(Math.min(maxResults, 999));
|
|
1225
|
+
// Apply OData filters where possible
|
|
1226
|
+
const filters = [];
|
|
1227
|
+
if (criteria.from && criteria.from.includes('@')) {
|
|
1228
|
+
filters.push(`from/emailAddress/address eq '${criteria.from}'`);
|
|
1229
|
+
}
|
|
1230
|
+
if (criteria.to && criteria.to.includes('@')) {
|
|
1231
|
+
filters.push(`toRecipients/any(r: r/emailAddress/address eq '${criteria.to}')`);
|
|
1232
|
+
}
|
|
1233
|
+
if (criteria.isUnread !== undefined) {
|
|
1234
|
+
filters.push(`isRead eq ${!criteria.isUnread}`);
|
|
1235
|
+
}
|
|
1236
|
+
if (criteria.hasAttachment !== undefined) {
|
|
1237
|
+
filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
|
|
1238
|
+
}
|
|
1239
|
+
if (criteria.importance) {
|
|
1240
|
+
filters.push(`importance eq '${criteria.importance}'`);
|
|
1241
|
+
}
|
|
1242
|
+
if (filters.length > 0) {
|
|
1243
|
+
apiCall = apiCall.filter(filters.join(' and '));
|
|
1244
|
+
}
|
|
1245
|
+
// Apply folder filter using specific folder API
|
|
1246
|
+
if (criteria.folder) {
|
|
1247
|
+
const folders = await this.findFolderByName(criteria.folder);
|
|
1248
|
+
if (folders.length > 0) {
|
|
1249
|
+
apiCall = graphClient
|
|
1250
|
+
.api(`/me/mailFolders/${folders[0].id}/messages`)
|
|
1251
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1252
|
+
.orderby('receivedDateTime desc')
|
|
1253
|
+
.top(Math.min(maxResults, 999));
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const result = await apiCall.get();
|
|
1257
|
+
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
1258
|
+
logger.log(`📧 Filtered Search returned ${messages.length} results`);
|
|
1259
|
+
return messages;
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
logger.log(`❌ Filtered Search failed: ${error}`);
|
|
1263
|
+
return [];
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Fallback to basic search (original implementation)
|
|
1268
|
+
*/
|
|
1269
|
+
async performBasicSearch(criteria) {
|
|
1270
|
+
logger.log('🔄 Using fallback basic search');
|
|
1271
|
+
const graphClient = await this.getGraphClient();
|
|
1272
|
+
const result = await graphClient
|
|
1273
|
+
.api('/me/messages')
|
|
1274
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
1275
|
+
.orderby('receivedDateTime desc')
|
|
1276
|
+
.top(criteria.maxResults || 50)
|
|
1277
|
+
.get();
|
|
1278
|
+
let messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
1279
|
+
messages = this.applyManualFiltering(messages, criteria);
|
|
1280
|
+
return {
|
|
1281
|
+
messages,
|
|
1282
|
+
hasMore: !!result['@odata.nextLink']
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Map email result to EmailInfo format
|
|
1287
|
+
*/
|
|
1288
|
+
mapEmailResult(email) {
|
|
1289
|
+
// Extract ID from various possible locations in the response
|
|
1290
|
+
let emailId = email.id;
|
|
1291
|
+
if (!emailId) {
|
|
1292
|
+
// Try alternative ID sources
|
|
1293
|
+
emailId = email['@odata.id'] ||
|
|
1294
|
+
email.internetMessageId ||
|
|
1295
|
+
email._id ||
|
|
1296
|
+
email.messageId ||
|
|
1297
|
+
email.resource?.id;
|
|
1298
|
+
// If still no ID, try to extract from webLink or other fields
|
|
1299
|
+
if (!emailId && email.webLink) {
|
|
1300
|
+
const linkMatch = email.webLink.match(/itemid=([^&]+)/i);
|
|
1301
|
+
if (linkMatch) {
|
|
1302
|
+
emailId = decodeURIComponent(linkMatch[1]);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
// Log the raw email object for debugging when ID is missing
|
|
1306
|
+
if (!emailId) {
|
|
1307
|
+
logger.log('DEBUG: Email object missing ID after all attempts:', JSON.stringify(email, null, 2));
|
|
1308
|
+
emailId = 'UNKNOWN';
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
logger.log(`DEBUG: Found email ID from alternative source: ${emailId}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
id: emailId,
|
|
1316
|
+
subject: email.subject || '',
|
|
1317
|
+
from: {
|
|
1318
|
+
name: email.from?.emailAddress?.name || '',
|
|
1319
|
+
address: email.from?.emailAddress?.address || ''
|
|
1320
|
+
},
|
|
1321
|
+
toRecipients: email.toRecipients?.map((recipient) => ({
|
|
1322
|
+
name: recipient.emailAddress?.name || '',
|
|
1323
|
+
address: recipient.emailAddress?.address || ''
|
|
1324
|
+
})) || [],
|
|
1325
|
+
ccRecipients: email.ccRecipients?.map((recipient) => ({
|
|
1326
|
+
name: recipient.emailAddress?.name || '',
|
|
1327
|
+
address: recipient.emailAddress?.address || ''
|
|
1328
|
+
})) || [],
|
|
1329
|
+
receivedDateTime: email.receivedDateTime,
|
|
1330
|
+
sentDateTime: email.sentDateTime,
|
|
1331
|
+
bodyPreview: email.bodyPreview || '',
|
|
1332
|
+
isRead: email.isRead || false,
|
|
1333
|
+
hasAttachments: email.hasAttachments || false,
|
|
1334
|
+
importance: email.importance || 'normal',
|
|
1335
|
+
conversationId: email.conversationId || '',
|
|
1336
|
+
parentFolderId: email.parentFolderId || '',
|
|
1337
|
+
webLink: email.webLink || '',
|
|
1338
|
+
attachments: []
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Remove duplicate messages based on ID
|
|
1343
|
+
*/
|
|
1344
|
+
removeDuplicateMessages(messages) {
|
|
1345
|
+
const seen = new Set();
|
|
725
1346
|
return messages.filter(message => {
|
|
726
|
-
|
|
1347
|
+
if (seen.has(message.id)) {
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
seen.add(message.id);
|
|
1351
|
+
return true;
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Apply advanced filtering with better logic
|
|
1356
|
+
*/
|
|
1357
|
+
applyAdvancedFiltering(messages, criteria) {
|
|
1358
|
+
return messages.filter(message => {
|
|
1359
|
+
// Enhanced text search across multiple fields
|
|
727
1360
|
if (criteria.query) {
|
|
728
|
-
const searchText = criteria.query.toLowerCase();
|
|
729
|
-
const
|
|
730
|
-
|
|
1361
|
+
const searchText = criteria.query.toLowerCase().trim();
|
|
1362
|
+
const searchableContent = [
|
|
1363
|
+
message.subject,
|
|
1364
|
+
message.bodyPreview,
|
|
1365
|
+
message.from.name,
|
|
1366
|
+
message.from.address,
|
|
1367
|
+
...message.toRecipients.map(r => `${r.name} ${r.address}`),
|
|
1368
|
+
...message.ccRecipients.map(r => `${r.name} ${r.address}`)
|
|
1369
|
+
].join(' ').toLowerCase();
|
|
1370
|
+
// Support multiple search terms (AND logic)
|
|
1371
|
+
const searchTerms = searchText.split(/\s+/).filter(term => term.length > 0);
|
|
1372
|
+
if (!searchTerms.every(term => searchableContent.includes(term))) {
|
|
731
1373
|
return false;
|
|
1374
|
+
}
|
|
732
1375
|
}
|
|
1376
|
+
// Enhanced sender search
|
|
733
1377
|
if (criteria.from) {
|
|
734
1378
|
const searchTerm = criteria.from.toLowerCase().trim();
|
|
735
1379
|
const fromName = message.from.name.toLowerCase();
|
|
736
1380
|
const fromAddress = message.from.address.toLowerCase();
|
|
737
|
-
// Multiple matching strategies for better partial name support
|
|
738
1381
|
const matches = [
|
|
739
|
-
// Direct name or email match
|
|
740
1382
|
fromName.includes(searchTerm),
|
|
741
1383
|
fromAddress.includes(searchTerm),
|
|
742
|
-
// Split search
|
|
1384
|
+
// Split name search
|
|
743
1385
|
searchTerm.split(/\s+/).every(part => fromName.includes(part)),
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
// Handle initials (e.g., "M Kumar" for "Madan Kumar")
|
|
751
|
-
searchTerm.split(/\s+/).length === 2 &&
|
|
752
|
-
fromName.split(/\s+/).length >= 2 &&
|
|
753
|
-
fromName.split(/\s+/)[0].startsWith(searchTerm.split(/\s+/)[0][0]) &&
|
|
754
|
-
fromName.includes(searchTerm.split(/\s+/)[1])
|
|
1386
|
+
// Word boundary search
|
|
1387
|
+
new RegExp(`\\b${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i').test(fromName),
|
|
1388
|
+
// Email domain search
|
|
1389
|
+
searchTerm.includes('@') && fromAddress === searchTerm,
|
|
1390
|
+
// Partial email search
|
|
1391
|
+
!searchTerm.includes('@') && fromAddress.includes(searchTerm)
|
|
755
1392
|
];
|
|
756
1393
|
if (!matches.some(match => match))
|
|
757
1394
|
return false;
|
|
758
1395
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (!toMatch)
|
|
762
|
-
return false;
|
|
763
|
-
}
|
|
764
|
-
if (criteria.cc) {
|
|
765
|
-
// Handle case where ccRecipients might be undefined
|
|
766
|
-
const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
|
|
767
|
-
message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
|
|
768
|
-
if (!ccMatch)
|
|
769
|
-
return false;
|
|
770
|
-
}
|
|
771
|
-
if (criteria.subject) {
|
|
772
|
-
if (!message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
773
|
-
return false;
|
|
774
|
-
}
|
|
775
|
-
// Apply date filters
|
|
776
|
-
if (criteria.after) {
|
|
777
|
-
const afterDate = new Date(criteria.after);
|
|
1396
|
+
// Date range filters
|
|
1397
|
+
if (criteria.after || criteria.before) {
|
|
778
1398
|
const messageDate = new Date(message.receivedDateTime);
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1399
|
+
if (criteria.after) {
|
|
1400
|
+
const afterDate = new Date(criteria.after);
|
|
1401
|
+
if (messageDate < afterDate)
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
if (criteria.before) {
|
|
1405
|
+
const beforeDate = new Date(criteria.before);
|
|
1406
|
+
if (messageDate > beforeDate)
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
787
1409
|
}
|
|
788
|
-
//
|
|
789
|
-
if (criteria.
|
|
1410
|
+
// Other filters remain the same but are more robust
|
|
1411
|
+
if (criteria.to && !message.toRecipients.some(r => r.address.toLowerCase().includes(criteria.to.toLowerCase())))
|
|
790
1412
|
return false;
|
|
791
|
-
|
|
792
|
-
// Apply read status filter
|
|
793
|
-
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread) {
|
|
1413
|
+
if (criteria.cc && (!message.ccRecipients || !message.ccRecipients.some(r => r.address.toLowerCase().includes(criteria.cc.toLowerCase()))))
|
|
794
1414
|
return false;
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (criteria.
|
|
1415
|
+
if (criteria.subject && !message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
1416
|
+
return false;
|
|
1417
|
+
if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment)
|
|
1418
|
+
return false;
|
|
1419
|
+
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread)
|
|
1420
|
+
return false;
|
|
1421
|
+
if (criteria.importance && message.importance !== criteria.importance)
|
|
798
1422
|
return false;
|
|
799
|
-
}
|
|
800
1423
|
return true;
|
|
801
1424
|
});
|
|
802
1425
|
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Sort search results by relevance and date
|
|
1428
|
+
*/
|
|
1429
|
+
sortSearchResults(messages, criteria) {
|
|
1430
|
+
return messages.sort((a, b) => {
|
|
1431
|
+
// Calculate relevance score
|
|
1432
|
+
const scoreA = this.calculateRelevanceScore(a, criteria);
|
|
1433
|
+
const scoreB = this.calculateRelevanceScore(b, criteria);
|
|
1434
|
+
if (scoreA !== scoreB) {
|
|
1435
|
+
return scoreB - scoreA; // Higher score first
|
|
1436
|
+
}
|
|
1437
|
+
// If relevance is same, sort by date (newer first)
|
|
1438
|
+
return new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime();
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Calculate relevance score for search results
|
|
1443
|
+
*/
|
|
1444
|
+
calculateRelevanceScore(message, criteria) {
|
|
1445
|
+
let score = 0;
|
|
1446
|
+
if (criteria.query) {
|
|
1447
|
+
const query = criteria.query.toLowerCase();
|
|
1448
|
+
// Subject matches get higher score
|
|
1449
|
+
if (message.subject.toLowerCase().includes(query))
|
|
1450
|
+
score += 10;
|
|
1451
|
+
// Sender name matches
|
|
1452
|
+
if (message.from.name.toLowerCase().includes(query))
|
|
1453
|
+
score += 5;
|
|
1454
|
+
// Body preview matches
|
|
1455
|
+
if (message.bodyPreview.toLowerCase().includes(query))
|
|
1456
|
+
score += 3;
|
|
1457
|
+
// Exact word matches get bonus
|
|
1458
|
+
const words = query.split(/\s+/);
|
|
1459
|
+
words.forEach(word => {
|
|
1460
|
+
if (message.subject.toLowerCase().includes(word))
|
|
1461
|
+
score += 2;
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
// Recent emails get slight boost
|
|
1465
|
+
const daysOld = (Date.now() - new Date(message.receivedDateTime).getTime()) / (1000 * 60 * 60 * 24);
|
|
1466
|
+
if (daysOld < 7)
|
|
1467
|
+
score += 2;
|
|
1468
|
+
else if (daysOld < 30)
|
|
1469
|
+
score += 1;
|
|
1470
|
+
// Unread emails get boost
|
|
1471
|
+
if (!message.isRead)
|
|
1472
|
+
score += 1;
|
|
1473
|
+
// Important emails get boost
|
|
1474
|
+
if (message.importance === 'high')
|
|
1475
|
+
score += 3;
|
|
1476
|
+
return score;
|
|
1477
|
+
}
|
|
803
1478
|
/**
|
|
804
1479
|
* List emails in a folder
|
|
805
1480
|
*/
|
|
@@ -957,10 +1632,13 @@ export class MS365Operations {
|
|
|
957
1632
|
const graphClient = await this.getGraphClient();
|
|
958
1633
|
const allFolders = [];
|
|
959
1634
|
// Get top-level folders
|
|
1635
|
+
logger.log('📁 Fetching top-level mail folders...');
|
|
960
1636
|
const result = await graphClient
|
|
961
1637
|
.api('/me/mailFolders')
|
|
962
1638
|
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1639
|
+
.top(999) // Request up to 999 folders to avoid pagination
|
|
963
1640
|
.get();
|
|
1641
|
+
logger.log(`📁 Found ${result.value?.length || 0} top-level folders from API`);
|
|
964
1642
|
// Process top-level folders
|
|
965
1643
|
const topLevelFolders = result.value?.map((folder) => ({
|
|
966
1644
|
id: folder.id,
|
|
@@ -971,12 +1649,17 @@ export class MS365Operations {
|
|
|
971
1649
|
depth: 0,
|
|
972
1650
|
fullPath: folder.displayName || ''
|
|
973
1651
|
})) || [];
|
|
1652
|
+
logger.log(`📁 Processed ${topLevelFolders.length} top-level folders: ${topLevelFolders.map((f) => f.displayName).join(', ')}`);
|
|
974
1653
|
allFolders.push(...topLevelFolders);
|
|
975
1654
|
// Recursively get child folders for each top-level folder
|
|
1655
|
+
logger.log('📂 Starting recursive child folder discovery...');
|
|
976
1656
|
for (const folder of topLevelFolders) {
|
|
1657
|
+
logger.log(`📂 Checking children of: ${folder.displayName} (${folder.id})`);
|
|
977
1658
|
const childFolders = await this.getChildFolders(folder.id, folder.fullPath || folder.displayName, 1);
|
|
1659
|
+
logger.log(`📂 Found ${childFolders.length} child folders for ${folder.displayName}`);
|
|
978
1660
|
allFolders.push(...childFolders);
|
|
979
1661
|
}
|
|
1662
|
+
logger.log(`📁 Total folders discovered: ${allFolders.length}`);
|
|
980
1663
|
// Sort folders by full path for better organization
|
|
981
1664
|
allFolders.sort((a, b) => (a.fullPath || '').localeCompare(b.fullPath || ''));
|
|
982
1665
|
return allFolders;
|
|
@@ -993,11 +1676,14 @@ export class MS365Operations {
|
|
|
993
1676
|
try {
|
|
994
1677
|
const graphClient = await this.getGraphClient();
|
|
995
1678
|
const childFolders = [];
|
|
1679
|
+
logger.log(`📂 [Depth ${depth}] Getting child folders for: ${parentPath} (${parentFolderId})`);
|
|
996
1680
|
// Get child folders of the specified parent
|
|
997
1681
|
const result = await graphClient
|
|
998
1682
|
.api(`/me/mailFolders/${parentFolderId}/childFolders`)
|
|
999
1683
|
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1684
|
+
.top(999) // Request up to 999 child folders to avoid pagination
|
|
1000
1685
|
.get();
|
|
1686
|
+
logger.log(`📂 [Depth ${depth}] API returned ${result.value?.length || 0} child folders for ${parentPath}`);
|
|
1001
1687
|
if (result.value && result.value.length > 0) {
|
|
1002
1688
|
const folders = result.value.map((folder) => ({
|
|
1003
1689
|
id: folder.id,
|
|
@@ -1008,20 +1694,26 @@ export class MS365Operations {
|
|
|
1008
1694
|
depth,
|
|
1009
1695
|
fullPath: `${parentPath}/${folder.displayName || ''}`
|
|
1010
1696
|
}));
|
|
1697
|
+
logger.log(`📂 [Depth ${depth}] Mapped child folders: ${folders.map((f) => f.displayName).join(', ')}`);
|
|
1011
1698
|
childFolders.push(...folders);
|
|
1012
1699
|
// Recursively get child folders (limit depth to prevent infinite recursion)
|
|
1013
1700
|
if (depth < 10) { // Max depth of 10 levels
|
|
1701
|
+
logger.log(`📂 [Depth ${depth}] Recursing into ${folders.length} child folders...`);
|
|
1014
1702
|
for (const folder of folders) {
|
|
1015
1703
|
const subChildFolders = await this.getChildFolders(folder.id, folder.fullPath || folder.displayName, depth + 1);
|
|
1016
1704
|
childFolders.push(...subChildFolders);
|
|
1017
1705
|
}
|
|
1018
1706
|
}
|
|
1707
|
+
else {
|
|
1708
|
+
logger.log(`📂 [Depth ${depth}] Maximum depth reached, stopping recursion`);
|
|
1709
|
+
}
|
|
1019
1710
|
}
|
|
1711
|
+
logger.log(`📂 [Depth ${depth}] Returning ${childFolders.length} total child folders for ${parentPath}`);
|
|
1020
1712
|
return childFolders;
|
|
1021
1713
|
}
|
|
1022
1714
|
catch (error) {
|
|
1023
1715
|
// Log the error but don't throw - some folders might not have children or access might be restricted
|
|
1024
|
-
logger.log(
|
|
1716
|
+
logger.log(`❌ [Depth ${depth}] Could not access child folders for ${parentFolderId} (${parentPath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
1025
1717
|
return [];
|
|
1026
1718
|
}
|
|
1027
1719
|
}
|
|
@@ -1034,6 +1726,7 @@ export class MS365Operations {
|
|
|
1034
1726
|
const result = await graphClient
|
|
1035
1727
|
.api(`/me/mailFolders/${parentFolderId}/childFolders`)
|
|
1036
1728
|
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1729
|
+
.top(999) // Request up to 999 child folders to avoid pagination
|
|
1037
1730
|
.get();
|
|
1038
1731
|
return result.value?.map((folder) => ({
|
|
1039
1732
|
id: folder.id,
|
|
@@ -1337,5 +2030,286 @@ export class MS365Operations {
|
|
|
1337
2030
|
throw error;
|
|
1338
2031
|
}
|
|
1339
2032
|
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Debug method to check raw folder API response
|
|
2035
|
+
*/
|
|
2036
|
+
async debugFolders() {
|
|
2037
|
+
try {
|
|
2038
|
+
const graphClient = await this.getGraphClient();
|
|
2039
|
+
logger.log('🔍 DEBUG: Testing folder API calls...');
|
|
2040
|
+
// Test basic folder listing
|
|
2041
|
+
const basicResult = await graphClient
|
|
2042
|
+
.api('/me/mailFolders')
|
|
2043
|
+
.get();
|
|
2044
|
+
logger.log(`🔍 DEBUG: Raw /me/mailFolders response: ${JSON.stringify(basicResult, null, 2)}`);
|
|
2045
|
+
// Test with specific selection
|
|
2046
|
+
const selectResult = await graphClient
|
|
2047
|
+
.api('/me/mailFolders')
|
|
2048
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
2049
|
+
.get();
|
|
2050
|
+
logger.log(`🔍 DEBUG: Selected fields response: ${JSON.stringify(selectResult, null, 2)}`);
|
|
2051
|
+
// Test well-known folder access
|
|
2052
|
+
try {
|
|
2053
|
+
const inboxResult = await graphClient
|
|
2054
|
+
.api('/me/mailFolders/inbox/childFolders')
|
|
2055
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
2056
|
+
.get();
|
|
2057
|
+
logger.log(`🔍 DEBUG: Inbox children: ${JSON.stringify(inboxResult, null, 2)}`);
|
|
2058
|
+
}
|
|
2059
|
+
catch (inboxError) {
|
|
2060
|
+
logger.log(`🔍 DEBUG: Inbox children error: ${inboxError}`);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
catch (error) {
|
|
2064
|
+
logger.error('🔍 DEBUG: Error in debugFolders:', error);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Apply manual filtering to search results (used when $filter can't be used with $search)
|
|
2069
|
+
*/
|
|
2070
|
+
applyManualFiltering(messages, criteria) {
|
|
2071
|
+
return messages.filter(message => {
|
|
2072
|
+
// Apply text search filters manually
|
|
2073
|
+
if (criteria.query) {
|
|
2074
|
+
const searchText = criteria.query.toLowerCase();
|
|
2075
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
2076
|
+
if (!messageText.includes(searchText))
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
if (criteria.from) {
|
|
2080
|
+
const searchTerm = criteria.from.toLowerCase().trim();
|
|
2081
|
+
const fromName = message.from.name.toLowerCase();
|
|
2082
|
+
const fromAddress = message.from.address.toLowerCase();
|
|
2083
|
+
const matches = [
|
|
2084
|
+
fromName.includes(searchTerm),
|
|
2085
|
+
fromAddress.includes(searchTerm),
|
|
2086
|
+
searchTerm.split(/\s+/).every(part => fromName.includes(part)),
|
|
2087
|
+
fromName.split(/\s+/).some(namePart => namePart.startsWith(searchTerm))
|
|
2088
|
+
];
|
|
2089
|
+
if (!matches.some(match => match))
|
|
2090
|
+
return false;
|
|
2091
|
+
}
|
|
2092
|
+
if (criteria.to) {
|
|
2093
|
+
const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
|
|
2094
|
+
if (!toMatch)
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
if (criteria.cc) {
|
|
2098
|
+
const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
|
|
2099
|
+
message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
|
|
2100
|
+
if (!ccMatch)
|
|
2101
|
+
return false;
|
|
2102
|
+
}
|
|
2103
|
+
if (criteria.subject) {
|
|
2104
|
+
if (!message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
if (criteria.after) {
|
|
2108
|
+
const afterDate = new Date(criteria.after);
|
|
2109
|
+
const messageDate = new Date(message.receivedDateTime);
|
|
2110
|
+
if (messageDate < afterDate)
|
|
2111
|
+
return false;
|
|
2112
|
+
}
|
|
2113
|
+
if (criteria.before) {
|
|
2114
|
+
const beforeDate = new Date(criteria.before);
|
|
2115
|
+
const messageDate = new Date(message.receivedDateTime);
|
|
2116
|
+
if (messageDate > beforeDate)
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment) {
|
|
2120
|
+
return false;
|
|
2121
|
+
}
|
|
2122
|
+
if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread) {
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
if (criteria.importance && message.importance !== criteria.importance) {
|
|
2126
|
+
return false;
|
|
2127
|
+
}
|
|
2128
|
+
return true;
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Build relaxed KQL query with fewer constraints
|
|
2133
|
+
*/
|
|
2134
|
+
buildRelaxedKQLQuery(criteria) {
|
|
2135
|
+
const kqlParts = [];
|
|
2136
|
+
// Only include the most important criteria for relaxed search
|
|
2137
|
+
if (criteria.from) {
|
|
2138
|
+
const fromTerm = criteria.from.trim();
|
|
2139
|
+
if (fromTerm.includes('@')) {
|
|
2140
|
+
// For email addresses, try domain search too
|
|
2141
|
+
const domain = fromTerm.split('@')[1];
|
|
2142
|
+
if (domain) {
|
|
2143
|
+
kqlParts.push(`(from:${fromTerm} OR from:*@${domain})`);
|
|
2144
|
+
}
|
|
2145
|
+
else {
|
|
2146
|
+
kqlParts.push(`from:${fromTerm}`);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
else {
|
|
2150
|
+
// For names, use wildcard search
|
|
2151
|
+
kqlParts.push(`from:*${fromTerm}*`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (criteria.subject) {
|
|
2155
|
+
// Use partial subject matching
|
|
2156
|
+
kqlParts.push(`subject:*${criteria.subject}*`);
|
|
2157
|
+
}
|
|
2158
|
+
// Skip attachment and read status filters in relaxed mode
|
|
2159
|
+
// Keep only critical filters
|
|
2160
|
+
if (criteria.importance === 'high') {
|
|
2161
|
+
kqlParts.push(`importance:high`);
|
|
2162
|
+
}
|
|
2163
|
+
// Expand date range for relaxed search
|
|
2164
|
+
if (criteria.after) {
|
|
2165
|
+
const afterDate = new Date(criteria.after);
|
|
2166
|
+
afterDate.setDate(afterDate.getDate() - 7); // Expand by a week
|
|
2167
|
+
const expandedDate = afterDate.toISOString().split('T')[0];
|
|
2168
|
+
kqlParts.push(`received>=${expandedDate}`);
|
|
2169
|
+
}
|
|
2170
|
+
return kqlParts.join(' AND ');
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Perform partial text search across recent emails with broader matching
|
|
2174
|
+
*/
|
|
2175
|
+
async performPartialTextSearch(criteria, maxResults) {
|
|
2176
|
+
try {
|
|
2177
|
+
const graphClient = await this.getGraphClient();
|
|
2178
|
+
// Get a large set of recent emails to search through
|
|
2179
|
+
const result = await graphClient
|
|
2180
|
+
.api('/me/messages')
|
|
2181
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
2182
|
+
.orderby('receivedDateTime desc')
|
|
2183
|
+
.top(Math.min(maxResults, 1000))
|
|
2184
|
+
.get();
|
|
2185
|
+
const messages = result.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
2186
|
+
// Apply very broad partial matching
|
|
2187
|
+
const partialMatches = messages.filter(message => {
|
|
2188
|
+
let matches = true;
|
|
2189
|
+
// Very flexible text search
|
|
2190
|
+
if (criteria.query) {
|
|
2191
|
+
const searchTerms = criteria.query.toLowerCase().split(/\s+/);
|
|
2192
|
+
const searchableText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
|
|
2193
|
+
// At least 50% of search terms should match
|
|
2194
|
+
const matchingTerms = searchTerms.filter(term => searchableText.includes(term));
|
|
2195
|
+
matches = matchingTerms.length >= Math.ceil(searchTerms.length * 0.5);
|
|
2196
|
+
}
|
|
2197
|
+
// Very flexible sender search
|
|
2198
|
+
if (criteria.from && matches) {
|
|
2199
|
+
const fromTerm = criteria.from.toLowerCase();
|
|
2200
|
+
const fromText = `${message.from.name} ${message.from.address}`.toLowerCase();
|
|
2201
|
+
// Partial matching with character-level similarity
|
|
2202
|
+
matches = fromText.includes(fromTerm) ||
|
|
2203
|
+
fromTerm.split('').some(char => fromText.includes(char)) ||
|
|
2204
|
+
this.calculateStringSimilarity(fromTerm, fromText) > 0.3;
|
|
2205
|
+
}
|
|
2206
|
+
return matches;
|
|
2207
|
+
});
|
|
2208
|
+
logger.log(`📧 Partial text search found ${partialMatches.length} potential matches`);
|
|
2209
|
+
return partialMatches;
|
|
2210
|
+
}
|
|
2211
|
+
catch (error) {
|
|
2212
|
+
logger.log(`❌ Partial text search failed: ${error}`);
|
|
2213
|
+
return [];
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Ultra-relaxed search that casts a very wide net
|
|
2218
|
+
*/
|
|
2219
|
+
async performUltraRelaxedSearch(criteria, maxResults) {
|
|
2220
|
+
try {
|
|
2221
|
+
const graphClient = await this.getGraphClient();
|
|
2222
|
+
// Search across multiple folders and time ranges
|
|
2223
|
+
const searches = [];
|
|
2224
|
+
// Search recent emails
|
|
2225
|
+
searches.push(graphClient
|
|
2226
|
+
.api('/me/messages')
|
|
2227
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
2228
|
+
.orderby('receivedDateTime desc')
|
|
2229
|
+
.top(500)
|
|
2230
|
+
.get());
|
|
2231
|
+
// Search sent items if looking for specific people
|
|
2232
|
+
if (criteria.from || criteria.to) {
|
|
2233
|
+
searches.push(graphClient
|
|
2234
|
+
.api('/me/mailFolders/sentitems/messages')
|
|
2235
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
2236
|
+
.orderby('receivedDateTime desc')
|
|
2237
|
+
.top(200)
|
|
2238
|
+
.get());
|
|
2239
|
+
}
|
|
2240
|
+
const results = await Promise.allSettled(searches);
|
|
2241
|
+
const allEmails = [];
|
|
2242
|
+
results.forEach((result, index) => {
|
|
2243
|
+
if (result.status === 'fulfilled') {
|
|
2244
|
+
const emails = result.value.value?.map((email) => this.mapEmailResult(email)) || [];
|
|
2245
|
+
allEmails.push(...emails);
|
|
2246
|
+
logger.log(`📧 Ultra-relaxed search ${index + 1} found ${emails.length} emails`);
|
|
2247
|
+
}
|
|
2248
|
+
});
|
|
2249
|
+
return this.removeDuplicateMessages(allEmails);
|
|
2250
|
+
}
|
|
2251
|
+
catch (error) {
|
|
2252
|
+
logger.log(`❌ Ultra-relaxed search failed: ${error}`);
|
|
2253
|
+
return [];
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Ultra-relaxed filtering with very permissive matching
|
|
2258
|
+
*/
|
|
2259
|
+
applyUltraRelaxedFiltering(messages, criteria) {
|
|
2260
|
+
return messages.filter(message => {
|
|
2261
|
+
let score = 0;
|
|
2262
|
+
let hasAnyMatch = false;
|
|
2263
|
+
// Any partial query match
|
|
2264
|
+
if (criteria.query) {
|
|
2265
|
+
const searchText = criteria.query.toLowerCase();
|
|
2266
|
+
const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name}`.toLowerCase();
|
|
2267
|
+
// Check for any word matches
|
|
2268
|
+
const queryWords = searchText.split(/\s+/);
|
|
2269
|
+
const matchingWords = queryWords.filter(word => messageText.includes(word));
|
|
2270
|
+
if (matchingWords.length > 0) {
|
|
2271
|
+
hasAnyMatch = true;
|
|
2272
|
+
score += matchingWords.length;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
// Any sender similarity
|
|
2276
|
+
if (criteria.from) {
|
|
2277
|
+
const fromTerm = criteria.from.toLowerCase();
|
|
2278
|
+
const fromText = `${message.from.name} ${message.from.address}`.toLowerCase();
|
|
2279
|
+
if (fromText.includes(fromTerm) ||
|
|
2280
|
+
this.calculateStringSimilarity(fromTerm, fromText) > 0.2) {
|
|
2281
|
+
hasAnyMatch = true;
|
|
2282
|
+
score += 2;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
// Any subject similarity
|
|
2286
|
+
if (criteria.subject) {
|
|
2287
|
+
const subjectTerm = criteria.subject.toLowerCase();
|
|
2288
|
+
if (message.subject.toLowerCase().includes(subjectTerm)) {
|
|
2289
|
+
hasAnyMatch = true;
|
|
2290
|
+
score += 3;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
// If no specific criteria, return recent emails
|
|
2294
|
+
if (!criteria.query && !criteria.from && !criteria.subject) {
|
|
2295
|
+
hasAnyMatch = true;
|
|
2296
|
+
}
|
|
2297
|
+
return hasAnyMatch;
|
|
2298
|
+
}).sort((a, b) => {
|
|
2299
|
+
// Sort by date for ultra-relaxed results
|
|
2300
|
+
return new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime();
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Calculate string similarity (simple version)
|
|
2305
|
+
*/
|
|
2306
|
+
calculateStringSimilarity(str1, str2) {
|
|
2307
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
2308
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
2309
|
+
if (longer.length === 0)
|
|
2310
|
+
return 1.0;
|
|
2311
|
+
const matches = shorter.split('').filter(char => longer.includes(char)).length;
|
|
2312
|
+
return matches / longer.length;
|
|
2313
|
+
}
|
|
1340
2314
|
}
|
|
1341
2315
|
export const ms365Operations = new MS365Operations();
|