gateway/modules/methods/methodOutlook.py
2025-08-16 23:32:36 +02:00

562 lines
No EOL
24 KiB
Python

"""
Outlook method module.
Handles Outlook operations using the Outlook service.
"""
import logging
from typing import Dict, Any, List, Optional
from datetime import datetime, UTC
import json
import uuid
from modules.chat.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)
class MethodOutlook(MethodBase):
"""Outlook method implementation for email operations"""
def __init__(self, serviceCenter: Any):
"""Initialize the Outlook method"""
super().__init__(serviceCenter)
self.name = "outlook"
self.description = "Handle Microsoft Outlook email operations"
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
"""Get Microsoft connection from connection reference"""
try:
userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
if not userConnection or userConnection.authority.value != "msft" or userConnection.status.value != "active":
return None
# Get the corresponding token for this user and authority
token = self.service.interfaceApp.getToken(userConnection.authority.value)
if not token:
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority.value}")
return None
return {
"id": userConnection.id,
"accessToken": token.tokenAccess,
"refreshToken": token.tokenRefresh,
"scopes": ["Mail.ReadWrite", "User.Read"] # Default Microsoft scopes
}
except Exception as e:
logger.error(f"Error getting Microsoft connection: {str(e)}")
return None
@action
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Read emails from Outlook
Parameters:
connectionReference (str): Reference to the Microsoft connection
folder (str, optional): Email folder to read from (default: "Inbox")
limit (int, optional): Maximum number of emails to read (default: 10)
filter (str, optional): Filter criteria for emails
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
"""
try:
connectionReference = parameters.get("connectionReference")
folder = parameters.get("folder", "Inbox")
limit = parameters.get("limit", 10)
filter = parameters.get("filter")
expectedDocumentFormats = parameters.get("expectedDocumentFormats", [])
if not connectionReference:
return self._createResult(
success=False,
data={},
error="Connection reference is required"
)
# Get Microsoft connection
connection = self._getMicrosoftConnection(connectionReference)
if not connection:
return self._createResult(
success=False,
data={},
error="No valid Microsoft connection found for the provided connection reference"
)
# Read emails using Microsoft Graph API
try:
import requests
# Microsoft Graph API endpoint for messages
graph_url = "https://graph.microsoft.com/v1.0"
headers = {
"Authorization": f"Bearer {connection['accessToken']}",
"Content-Type": "application/json"
}
# Build the API request
api_url = f"{graph_url}/me/mailFolders/{folder}/messages"
params = {
"$top": limit,
"$orderby": "receivedDateTime desc"
}
if filter:
params["$filter"] = filter
# Make the API call
response = requests.get(api_url, headers=headers, params=params)
response.raise_for_status()
emails_data = response.json()
email_data = {
"emails": emails_data.get("value", []),
"count": len(emails_data.get("value", [])),
"folder": folder,
"filter": filter,
"apiResponse": emails_data
}
logger.info(f"Successfully retrieved {len(emails_data.get('value', []))} emails from {folder}")
except ImportError:
logger.error("requests module not available, falling back to simulation")
# Fallback to simulation if requests module is not available
email_prompt = f"""
Simulate reading emails from Microsoft Outlook.
Connection: {connection['id']}
Folder: {folder}
Limit: {limit}
Filter: {filter or 'None'}
Please provide:
1. List of emails with subject, sender, date, and content
2. Summary of email statistics
3. Important or urgent emails highlighted
4. Email categorization if possible
"""
email_data = await self.service.interfaceAiCalls.callAiTextAdvanced(email_prompt)
except Exception as e:
logger.error(f"Error reading emails from Microsoft Graph API: {str(e)}")
# Fallback to simulation on API error
email_prompt = f"""
Simulate reading emails from Microsoft Outlook.
Connection: {connection['id']}
Folder: {folder}
Limit: {limit}
Filter: {filter or 'None'}
Please provide:
1. List of emails with subject, sender, date, and content
2. Summary of email statistics
3. Important or urgent emails highlighted
4. Email categorization if possible
"""
email_data = await self.service.interfaceAiCalls.callAiTextAdvanced(email_prompt)
# Create result data
result_data = {
"connectionReference": connectionReference,
"folder": folder,
"limit": limit,
"filter": filter,
"emails": email_data,
"connection": {
"id": connection["id"],
"authority": "microsoft",
"reference": connectionReference
},
"timestamp": datetime.now(UTC).isoformat()
}
# Determine output format based on expected formats
output_extension = ".json" # Default
output_mime_type = "application/json" # Default
if expectedDocumentFormats and len(expectedDocumentFormats) > 0:
# Use the first expected format
expected_format = expectedDocumentFormats[0]
output_extension = expected_format.get("extension", ".json")
output_mime_type = expected_format.get("mimeType", "application/json")
logger.info(f"Using expected format: {output_extension} ({output_mime_type})")
else:
logger.info("No expected format specified, using default .json format")
return self._createResult(
success=True,
data={
"documents": [
{
"documentName": f"outlook_emails_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}",
"documentData": result_data,
"mimeType": output_mime_type
}
]
}
)
except Exception as e:
logger.error(f"Error reading emails: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)
@action
async def sendEmail(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Create email draft in Outlook for sending out
Parameters:
connectionReference (str): Reference to the Microsoft connection
to (List[str]): List of recipient email addresses
subject (str): Email subject
body (str): Email body content
cc (List[str], optional): CC recipients
bcc (List[str], optional): BCC recipients
attachments (List[str], optional): List of document references to attach
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
"""
try:
connectionReference = parameters.get("connectionReference")
to = parameters.get("to")
subject = parameters.get("subject")
body = parameters.get("body")
cc = parameters.get("cc", [])
bcc = parameters.get("bcc", [])
attachments = parameters.get("attachments", [])
expectedDocumentFormats = parameters.get("expectedDocumentFormats", [])
if not connectionReference or not to or not subject or not body:
return self._createResult(
success=False,
data={},
error="Connection reference, to, subject, and body are required"
)
# Get Microsoft connection
connection = self._getMicrosoftConnection(connectionReference)
if not connection:
return self._createResult(
success=False,
data={},
error="No valid Microsoft connection found for the provided connection reference"
)
# Create email draft using Microsoft Graph API
try:
import requests
# Microsoft Graph API endpoint for creating draft messages
graph_url = "https://graph.microsoft.com/v1.0"
headers = {
"Authorization": f"Bearer {connection['accessToken']}",
"Content-Type": "application/json"
}
# Build the email message
message = {
"subject": subject,
"body": {
"contentType": "HTML",
"content": body
},
"toRecipients": [{"emailAddress": {"address": email}} for email in to],
"ccRecipients": [{"emailAddress": {"address": email}} for email in cc] if cc else [],
"bccRecipients": [{"emailAddress": {"address": email}} for email in bcc] if bcc else []
}
# Add attachments if provided
if attachments:
message["attachments"] = []
for attachment_ref in attachments:
# Get attachment document from service center
attachment_docs = self.service.getChatDocumentsFromDocumentList([attachment_ref])
if attachment_docs:
for doc in attachment_docs:
# Create attachment object for Graph API
attachment = {
"@odata.type": "#microsoft.graph.fileAttachment",
"name": doc.filename,
"contentType": doc.mimeType,
"contentBytes": doc.data if hasattr(doc, 'data') else ""
}
message["attachments"].append(attachment)
# Create the draft message
api_url = f"{graph_url}/me/messages"
response = requests.post(api_url, headers=headers, json=message)
response.raise_for_status()
draft_data = response.json()
draft_result = {
"status": "draft_created",
"messageId": draft_data.get("id", "unknown"),
"draftId": draft_data.get("id", "unknown"),
"recipients": to,
"cc": cc,
"bcc": bcc,
"attachments": len(attachments) if attachments else 0,
"draftLocation": "Drafts folder",
"apiResponse": response.status_code,
"draftData": draft_data
}
logger.info(f"Successfully created email draft for {len(to)} recipients with {len(attachments) if attachments else 0} attachments")
except ImportError:
logger.error("requests module not available, falling back to simulation")
# Fallback to simulation if requests module is not available
send_prompt = f"""
Simulate creating an email draft in Microsoft Outlook.
Connection: {connection['id']}
To: {to}
Subject: {subject}
Body: {body}
CC: {cc}
BCC: {bcc}
Attachments: {attachments if attachments else 'None'}
Please provide:
1. Email composition details
2. Validation of email addresses
3. Email formatting and structure
4. Attachment processing and validation
5. Draft creation confirmation
"""
draft_result = await self.service.interfaceAiCalls.callAiTextAdvanced(send_prompt)
except Exception as e:
logger.error(f"Error creating email draft via Microsoft Graph API: {str(e)}")
# Fallback to simulation on API error
send_prompt = f"""
Simulate creating an email draft in Microsoft Outlook.
Connection: {connection['id']}
To: {to}
Subject: {subject}
Body: {body}
CC: {cc}
BCC: {bcc}
Attachments: {attachments if attachments else 'None'}
Please provide:
1. Email composition details
2. Validation of email addresses
3. Email formatting and structure
4. Attachment processing and validation
5. Draft creation confirmation
"""
draft_result = await self.service.interfaceAiCalls.callAiTextAdvanced(send_prompt)
# Create result data
result_data = {
"connectionReference": connectionReference,
"to": to,
"subject": subject,
"body": body,
"cc": cc,
"bcc": bcc,
"attachments": attachments,
"draftResult": draft_result,
"connection": {
"id": connection["id"],
"authority": "microsoft",
"reference": connectionReference
},
"timestamp": datetime.now(UTC).isoformat()
}
# Determine output format based on expected formats
output_extension = ".json" # Default
output_mime_type = "application/json" # Default
if expectedDocumentFormats and len(expectedDocumentFormats) > 0:
# Use the first expected format
expected_format = expectedDocumentFormats[0]
output_extension = expected_format.get("extension", ".json")
output_mime_type = expected_format.get("mimeType", "application/json")
logger.info(f"Using expected format: {output_extension} ({output_mime_type})")
else:
logger.info("No expected format specified, using default .json format")
return self._createResult(
success=True,
data={
"documents": [
{
"documentName": f"outlook_email_draft_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}",
"documentData": result_data,
"mimeType": output_mime_type
}
]
}
)
except Exception as e:
logger.error(f"Error creating email draft: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)
@action
async def searchEmails(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Search emails in Outlook
Parameters:
connectionReference (str): Reference to the Microsoft connection
query (str): Search query
folder (str, optional): Folder to search in (default: "All")
limit (int, optional): Maximum number of results (default: 20)
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
"""
try:
connectionReference = parameters.get("connectionReference")
query = parameters.get("query")
folder = parameters.get("folder", "All")
limit = parameters.get("limit", 20)
expectedDocumentFormats = parameters.get("expectedDocumentFormats", [])
if not connectionReference or not query:
return self._createResult(
success=False,
data={},
error="Connection reference and query are required"
)
# Get Microsoft connection
connection = self._getMicrosoftConnection(connectionReference)
if not connection:
return self._createResult(
success=False,
data={},
error="No valid Microsoft connection found for the provided connection reference"
)
# Search emails using Microsoft Graph API
try:
import requests
# Microsoft Graph API endpoint for searching messages
graph_url = "https://graph.microsoft.com/v1.0"
headers = {
"Authorization": f"Bearer {connection['accessToken']}",
"Content-Type": "application/json"
}
# Build the search API request
api_url = f"{graph_url}/me/messages"
params = {
"$top": limit,
"$orderby": "receivedDateTime desc",
"$search": f'"{query}"'
}
# Add folder filter if specified
if folder and folder.lower() != "all":
params["$filter"] = f"parentFolderId eq '{folder}'"
# Make the API call
response = requests.get(api_url, headers=headers, params=params)
response.raise_for_status()
search_data = response.json()
search_result = {
"query": query,
"results": search_data.get("value", []),
"count": len(search_data.get("value", [])),
"folder": folder,
"limit": limit,
"apiResponse": search_data
}
logger.info(f"Successfully searched emails with query '{query}', found {len(search_data.get('value', []))} results")
except ImportError:
logger.error("requests module not available, falling back to simulation")
# Fallback to simulation if requests module is not available
search_prompt = f"""
Simulate searching emails in Microsoft Outlook.
Connection: {connection['id']}
Query: {query}
Folder: {folder}
Limit: {limit}
Please provide:
1. Search results with relevant emails
2. Search statistics and relevance scores
3. Email previews and key information
4. Search suggestions and refinements
"""
search_result = await self.service.interfaceAiCalls.callAiTextAdvanced(search_prompt)
except Exception as e:
logger.error(f"Error searching emails via Microsoft Graph API: {str(e)}")
# Fallback to simulation on API error
search_prompt = f"""
Simulate searching emails in Microsoft Outlook.
Connection: {connection['id']}
Query: {query}
Folder: {folder}
Limit: {limit}
Please provide:
1. Search results with relevant emails
2. Search statistics and relevance scores
3. Email previews and key information
4. Search suggestions and refinements
"""
search_result = await self.service.interfaceAiCalls.callAiTextAdvanced(search_prompt)
# Create result data
result_data = {
"connectionReference": connectionReference,
"query": query,
"folder": folder,
"limit": limit,
"searchResults": search_result,
"connection": {
"id": connection["id"],
"authority": "microsoft",
"reference": connectionReference
},
"timestamp": datetime.now(UTC).isoformat()
}
# Determine output format based on expected formats
output_extension = ".json" # Default
output_mime_type = "application/json" # Default
if expectedDocumentFormats and len(expectedDocumentFormats) > 0:
# Use the first expected format
expected_format = expectedDocumentFormats[0]
output_extension = expected_format.get("extension", ".json")
output_mime_type = expected_format.get("mimeType", "application/json")
logger.info(f"Using expected format: {output_extension} ({output_mime_type})")
else:
logger.info("No expected format specified, using default .json format")
return self._createResult(
success=True,
data={
"documents": [
{
"documentName": f"outlook_email_search_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}",
"documentData": result_data,
"mimeType": output_mime_type
}
]
}
)
except Exception as e:
logger.error(f"Error searching emails: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)