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 +28 -7
- package/dist/utils/ms365-operations.js +446 -56
- package/package.json +1 -1
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.
|
|
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/
|
|
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) =>
|
|
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:
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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, '&')
|
|
543
|
+
.replace(/</g, '<')
|
|
544
|
+
.replace(/>/g, '>')
|
|
545
|
+
.replace(/"/g, '"')
|
|
546
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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, '&')
|
|
702
|
+
.replace(/</g, '<')
|
|
703
|
+
.replace(/>/g, '>')
|
|
704
|
+
.replace(/"/g, '"')
|
|
705
|
+
.replace(/'/g, ''');
|
|
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, '&')
|
|
760
|
+
.replace(/</g, '<')
|
|
761
|
+
.replace(/>/g, '>')
|
|
762
|
+
.replace(/"/g, '"')
|
|
763
|
+
.replace(/'/g, ''');
|
|
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
|
|
664
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
700
|
-
if (allMessages.length
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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",
|