310 lines
15 KiB
Python
310 lines
15 KiB
Python
# 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))
|
|
|