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.
@@ -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 emails with criteria
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
- // For complex searches, use a simpler approach
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
- // Start with a basic query to get recent emails
442
- const apiCall = graphClient.api('/me/messages')
443
- .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
444
- .orderby('receivedDateTime desc')
445
- .top(criteria.maxResults || 100);
446
- const result = await apiCall.get();
447
- let messages = result.value?.map((email) => ({
448
- id: email.id,
449
- subject: email.subject || '',
450
- from: {
451
- name: email.from?.emailAddress?.name || '',
452
- address: email.from?.emailAddress?.address || ''
453
- },
454
- toRecipients: email.toRecipients?.map((recipient) => ({
455
- name: recipient.emailAddress?.name || '',
456
- address: recipient.emailAddress?.address || ''
457
- })) || [],
458
- ccRecipients: email.ccRecipients?.map((recipient) => ({
459
- name: recipient.emailAddress?.name || '',
460
- address: recipient.emailAddress?.address || ''
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
- catch (error) {
485
- logger.error(`Error getting attachment count for message ${message.id}:`, error);
486
- message.attachments = [];
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: !!result['@odata.nextLink']
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
- throw error;
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
- * Apply manual filtering to search results (used when $filter can't be used with $search)
777
+ * Use Microsoft Graph Search API for full-text search
505
778
  */
506
- applyManualFiltering(messages, criteria) {
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
- // Apply text search filters manually
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 messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
512
- if (!messageText.includes(searchText))
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 term and check if all parts exist in name
1048
+ // Split name search
525
1049
  searchTerm.split(/\s+/).every(part => fromName.includes(part)),
526
- // Check if any word in the name starts with the search term
527
- fromName.split(/\s+/).some(namePart => namePart.startsWith(searchTerm)),
528
- // Check if search term matches any word in the name exactly
529
- fromName.split(/\s+/).some(namePart => namePart === searchTerm),
530
- // Handle "Last, First" format
531
- fromName.replace(/,\s*/g, ' ').includes(searchTerm),
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
- if (criteria.to) {
542
- const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
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 (messageDate > beforeDate)
568
- return false;
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
- // Apply attachment filter
571
- if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment) {
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
- // Apply importance filter
579
- if (criteria.importance && message.importance !== criteria.importance) {
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('Error listing folders:', 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();