# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Send Draft Email action for Outlook operations. Sends draft email(s) using draft email JSON document(s) from action outlook.composeAndDraftEmailWithContext. """ import logging import time import json import requests from typing import Dict, Any from modules.workflows.methods.methodBase import action from modules.datamodels.datamodelChat import ActionResult, ActionDocument logger = logging.getLogger(__name__) @action async def sendDraftEmail(self, parameters: Dict[str, Any]) -> ActionResult: """ GENERAL: - Purpose: Send draft email(s) using draft email JSON document(s) from action outlook.composeAndDraftEmailWithContext. - Input requirements: connectionReference (required); documentList with draft email JSON documents (required). - Output format: JSON confirmation with sent mail metadata for all emails. Parameters: - connectionReference (str, required): Microsoft connection label. - documentList (list, required): Document reference(s) to draft emails in JSON format (outputs from outlook.composeAndDraftEmailWithContext function). """ 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 both direct JSON and JSON wrapped in documentData try: draftEmailData = json.loads(jsonContent) # If the JSON contains a 'documentData' field, extract it if isinstance(draftEmailData, dict) and 'documentData' in draftEmailData: documentDataStr = draftEmailData['documentData'] if isinstance(documentDataStr, str): draftEmailData = json.loads(documentDataStr) # 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") return ActionResult.isFailure(error="requests module not available") except Exception as e: logger.error(f"Error in sendDraftEmail: {str(e)}") return ActionResult.isFailure(error=str(e))