gateway/modules/workflows/methods/methodOutlook/actions/sendDraftEmail.py
2025-12-17 10:45:09 +01:00

312 lines
15 KiB
Python

# 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))