ms365-mcp-server 1.1.11 → 1.1.13

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 CHANGED
@@ -67,7 +67,7 @@ function parseArgs() {
67
67
  }
68
68
  const server = new Server({
69
69
  name: "ms365-mcp-server",
70
- version: "1.1.11"
70
+ version: "1.1.13"
71
71
  }, {
72
72
  capabilities: {
73
73
  resources: {
@@ -83,6 +83,7 @@ const server = new Server({
83
83
  }
84
84
  }
85
85
  });
86
+ logger.log('Server started with version 1.1.13');
86
87
  // Set up the resource listing request handler
87
88
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
88
89
  logger.log('Received list resources request');
@@ -185,7 +186,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
185
186
  },
186
187
  {
187
188
  name: "manage_email",
188
- description: "UNIFIED EMAIL MANAGEMENT: Read, search, list, mark, move, or delete emails. Combines all email operations in one powerful tool. Supports intelligent partial name matching, folder management, and advanced search criteria.",
189
+ description: "UNIFIED EMAIL MANAGEMENT: Read, search, list, mark, move, or delete emails. Combines all email operations in one powerful tool. Supports intelligent partial name matching, folder management, and advanced search criteria. IMPORTANT: Use 'reply_draft' or 'forward_draft' actions (not 'draft') to create threaded drafts that appear in email conversations.",
189
190
  inputSchema: {
190
191
  type: "object",
191
192
  properties: {
@@ -196,7 +197,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
196
197
  action: {
197
198
  type: "string",
198
199
  enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me", "draft", "update_draft", "send_draft", "list_drafts", "reply_draft", "forward_draft"],
199
- description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create/save draft), update_draft (modify existing draft), send_draft (send saved draft), list_drafts (list draft emails), reply_draft (create reply draft), forward_draft (create forward draft)"
200
+ description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create standalone draft - NOTE: Use reply_draft/forward_draft for threaded drafts), update_draft (modify existing draft), send_draft (send saved draft), list_drafts (list draft emails), reply_draft (create threaded reply draft that appears in conversation), forward_draft (create threaded forward draft that appears in conversation)"
200
201
  },
201
202
  messageId: {
202
203
  type: "string",
@@ -631,6 +632,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
631
632
  }
632
633
  // ============ UNIFIED EMAIL MANAGEMENT TOOL ============
633
634
  case "manage_email":
635
+ // Debug logging for parameter validation
636
+ logger.log('DEBUG: manage_email called with args:', JSON.stringify(args, null, 2));
634
637
  if (ms365Config.multiUser) {
635
638
  const userId = args?.userId;
636
639
  if (!userId) {
@@ -644,6 +647,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
644
647
  ms365Ops.setGraphClient(graphClient);
645
648
  }
646
649
  const emailAction = args?.action;
650
+ logger.log(`DEBUG: Email action: ${emailAction}`);
647
651
  switch (emailAction) {
648
652
  case "read":
649
653
  if (!args?.messageId) {
@@ -681,6 +685,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
681
685
  };
682
686
  case "search":
683
687
  const searchResults = await ms365Ops.searchEmails(args);
688
+ logger.log(`DEBUG: Search results count: ${searchResults.messages.length}`);
689
+ logger.log(`DEBUG: First result:`, JSON.stringify(searchResults.messages[0], null, 2));
684
690
  // Enhanced feedback for search results
685
691
  let responseText = `šŸ” Email Search Results (${searchResults.messages.length} found)`;
686
692
  if (searchResults.messages.length === 0) {
@@ -698,7 +704,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
698
704
  responseText += `• Remove some search criteria to get broader results`;
699
705
  }
700
706
  else {
701
- responseText += `\n\n${searchResults.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'šŸ“– Read' : 'šŸ“© Unread'}\n šŸ†” ID: ${email.id}\n`).join('\n')}`;
707
+ responseText += `\n\n${searchResults.messages.map((email, index) => {
708
+ // Handle missing IDs more gracefully
709
+ const emailId = email.id || 'ID_MISSING';
710
+ const fromDisplay = email.from?.name ? `${email.from.name} <${email.from.address}>` : email.from?.address || 'Unknown sender';
711
+ return `${index + 1}. šŸ“§ ${email.subject || 'No subject'}\n šŸ‘¤ From: ${fromDisplay}\n šŸ“… ${email.receivedDateTime ? new Date(email.receivedDateTime).toLocaleDateString() : 'Unknown date'}\n ${email.isRead ? 'šŸ“– Read' : 'šŸ“© Unread'}\n šŸ†” ID: ${emailId}\n`;
712
+ }).join('\n')}`;
702
713
  if (searchResults.hasMore) {
703
714
  responseText += `\nšŸ’” There are more results available. Use maxResults parameter to get more emails.`;
704
715
  }
@@ -840,6 +851,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
840
851
  if (!args?.draftTo || !args?.draftSubject || !args?.draftBody) {
841
852
  throw new Error("draftTo, draftSubject, and draftBody are required for draft action");
842
853
  }
854
+ // Check if this looks like a reply based on subject
855
+ const isLikelyReply = args.draftSubject?.toString().toLowerCase().startsWith('re:');
856
+ if (isLikelyReply) {
857
+ logger.log('šŸ”” SUGGESTION: Consider using "reply_draft" action instead of "draft" for threaded replies that appear in conversations');
858
+ }
843
859
  const draftResult = await ms365Ops.saveDraftEmail({
844
860
  to: args.draftTo,
845
861
  cc: args.draftCc,
@@ -851,11 +867,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
851
867
  attachments: args.draftAttachments,
852
868
  conversationId: args.conversationId
853
869
  });
870
+ let draftResponseText = `āœ… Draft email saved successfully!\nšŸ“§ Subject: ${args.draftSubject}\nšŸ‘„ To: ${Array.isArray(args.draftTo) ? args.draftTo.join(', ') : args.draftTo}\nšŸ†” Draft ID: ${draftResult.id}`;
871
+ // Add helpful suggestion for threaded drafts
872
+ if (isLikelyReply) {
873
+ draftResponseText += `\n\nšŸ’” TIP: This looks like a reply email. For drafts that appear in email threads, use "reply_draft" action with the original message ID instead of "draft".`;
874
+ }
854
875
  return {
855
876
  content: [
856
877
  {
857
878
  type: "text",
858
- text: `āœ… Draft email saved successfully!\nšŸ“§ Subject: ${args.draftSubject}\nšŸ‘„ To: ${Array.isArray(args.draftTo) ? args.draftTo.join(', ') : args.draftTo}\nšŸ†” Draft ID: ${draftResult.id}`
879
+ text: draftResponseText
859
880
  }
860
881
  ]
861
882
  };
@@ -916,7 +937,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
916
937
  if (!args?.originalMessageId) {
917
938
  throw new Error("originalMessageId is required for reply_draft action");
918
939
  }
919
- const replyDraftResult = await ms365Ops.createReplyDraft(args.originalMessageId, args.draftBody, args.replyToAll || false);
940
+ const replyDraftResult = await ms365Ops.createReplyDraft(args.originalMessageId, args.draftBody, args.replyToAll || false, args.draftBodyType || 'text');
920
941
  return {
921
942
  content: [
922
943
  {
@@ -929,7 +950,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
929
950
  if (!args?.originalMessageId) {
930
951
  throw new Error("originalMessageId is required for forward_draft action");
931
952
  }
932
- const forwardDraftResult = await ms365Ops.createForwardDraft(args.originalMessageId, args.draftBody);
953
+ const forwardDraftResult = await ms365Ops.createForwardDraft(args.originalMessageId, args.draftBody, args.draftBodyType || 'text');
933
954
  return {
934
955
  content: [
935
956
  {
@@ -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
  */
@@ -474,30 +522,164 @@ export class MS365Operations {
474
522
  /**
475
523
  * Create a threaded reply draft from a specific message
476
524
  */
477
- async createReplyDraft(originalMessageId, body, replyToAll = false) {
525
+ async createReplyDraft(originalMessageId, body, replyToAll = false, bodyType = 'text') {
478
526
  try {
479
527
  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
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
+ // Helper function to escape HTML characters
540
+ const escapeHtml = (text) => {
541
+ return text
542
+ .replace(/&/g, '&amp;')
543
+ .replace(/</g, '&lt;')
544
+ .replace(/>/g, '&gt;')
545
+ .replace(/"/g, '&quot;')
546
+ .replace(/'/g, '&#x27;');
500
547
  };
548
+ // Helper function to convert text to HTML
549
+ const textToHtml = (text) => {
550
+ return escapeHtml(text).replace(/\n/g, '<br>');
551
+ };
552
+ // Process the user's body based on the specified type
553
+ let processedUserBody = body || '';
554
+ if (bodyType === 'text' && processedUserBody) {
555
+ processedUserBody = textToHtml(processedUserBody);
556
+ }
557
+ const completeReplyBody = `${processedUserBody}
558
+
559
+ <br><br>
560
+ <div style="border-left: 2px solid #ccc; padding-left: 10px; margin-top: 10px;">
561
+ <p><strong>From:</strong> ${fromDisplay}<br>
562
+ <strong>Sent:</strong> ${sentDate}<br>
563
+ <strong>Subject:</strong> ${originalMessage.subject}</p>
564
+ <hr style="border: none; border-top: 1px solid #ccc; margin: 10px 0;">
565
+ ${originalBodyContent}
566
+ </div>`;
567
+ // First, try using the official Microsoft Graph createReply endpoint for proper threading
568
+ try {
569
+ const endpoint = replyToAll
570
+ ? `/me/messages/${originalMessageId}/createReplyAll`
571
+ : `/me/messages/${originalMessageId}/createReply`;
572
+ logger.log(`Using official Graph API endpoint: ${endpoint}`);
573
+ const replyDraft = await graphClient
574
+ .api(endpoint)
575
+ .post({
576
+ message: {
577
+ body: {
578
+ contentType: 'html',
579
+ content: completeReplyBody
580
+ }
581
+ }
582
+ });
583
+ logger.log(`Reply draft created successfully with ID: ${replyDraft.id}`);
584
+ logger.log(`Draft conversation ID: ${replyDraft.conversationId}`);
585
+ logger.log(`Draft appears as threaded reply in conversation with original content`);
586
+ return {
587
+ id: replyDraft.id,
588
+ subject: replyDraft.subject,
589
+ conversationId: replyDraft.conversationId,
590
+ toRecipients: replyDraft.toRecipients?.map((r) => r.emailAddress.address),
591
+ ccRecipients: replyDraft.ccRecipients?.map((r) => r.emailAddress.address),
592
+ bodyPreview: replyDraft.bodyPreview,
593
+ isDraft: replyDraft.isDraft,
594
+ parentFolderId: replyDraft.parentFolderId
595
+ };
596
+ }
597
+ catch (officialApiError) {
598
+ logger.log(`Official API failed: ${officialApiError}, trying fallback approach`);
599
+ logger.log(`Fallback: Creating manual reply draft with enhanced threading`);
600
+ logger.log(`Original message conversation ID: ${originalMessage.conversationId}`);
601
+ logger.log(`Original message folder: ${originalMessage.parentFolderId}`);
602
+ // Build proper References header from existing chain
603
+ let referencesHeader = originalMessage.internetMessageId || originalMessage.id;
604
+ if (originalMessage.internetMessageHeaders) {
605
+ const existingReferences = originalMessage.internetMessageHeaders.find((h) => h.name.toLowerCase() === 'references');
606
+ if (existingReferences && existingReferences.value) {
607
+ referencesHeader = `${existingReferences.value} ${referencesHeader}`;
608
+ }
609
+ }
610
+ const currentUserEmail = await this.getCurrentUserEmail();
611
+ const draftBody = {
612
+ subject: originalMessage.subject?.startsWith('Re:') ? originalMessage.subject : `Re: ${originalMessage.subject}`,
613
+ body: {
614
+ contentType: 'html',
615
+ content: completeReplyBody
616
+ },
617
+ conversationId: originalMessage.conversationId,
618
+ internetMessageHeaders: [
619
+ {
620
+ name: 'X-In-Reply-To',
621
+ value: originalMessage.internetMessageId || originalMessage.id
622
+ },
623
+ {
624
+ name: 'X-References',
625
+ value: referencesHeader
626
+ },
627
+ {
628
+ name: 'X-Thread-Topic',
629
+ value: originalMessage.subject?.replace(/^Re:\s*/i, '') || ''
630
+ }
631
+ ]
632
+ };
633
+ // Include conversation index if available for proper Outlook threading
634
+ if (originalMessage.conversationIndex) {
635
+ draftBody.internetMessageHeaders.push({
636
+ name: 'X-Thread-Index',
637
+ value: originalMessage.conversationIndex
638
+ });
639
+ }
640
+ // Set recipients based on reply type
641
+ if (replyToAll) {
642
+ draftBody.toRecipients = [
643
+ ...(originalMessage.from ? [{ emailAddress: originalMessage.from.emailAddress }] : []),
644
+ ...(originalMessage.toRecipients || []).filter((r) => r.emailAddress.address !== currentUserEmail)
645
+ ];
646
+ draftBody.ccRecipients = originalMessage.ccRecipients || [];
647
+ }
648
+ else {
649
+ draftBody.toRecipients = originalMessage.from ? [{ emailAddress: originalMessage.from.emailAddress }] : [];
650
+ }
651
+ // Create the fallback draft
652
+ const replyDraft = await graphClient
653
+ .api('/me/messages')
654
+ .post(draftBody);
655
+ logger.log(`Fallback reply draft created with ID: ${replyDraft.id}`);
656
+ logger.log(`Draft conversation ID: ${replyDraft.conversationId}`);
657
+ // Try to move the draft to the same folder as the original message for better threading
658
+ if (originalMessage.parentFolderId && originalMessage.parentFolderId !== 'drafts') {
659
+ try {
660
+ await graphClient
661
+ .api(`/me/messages/${replyDraft.id}/move`)
662
+ .post({
663
+ destinationId: originalMessage.parentFolderId
664
+ });
665
+ logger.log(`Draft moved to original message folder: ${originalMessage.parentFolderId}`);
666
+ }
667
+ catch (moveError) {
668
+ logger.log(`Could not move draft to original folder: ${moveError}`);
669
+ // This is not critical, draft will remain in drafts folder
670
+ }
671
+ }
672
+ return {
673
+ id: replyDraft.id,
674
+ subject: replyDraft.subject,
675
+ conversationId: replyDraft.conversationId,
676
+ toRecipients: replyDraft.toRecipients?.map((r) => r.emailAddress.address),
677
+ ccRecipients: replyDraft.ccRecipients?.map((r) => r.emailAddress.address),
678
+ bodyPreview: replyDraft.bodyPreview,
679
+ isDraft: replyDraft.isDraft,
680
+ parentFolderId: originalMessage.parentFolderId
681
+ };
682
+ }
501
683
  }
502
684
  catch (error) {
503
685
  throw new Error(`Error creating reply draft: ${error}`);
@@ -506,26 +688,145 @@ export class MS365Operations {
506
688
  /**
507
689
  * Create a threaded forward draft from a specific message
508
690
  */
509
- async createForwardDraft(originalMessageId, comment) {
691
+ async createForwardDraft(originalMessageId, comment, bodyType = 'text') {
510
692
  try {
511
693
  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
- };
694
+ logger.log(`Creating forward draft for message: ${originalMessageId}`);
695
+ // First, try using the official Microsoft Graph createForward endpoint for proper threading
696
+ try {
697
+ logger.log(`Using official Graph API endpoint: /me/messages/${originalMessageId}/createForward`);
698
+ // Helper function to escape HTML characters
699
+ const escapeHtml = (text) => {
700
+ return text
701
+ .replace(/&/g, '&amp;')
702
+ .replace(/</g, '&lt;')
703
+ .replace(/>/g, '&gt;')
704
+ .replace(/"/g, '&quot;')
705
+ .replace(/'/g, '&#x27;');
706
+ };
707
+ // Helper function to convert text to HTML
708
+ const textToHtml = (text) => {
709
+ return escapeHtml(text).replace(/\n/g, '<br>');
710
+ };
711
+ // Process the comment based on the specified type
712
+ let processedComment = comment || '';
713
+ if (bodyType === 'text' && processedComment) {
714
+ processedComment = textToHtml(processedComment);
715
+ }
716
+ const forwardDraft = await graphClient
717
+ .api(`/me/messages/${originalMessageId}/createForward`)
718
+ .post({
719
+ message: {
720
+ body: {
721
+ contentType: 'html',
722
+ content: processedComment
723
+ }
724
+ }
725
+ });
726
+ logger.log(`Forward draft created successfully with ID: ${forwardDraft.id}`);
727
+ logger.log(`Draft conversation ID: ${forwardDraft.conversationId}`);
728
+ logger.log(`Draft appears as threaded forward in conversation`);
729
+ return {
730
+ id: forwardDraft.id,
731
+ subject: forwardDraft.subject,
732
+ conversationId: forwardDraft.conversationId,
733
+ bodyPreview: forwardDraft.bodyPreview,
734
+ isDraft: forwardDraft.isDraft,
735
+ parentFolderId: forwardDraft.parentFolderId
736
+ };
737
+ }
738
+ catch (officialApiError) {
739
+ logger.log(`Official API failed: ${officialApiError}, trying fallback approach`);
740
+ // Fallback to manual creation if the official endpoint fails
741
+ const originalMessage = await graphClient
742
+ .api(`/me/messages/${originalMessageId}`)
743
+ .select('id,subject,from,toRecipients,ccRecipients,conversationId,parentFolderId,internetMessageId,internetMessageHeaders,conversationIndex,body,sentDateTime')
744
+ .get();
745
+ logger.log(`Fallback: Creating manual forward draft with enhanced threading`);
746
+ logger.log(`Original message conversation ID: ${originalMessage.conversationId}`);
747
+ logger.log(`Original message folder: ${originalMessage.parentFolderId}`);
748
+ // Build proper References header from existing chain
749
+ let referencesHeader = originalMessage.internetMessageId || originalMessage.id;
750
+ if (originalMessage.internetMessageHeaders) {
751
+ const existingReferences = originalMessage.internetMessageHeaders.find((h) => h.name.toLowerCase() === 'references');
752
+ if (existingReferences && existingReferences.value) {
753
+ referencesHeader = `${existingReferences.value} ${referencesHeader}`;
754
+ }
755
+ }
756
+ // Helper function to escape HTML characters for fallback
757
+ const escapeHtml = (text) => {
758
+ return text
759
+ .replace(/&/g, '&amp;')
760
+ .replace(/</g, '&lt;')
761
+ .replace(/>/g, '&gt;')
762
+ .replace(/"/g, '&quot;')
763
+ .replace(/'/g, '&#x27;');
764
+ };
765
+ // Helper function to convert text to HTML for fallback
766
+ const textToHtml = (text) => {
767
+ return escapeHtml(text).replace(/\n/g, '<br>');
768
+ };
769
+ // Process the comment based on the specified type for fallback
770
+ let processedComment = comment || '';
771
+ if (bodyType === 'text' && processedComment) {
772
+ processedComment = textToHtml(processedComment);
773
+ }
774
+ const forwardedBody = `${processedComment ? processedComment + '<br><br>' : ''}---------- Forwarded message ----------<br>From: ${originalMessage.from?.emailAddress?.name || originalMessage.from?.emailAddress?.address}<br>Date: ${originalMessage.sentDateTime}<br>Subject: ${originalMessage.subject}<br>To: ${originalMessage.toRecipients?.map((r) => r.emailAddress.address).join(', ')}<br><br>${originalMessage.body?.content || ''}`;
775
+ const draftBody = {
776
+ subject: originalMessage.subject?.startsWith('Fwd:') ? originalMessage.subject : `Fwd: ${originalMessage.subject}`,
777
+ body: {
778
+ contentType: 'html',
779
+ content: forwardedBody
780
+ },
781
+ conversationId: originalMessage.conversationId,
782
+ internetMessageHeaders: [
783
+ {
784
+ name: 'X-References',
785
+ value: referencesHeader
786
+ },
787
+ {
788
+ name: 'X-Thread-Topic',
789
+ value: originalMessage.subject?.replace(/^(Re:|Fwd?):\s*/i, '') || ''
790
+ }
791
+ ]
792
+ };
793
+ // Include conversation index if available for proper Outlook threading
794
+ if (originalMessage.conversationIndex) {
795
+ draftBody.internetMessageHeaders.push({
796
+ name: 'X-Thread-Index',
797
+ value: originalMessage.conversationIndex
798
+ });
799
+ }
800
+ // Create the fallback draft
801
+ const forwardDraft = await graphClient
802
+ .api('/me/messages')
803
+ .post(draftBody);
804
+ logger.log(`Fallback forward draft created with ID: ${forwardDraft.id}`);
805
+ logger.log(`Draft conversation ID: ${forwardDraft.conversationId}`);
806
+ // Try to move the draft to the same folder as the original message for better threading
807
+ if (originalMessage.parentFolderId && originalMessage.parentFolderId !== 'drafts') {
808
+ try {
809
+ await graphClient
810
+ .api(`/me/messages/${forwardDraft.id}/move`)
811
+ .post({
812
+ destinationId: originalMessage.parentFolderId
813
+ });
814
+ logger.log(`Draft moved to original message folder: ${originalMessage.parentFolderId}`);
815
+ }
816
+ catch (moveError) {
817
+ logger.log(`Could not move draft to original folder: ${moveError}`);
818
+ // This is not critical, draft will remain in drafts folder
819
+ }
820
+ }
821
+ return {
822
+ id: forwardDraft.id,
823
+ subject: forwardDraft.subject,
824
+ conversationId: forwardDraft.conversationId,
825
+ bodyPreview: forwardDraft.bodyPreview,
826
+ isDraft: forwardDraft.isDraft,
827
+ parentFolderId: originalMessage.parentFolderId
828
+ };
829
+ }
529
830
  }
530
831
  catch (error) {
531
832
  throw new Error(`Error creating forward draft: ${error}`);
@@ -648,10 +949,12 @@ export class MS365Operations {
648
949
  async searchEmails(criteria = {}) {
649
950
  return await this.executeWithAuth(async () => {
650
951
  const graphClient = await this.getGraphClient();
952
+ logger.log(`šŸ” Starting email search with criteria:`, JSON.stringify(criteria, null, 2));
651
953
  // Create cache key from criteria
652
954
  const cacheKey = JSON.stringify(criteria);
653
955
  const cachedResults = this.getCachedResults(cacheKey);
654
956
  if (cachedResults) {
957
+ logger.log('šŸ“¦ Returning cached results');
655
958
  return cachedResults;
656
959
  }
657
960
  const maxResults = criteria.maxResults || 50;
@@ -660,8 +963,18 @@ export class MS365Operations {
660
963
  const maxAttempts = 6; // Try multiple strategies
661
964
  logger.log(`šŸ” Starting persistent search with criteria: ${JSON.stringify(criteria)}`);
662
965
  try {
663
- // Strategy 1: Use Microsoft Graph Search API for text-based queries
664
- if (criteria.query && searchAttempts < maxAttempts) {
966
+ // Strategy 1: Use reliable OData filter search first (has proper IDs)
967
+ if (searchAttempts < maxAttempts) {
968
+ searchAttempts++;
969
+ logger.log(`šŸ” Attempt ${searchAttempts}: Using reliable OData filter search first`);
970
+ const filterResults = await this.performFilteredSearch(criteria, maxResults * 2);
971
+ allMessages.push(...filterResults);
972
+ if (allMessages.length > 0) {
973
+ logger.log(`āœ… Found ${allMessages.length} results with OData filter search`);
974
+ }
975
+ }
976
+ // Strategy 2: Use Microsoft Graph Search API for text-based queries (backup)
977
+ if (allMessages.length === 0 && criteria.query && searchAttempts < maxAttempts) {
665
978
  searchAttempts++;
666
979
  logger.log(`šŸ” Attempt ${searchAttempts}: Using Graph Search API for query: "${criteria.query}"`);
667
980
  const searchResults = await this.performGraphSearch(criteria.query, maxResults * 2); // Search for more to increase chances
@@ -670,7 +983,7 @@ export class MS365Operations {
670
983
  logger.log(`āœ… Found ${allMessages.length} results with Graph Search API`);
671
984
  }
672
985
  }
673
- // Strategy 2: Use KQL (Keyword Query Language) for advanced searches
986
+ // Strategy 3: Use KQL (Keyword Query Language) for advanced searches
674
987
  if (allMessages.length === 0 && searchAttempts < maxAttempts) {
675
988
  const kqlQuery = this.buildKQLQuery(criteria);
676
989
  if (kqlQuery) {
@@ -683,7 +996,7 @@ export class MS365Operations {
683
996
  }
684
997
  }
685
998
  }
686
- // Strategy 3: Try relaxed KQL search (remove some constraints)
999
+ // Strategy 4: Try relaxed KQL search (remove some constraints)
687
1000
  if (allMessages.length === 0 && searchAttempts < maxAttempts) {
688
1001
  const relaxedKQL = this.buildRelaxedKQLQuery(criteria);
689
1002
  if (relaxedKQL && relaxedKQL !== this.buildKQLQuery(criteria)) {
@@ -696,17 +1009,12 @@ export class MS365Operations {
696
1009
  }
697
1010
  }
698
1011
  }
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
- }
1012
+ // Strategy 5: If we found results but they have UNKNOWN IDs, try to resolve them
1013
+ if (allMessages.length > 0 && allMessages.some(msg => msg.id === 'UNKNOWN')) {
1014
+ logger.log(`šŸ” Attempting to resolve UNKNOWN message IDs using direct message queries`);
1015
+ allMessages = await this.resolveUnknownMessageIds(allMessages, criteria);
708
1016
  }
709
- // Strategy 5: Partial text search across recent emails (expanded scope)
1017
+ // Strategy 6: Partial text search across recent emails (expanded scope)
710
1018
  if (allMessages.length === 0 && searchAttempts < maxAttempts) {
711
1019
  searchAttempts++;
712
1020
  logger.log(`šŸ” Attempt ${searchAttempts}: Using expanded partial text search`);
@@ -716,7 +1024,7 @@ export class MS365Operations {
716
1024
  logger.log(`āœ… Found ${allMessages.length} results with partial text search`);
717
1025
  }
718
1026
  }
719
- // Strategy 6: Fallback to basic search with maximum scope
1027
+ // Strategy 7: Fallback to basic search with maximum scope
720
1028
  if (allMessages.length === 0 && searchAttempts < maxAttempts) {
721
1029
  searchAttempts++;
722
1030
  logger.log(`šŸ” Attempt ${searchAttempts}: Using fallback basic search with maximum scope`);
@@ -773,6 +1081,53 @@ export class MS365Operations {
773
1081
  }
774
1082
  }, 'searchEmails');
775
1083
  }
1084
+ /**
1085
+ * Resolve UNKNOWN message IDs by finding messages using alternative search criteria
1086
+ */
1087
+ async resolveUnknownMessageIds(messages, criteria) {
1088
+ try {
1089
+ const graphClient = await this.getGraphClient();
1090
+ const resolvedMessages = [];
1091
+ for (const message of messages) {
1092
+ if (message.id === 'UNKNOWN') {
1093
+ // Try to find this message using subject and sender
1094
+ try {
1095
+ const searchFilter = [];
1096
+ if (message.subject) {
1097
+ searchFilter.push(`subject eq '${message.subject.replace(/'/g, "''")}'`);
1098
+ }
1099
+ if (message.from?.address) {
1100
+ searchFilter.push(`from/emailAddress/address eq '${message.from.address}'`);
1101
+ }
1102
+ if (searchFilter.length > 0) {
1103
+ const result = await graphClient
1104
+ .api('/me/messages')
1105
+ .filter(searchFilter.join(' and '))
1106
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
1107
+ .top(1)
1108
+ .get();
1109
+ if (result.value && result.value.length > 0) {
1110
+ const resolvedMessage = this.mapEmailResult(result.value[0]);
1111
+ logger.log(`āœ… Resolved UNKNOWN ID to: ${resolvedMessage.id}`);
1112
+ resolvedMessages.push(resolvedMessage);
1113
+ continue;
1114
+ }
1115
+ }
1116
+ }
1117
+ catch (resolveError) {
1118
+ logger.log(`āš ļø Could not resolve message ID: ${resolveError}`);
1119
+ }
1120
+ }
1121
+ // If we couldn't resolve it, keep the original message
1122
+ resolvedMessages.push(message);
1123
+ }
1124
+ return resolvedMessages;
1125
+ }
1126
+ catch (error) {
1127
+ logger.log(`āš ļø Error resolving unknown message IDs: ${error}`);
1128
+ return messages; // Return original messages if resolution fails
1129
+ }
1130
+ }
776
1131
  /**
777
1132
  * Use Microsoft Graph Search API for full-text search
778
1133
  */
@@ -792,21 +1147,31 @@ export class MS365Operations {
792
1147
  }
793
1148
  ]
794
1149
  };
1150
+ logger.log(`šŸ” Graph Search request:`, JSON.stringify(searchRequest, null, 2));
795
1151
  const result = await graphClient
796
1152
  .api('/search/query')
797
1153
  .post(searchRequest);
1154
+ logger.log(`šŸ” Graph Search raw response:`, JSON.stringify(result, null, 2));
798
1155
  const messages = [];
799
1156
  if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
1157
+ logger.log(`šŸ” Processing ${result.value[0].hitsContainers[0].hits.length} hits`);
800
1158
  for (const hit of result.value[0].hitsContainers[0].hits) {
801
1159
  const email = hit.resource;
802
- messages.push(this.mapEmailResult(email));
1160
+ logger.log(`šŸ” Raw email from Graph Search:`, JSON.stringify(email, null, 2));
1161
+ const mappedEmail = this.mapEmailResult(email);
1162
+ logger.log(`šŸ” Mapped email:`, JSON.stringify(mappedEmail, null, 2));
1163
+ messages.push(mappedEmail);
803
1164
  }
804
1165
  }
1166
+ else {
1167
+ logger.log(`šŸ” No hits found in Graph Search response structure`);
1168
+ }
805
1169
  logger.log(`šŸ“§ Graph Search returned ${messages.length} results`);
806
1170
  return messages;
807
1171
  }
808
1172
  catch (error) {
809
1173
  logger.log(`āŒ Graph Search failed: ${error}. Falling back to alternative search.`);
1174
+ logger.error(`āŒ Graph Search error details:`, JSON.stringify(error, null, 2));
810
1175
  return [];
811
1176
  }
812
1177
  }
@@ -975,8 +1340,33 @@ export class MS365Operations {
975
1340
  * Map email result to EmailInfo format
976
1341
  */
977
1342
  mapEmailResult(email) {
1343
+ // Extract ID from various possible locations in the response
1344
+ let emailId = email.id;
1345
+ if (!emailId) {
1346
+ // Try alternative ID sources
1347
+ emailId = email['@odata.id'] ||
1348
+ email.internetMessageId ||
1349
+ email._id ||
1350
+ email.messageId ||
1351
+ email.resource?.id;
1352
+ // If still no ID, try to extract from webLink or other fields
1353
+ if (!emailId && email.webLink) {
1354
+ const linkMatch = email.webLink.match(/itemid=([^&]+)/i);
1355
+ if (linkMatch) {
1356
+ emailId = decodeURIComponent(linkMatch[1]);
1357
+ }
1358
+ }
1359
+ // Log the raw email object for debugging when ID is missing
1360
+ if (!emailId) {
1361
+ logger.log('DEBUG: Email object missing ID after all attempts:', JSON.stringify(email, null, 2));
1362
+ emailId = 'UNKNOWN';
1363
+ }
1364
+ else {
1365
+ logger.log(`DEBUG: Found email ID from alternative source: ${emailId}`);
1366
+ }
1367
+ }
978
1368
  return {
979
- id: email.id,
1369
+ id: emailId,
980
1370
  subject: email.subject || '',
981
1371
  from: {
982
1372
  name: email.from?.emailAddress?.name || '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms365-mcp-server",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",