From 668a60c18b2a251a5beec144cf1b97e68884db6a Mon Sep 17 00:00:00 2001 From: dimavedenyapin Date: Thu, 26 Mar 2026 23:13:58 +0700 Subject: [PATCH 1/2] feat: return message ID from Outlook send_email for delivery tracking Use create-draft-then-send pattern instead of sendMail so we can capture the Graph messageId and internetMessageId. This enables downstream delivery tracking (bounce/open/click events) via Microsoft Graph change notification subscriptions. Co-Authored-By: Claude Opus 4.6 --- src/servers/outlook/main.py | 78 ++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/src/servers/outlook/main.py b/src/servers/outlook/main.py index 9c6a079..3461333 100644 --- a/src/servers/outlook/main.py +++ b/src/servers/outlook/main.py @@ -520,53 +520,77 @@ async def handle_call_tool( if email.strip() ] - # Prepare the email payload - email_payload = { - "message": { - "subject": subject, - "body": {"contentType": "Text", "content": body}, - "toRecipients": to_list, - "ccRecipients": cc_list, - "bccRecipients": bcc_list, - "internetMessageHeaders": [ - {"name": "X-Mailer", "value": "Microsoft Graph API"} - ], - }, - "saveToSentItems": "true", - } - headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", } - # Log the request details - logger.info(f"Sending email with payload: {email_payload}") + # Step 1: Create draft message to obtain a message ID + draft_payload = { + "subject": subject, + "body": {"contentType": "Text", "content": body}, + "toRecipients": to_list, + "ccRecipients": cc_list, + "bccRecipients": bcc_list, + "internetMessageHeaders": [ + {"name": "X-Mailer", "value": "Microsoft Graph API"} + ], + } - response = requests.post( - "https://graph.microsoft.com/v1.0/me/sendMail", + logger.info(f"Creating draft email with payload: {draft_payload}") + + draft_response = requests.post( + "https://graph.microsoft.com/v1.0/me/messages", headers=headers, - data=json.dumps(email_payload), + data=json.dumps(draft_payload), ) - # Log the response - logger.info(f"Response status code: {response.status_code}") - logger.info(f"Response content: {response.content}") + if draft_response.status_code not in [200, 201]: + error_message = ( + draft_response.json().get("error", {}).get("message", "Unknown error") + ) + return [ + TextContent( + type="text", + text=f"Failed to create draft email: {error_message}", + ) + ] - if response.status_code in [200, 202]: + draft_data = draft_response.json() + message_id = draft_data.get("id", "") + internet_message_id = draft_data.get("internetMessageId", "") + + logger.info(f"Draft created with id: {message_id}, internetMessageId: {internet_message_id}") + + # Step 2: Send the draft message + send_response = requests.post( + f"https://graph.microsoft.com/v1.0/me/messages/{message_id}/send", + headers=headers, + ) + + logger.info(f"Send response status code: {send_response.status_code}") + + if send_response.status_code in [200, 202]: + result_data = { + "success": True, + "messageId": message_id, + "internetMessageId": internet_message_id, + "recipients": [email.strip() for email in to_recipients if email.strip()], + } return [ TextContent( type="text", - text=f"Email sent successfully to {', '.join(to_recipients)}", + text=json.dumps(result_data), ) ] else: error_message = ( - response.json().get("error", {}).get("message", "Unknown error") + send_response.json().get("error", {}).get("message", "Unknown error") ) return [ TextContent( - type="text", text=f"Failed to send email: {error_message}" + type="text", + text=f"Failed to send email: {error_message}", ) ] From 7fbca3ad3921691061e39fc94348f5112a8e80c9 Mon Sep 17 00:00:00 2001 From: dimavedenyapin Date: Thu, 26 Mar 2026 23:50:14 +0700 Subject: [PATCH 2/2] fix: format outlook/main.py with black Co-Authored-By: Claude Opus 4.6 --- src/servers/outlook/main.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/servers/outlook/main.py b/src/servers/outlook/main.py index 3461333..adcd29b 100644 --- a/src/servers/outlook/main.py +++ b/src/servers/outlook/main.py @@ -547,7 +547,9 @@ async def handle_call_tool( if draft_response.status_code not in [200, 201]: error_message = ( - draft_response.json().get("error", {}).get("message", "Unknown error") + draft_response.json() + .get("error", {}) + .get("message", "Unknown error") ) return [ TextContent( @@ -560,7 +562,9 @@ async def handle_call_tool( message_id = draft_data.get("id", "") internet_message_id = draft_data.get("internetMessageId", "") - logger.info(f"Draft created with id: {message_id}, internetMessageId: {internet_message_id}") + logger.info( + f"Draft created with id: {message_id}, internetMessageId: {internet_message_id}" + ) # Step 2: Send the draft message send_response = requests.post( @@ -575,7 +579,9 @@ async def handle_call_tool( "success": True, "messageId": message_id, "internetMessageId": internet_message_id, - "recipients": [email.strip() for email in to_recipients if email.strip()], + "recipients": [ + email.strip() for email in to_recipients if email.strip() + ], } return [ TextContent( @@ -585,7 +591,9 @@ async def handle_call_tool( ] else: error_message = ( - send_response.json().get("error", {}).get("message", "Unknown error") + send_response.json() + .get("error", {}) + .get("message", "Unknown error") ) return [ TextContent(