# Copyright (c) 2025 Patrick Motsch # All rights reserved. import logging import time import json import requests from typing import Dict, Any from modules.datamodels.datamodelChat import ActionResult, ActionDocument logger = logging.getLogger(__name__) async def sendDraftEmail(self, parameters: Dict[str, Any]) -> ActionResult: operationId = None try: # Init progress logger workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}" operationId = f"outlook_send_{workflowId}_{int(time.time())}" # Start progress tracking parentOperationId = parameters.get('parentOperationId') self.services.chat.progressLogStart( operationId, "Send Draft Email", "Outlook Email Sending", f"Processing {len(parameters.get('documentList', []))} draft(s)", parentOperationId=parentOperationId ) connectionReference = parameters.get("connectionReference") documentList = parameters.get("documentList", []) if not connectionReference: if operationId: self.services.chat.progressLogFinish(operationId, False) return ActionResult.isFailure(error="Connection reference is required") if not documentList: if operationId: self.services.chat.progressLogFinish(operationId, False) return ActionResult.isFailure(error="documentList is required and cannot be empty") # Convert single value to list if needed if isinstance(documentList, str): documentList = [documentList] # Get Microsoft connection self.services.chat.progressLogUpdate(operationId, 0.2, "Getting Microsoft connection") connection = self.connection.getMicrosoftConnection(connectionReference) if not connection: self.services.chat.progressLogFinish(operationId, False) return ActionResult.isFailure(error="No valid Microsoft connection found for the provided connection reference") # Check permissions self.services.chat.progressLogUpdate(operationId, 0.3, "Checking permissions") permissions_ok = await self.connection.checkPermissions(connection) if not permissions_ok: self.services.chat.progressLogFinish(operationId, False) return ActionResult.isFailure(error="Connection lacks necessary permissions for Outlook operations") # Read draft email JSON documents from documentList self.services.chat.progressLogUpdate(operationId, 0.4, "Reading draft email documents") draftEmails = [] for docRef in documentList: try: # Get documents from document reference from modules.datamodels.datamodelDocref import DocumentReferenceList chatDocuments = self.services.chat.getChatDocumentsFromDocumentList(DocumentReferenceList.from_string_list([docRef])) if not chatDocuments: logger.warning(f"No documents found for reference: {docRef}") continue # Process each document in the reference for doc in chatDocuments: try: # Read file data fileId = getattr(doc, 'fileId', None) if not fileId: logger.warning(f"Document {doc.fileName} has no fileId") continue fileData = self.services.chat.getFileData(fileId) if not fileData: logger.warning(f"No file data found for document: {doc.fileName}") continue # Parse JSON content if isinstance(fileData, bytes): jsonContent = fileData.decode('utf-8') else: jsonContent = str(fileData) # Parse JSON - handle ActionDocument format with validationMetadata wrapper try: draftEmailData = json.loads(jsonContent) # ActionDocument format: { "validationMetadata": {...}, "documentData": {...} } # Extract documentData which contains the actual draft email data if isinstance(draftEmailData, dict) and 'documentData' in draftEmailData: documentDataContent = draftEmailData['documentData'] # documentData should be a dict (parsed from JSON by processSingleDocument) if isinstance(documentDataContent, dict): draftEmailData = documentDataContent elif isinstance(documentDataContent, str): # Legacy/fallback: parse if still a string draftEmailData = json.loads(documentDataContent) # Validate draft email structure if not isinstance(draftEmailData, dict): logger.warning(f"Document {doc.fileName} does not contain a valid draft email JSON object") continue draftId = draftEmailData.get("draftId") if not draftId: logger.warning(f"Document {doc.fileName} does not contain 'draftId' field") continue draftEmails.append({ "draftEmailJson": draftEmailData, "draftId": draftId, "sourceDocument": doc.fileName, "sourceReference": docRef }) except json.JSONDecodeError as e: logger.error(f"Failed to parse JSON from document {doc.fileName}: {str(e)}") continue except Exception as e: logger.error(f"Error processing document {doc.fileName}: {str(e)}") continue except Exception as e: logger.error(f"Error reading documents from reference {docRef}: {str(e)}") continue if not draftEmails: self.services.chat.progressLogFinish(operationId, False) return ActionResult.isFailure(error="No valid draft email JSON documents found in documentList") self.services.chat.progressLogUpdate(operationId, 0.6, f"Found {len(draftEmails)} draft email(s) to send") # Send all draft emails graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } sentResults = [] failedResults = [] self.services.chat.progressLogUpdate(operationId, 0.7, "Sending emails") for idx, draftEmail in enumerate(draftEmails): draftEmailJson = draftEmail["draftEmailJson"] draftId = draftEmail["draftId"] sourceDocument = draftEmail["sourceDocument"] try: send_url = f"{graph_url}/me/messages/{draftId}/send" sendResponse = requests.post(send_url, headers=headers) # Extract email details from draft JSON for confirmation subject = draftEmailJson.get("subject", "Unknown") recipients = draftEmailJson.get("recipients", []) cc = draftEmailJson.get("cc", []) bcc = draftEmailJson.get("bcc", []) attachmentsCount = draftEmailJson.get("attachments", 0) if sendResponse.status_code in [200, 202, 204]: sentResults.append({ "status": "sent", "message": "Email sent successfully", "draftId": draftId, "subject": subject, "recipients": recipients, "cc": cc, "bcc": bcc, "attachments": attachmentsCount, "sentTimestamp": self.services.utils.timestampGetUtc(), "sourceDocument": sourceDocument }) logger.info(f"Email sent successfully. Draft ID: {draftId}, Subject: {subject}") self.services.chat.progressLogUpdate(operationId, 0.7 + (idx + 1) * 0.2 / len(draftEmails), f"Sent {idx + 1}/{len(draftEmails)}: {subject}") else: errorResult = { "status": "error", "message": "Failed to send draft email", "draftId": draftId, "subject": subject, "recipients": recipients, "sendError": { "statusCode": sendResponse.status_code, "response": sendResponse.text }, "sentTimestamp": self.services.utils.timestampGetUtc(), "sourceDocument": sourceDocument } failedResults.append(errorResult) logger.error(f"Failed to send email. Draft ID: {draftId}, Status: {sendResponse.status_code}, Response: {sendResponse.text}") except Exception as e: errorResult = { "status": "error", "message": f"Exception while sending draft email: {str(e)}", "draftId": draftId, "subject": draftEmailJson.get("subject", "Unknown"), "recipients": draftEmailJson.get("recipients", []), "exception": str(e), "sentTimestamp": self.services.utils.timestampGetUtc(), "sourceDocument": sourceDocument } failedResults.append(errorResult) logger.error(f"Error sending draft email {draftId}: {str(e)}") # Build result summary totalEmails = len(draftEmails) successfulEmails = len(sentResults) failedEmails = len(failedResults) resultData = { "totalEmails": totalEmails, "successfulEmails": successfulEmails, "failedEmails": failedEmails, "sentResults": sentResults, "failedResults": failedResults, "timestamp": self.services.utils.timestampGetUtc() } # Determine overall success status self.services.chat.progressLogUpdate(operationId, 0.9, f"Sent {successfulEmails}/{totalEmails} email(s)") if successfulEmails == 0: self.services.chat.progressLogFinish(operationId, False) validationMetadata = { "actionType": "outlook.sendDraftEmail", "connectionReference": connectionReference, "totalEmails": totalEmails, "successfulEmails": successfulEmails, "failedEmails": failedEmails, "status": "all_failed" } return ActionResult.isFailure( error=f"Failed to send all {totalEmails} email(s)", documents=[ActionDocument( documentName=f"sent_mail_confirmation_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(resultData, indent=2), mimeType="application/json", validationMetadata=validationMetadata )] ) elif failedEmails > 0: # Partial success logger.warning(f"Sent {successfulEmails} out of {totalEmails} emails. {failedEmails} failed.") validationMetadata = { "actionType": "outlook.sendDraftEmail", "connectionReference": connectionReference, "totalEmails": totalEmails, "successfulEmails": successfulEmails, "failedEmails": failedEmails, "status": "partial_success" } self.services.chat.progressLogFinish(operationId, True) return ActionResult( success=True, documents=[ActionDocument( documentName=f"sent_mail_confirmation_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(resultData, indent=2), mimeType="application/json", validationMetadata=validationMetadata )] ) else: # All successful logger.info(f"Successfully sent all {totalEmails} email(s)") validationMetadata = { "actionType": "outlook.sendDraftEmail", "connectionReference": connectionReference, "totalEmails": totalEmails, "successfulEmails": successfulEmails, "failedEmails": failedEmails, "status": "all_successful" } self.services.chat.progressLogFinish(operationId, True) return ActionResult( success=True, documents=[ActionDocument( documentName=f"sent_mail_confirmation_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(resultData, indent=2), mimeType="application/json", validationMetadata=validationMetadata )] ) except ImportError: logger.error("requests module not available") if operationId: try: self.services.chat.progressLogFinish(operationId, False) except Exception: pass return ActionResult.isFailure(error="requests module not available") except Exception as e: logger.error(f"Error in sendDraftEmail: {str(e)}") if operationId: try: self.services.chat.progressLogFinish(operationId, False) except Exception: pass return ActionResult.isFailure(error=str(e))