gateway/modules/methods/methodOutlook.py
2025-08-17 00:19:55 +02:00

960 lines
No EOL
43 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:
logger.warning(f"No user connection found for reference: {connectionReference}")
return None
if userConnection.authority.value != "msft":
logger.warning(f"Connection {userConnection.id} is not Microsoft (authority: {userConnection.authority.value})")
return None
# Check if connection is active or pending (pending means OAuth in progress)
if userConnection.status.value not in ["active", "pending"]:
logger.warning(f"Connection {userConnection.id} status is not active/pending: {userConnection.status.value}")
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
# Check if token is expired
if hasattr(token, 'expiresAt') and token.expiresAt:
import time
current_time = time.time()
if current_time > token.expiresAt:
logger.warning(f"Token for connection {userConnection.id} is expired (expiresAt: {token.expiresAt}, current: {current_time})")
return None
logger.info(f"Successfully retrieved Microsoft connection: {userConnection.id}, status: {userConnection.status.value}, externalId: {userConnection.externalId}")
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
def _sanitizeSearchQuery(self, query: str) -> str:
"""
Sanitize and validate search query for Microsoft Graph API
Microsoft Graph API has specific requirements for search queries:
- Escape special characters properly
- Handle search operators correctly
- Ensure query format is valid
"""
if not query:
return ""
# Clean the query
clean_query = query.strip()
# Remove any double quotes that might cause issues
clean_query = clean_query.replace('"', '')
# Handle common search operators
if any(op in clean_query.lower() for op in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']):
# This is an advanced search query, return as-is
return clean_query
# For basic text search, ensure it's safe for contains() filter
# Remove any characters that might break the OData filter syntax
import re
# Remove or escape characters that could break OData filter syntax
safe_query = re.sub(r'[\\\'"]', '', clean_query)
return safe_query
def _buildSearchParameters(self, query: str, folder: str, limit: int) -> Dict[str, Any]:
"""
Build search parameters for Microsoft Graph API
This method handles the complexity of building search parameters
while avoiding conflicts between $search and $filter parameters.
"""
params = {
"$top": limit,
"$orderby": "receivedDateTime desc"
}
if not query or not query.strip():
# No query specified, just get emails from folder
if folder and folder.lower() != "all":
params["$filter"] = f"parentFolderId eq '{folder}'"
return params
clean_query = self._sanitizeSearchQuery(query)
# Check if this is a complex search query with multiple operators
if any(op in clean_query.lower() for op in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']):
# This is an advanced search query, use $search
# Microsoft Graph API supports complex search syntax
params["$search"] = f'"{clean_query}"'
logger.info(f"Using advanced search query: {clean_query}")
# Note: When using $search, we cannot combine it with $filter for folder
# We'll need to filter results after the API call
if folder and folder.lower() != "all":
logger.info(f"Will filter results by folder '{folder}' after search")
else:
# Use $filter for basic text search in subject and body
# Microsoft Graph API supports contains() for text search
filter_parts = [f"contains(subject,'{clean_query}') or contains(body/content,'{clean_query}')"]
# Add folder filter if specified
if folder and folder.lower() != "all":
filter_parts.append(f"parentFolderId eq '{folder}'")
# Combine all filter parts
params["$filter"] = " and ".join(f"({part})" for part in filter_parts)
logger.info(f"Using basic text search filter: {clean_query}")
return params
def _getFolderId(self, folder_name: str, connection: Dict[str, Any]) -> Optional[str]:
"""
Get the folder ID for a given folder name
This is needed for proper filtering when using advanced search queries
"""
try:
import requests
graph_url = "https://graph.microsoft.com/v1.0"
headers = {
"Authorization": f"Bearer {connection['accessToken']}",
"Content-Type": "application/json"
}
# Get mail folders
api_url = f"{graph_url}/me/mailFolders"
response = requests.get(api_url, headers=headers)
if response.status_code == 200:
folders_data = response.json()
for folder in folders_data.get("value", []):
if folder.get("displayName", "").lower() == folder_name.lower():
return folder.get("id")
logger.warning(f"Folder '{folder_name}' not found")
return None
else:
logger.warning(f"Could not retrieve folders: {response.status_code}")
return None
except Exception as e:
logger.warning(f"Error getting folder ID for '{folder_name}': {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
# First, get the Drafts folder ID to ensure the draft is created there
drafts_folder_id = self._getFolderId("Drafts", connection)
if drafts_folder_id:
# Create draft in the Drafts folder specifically
api_url = f"{graph_url}/me/mailFolders/{drafts_folder_id}/messages"
logger.info(f"Creating draft in Drafts folder (ID: {drafts_folder_id})")
else:
# Fallback: create in default location
api_url = f"{graph_url}/me/messages"
logger.warning("Could not find Drafts folder, creating draft in default location")
response = requests.post(api_url, headers=headers, json=message)
response.raise_for_status()
draft_data = response.json()
# Get the actual folder information for the created draft
actual_folder = "Drafts"
if drafts_folder_id:
actual_folder = "Drafts"
else:
# Try to determine where the draft was actually created
if "parentFolderId" in draft_data:
actual_folder = f"Folder ID: {draft_data['parentFolderId']}"
else:
actual_folder = "Default location"
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": actual_folder,
"draftsFolderId": drafts_folder_id,
"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 in {actual_folder}")
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", [])
# Validate parameters
if not connectionReference:
return self._createResult(
success=False,
data={},
error="Connection reference is required"
)
if not query or not query.strip():
return self._createResult(
success=False,
data={},
error="Search query is required and cannot be empty"
)
# Validate limit
try:
limit = int(limit)
if limit <= 0 or limit > 1000: # Microsoft Graph API has limits
limit = 20
logger.warning(f"Limit {limit} is out of range, using default value 20")
except (ValueError, TypeError):
limit = 20
logger.warning(f"Invalid limit value, using default value 20")
# 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 = self._buildSearchParameters(query, folder, limit)
logger.info(f"Search API parameters: {params}")
# Make the API call
response = requests.get(api_url, headers=headers, params=params)
# Log response details for debugging
logger.debug(f"Microsoft Graph API response status: {response.status_code}")
logger.debug(f"Microsoft Graph API response headers: {dict(response.headers)}")
if response.status_code != 200:
# Log detailed error information
try:
error_data = response.json()
logger.error(f"Microsoft Graph API error: {response.status_code} - {error_data}")
except:
logger.error(f"Microsoft Graph API error: {response.status_code} - {response.text}")
# Check for specific error types and provide helpful messages
if response.status_code == 400:
logger.error("Bad Request (400) - Check search query format and parameters")
logger.error(f"Search query: '{query}'")
logger.error(f"Search parameters: {params}")
logger.error(f"API URL: {api_url}")
elif response.status_code == 401:
logger.error("Unauthorized (401) - Check access token and permissions")
elif response.status_code == 403:
logger.error("Forbidden (403) - Check API permissions and scopes")
elif response.status_code == 429:
logger.error("Too Many Requests (429) - Rate limit exceeded")
# Fall back to simulation on API error
raise Exception(f"Microsoft Graph API returned {response.status_code}: {response.text}")
response.raise_for_status()
search_data = response.json()
emails = search_data.get("value", [])
logger.info(f"Successfully retrieved {len(emails)} emails from Microsoft Graph API")
# Apply folder filtering if needed and we used $search
if folder and folder.lower() != "all" and "$search" in params:
# Get the actual folder ID for proper filtering
folder_id = self._getFolderId(folder, connection)
if folder_id:
# Filter results by folder ID
filtered_emails = []
for email in emails:
if email.get("parentFolderId") == folder_id:
filtered_emails.append(email)
emails = filtered_emails
logger.info(f"Filtered {len(filtered_emails)} emails for folder '{folder}' (ID: {folder_id}) from {len(search_data.get('value', []))} total results")
else:
# Fallback: try to filter by folder name (less reliable)
filtered_emails = []
for email in emails:
# Check if email has folder information
if hasattr(email, 'parentFolderId') and email.get('parentFolderId'):
if email.get('parentFolderId') == folder:
filtered_emails.append(email)
else:
# If no folder info, include the email (less strict filtering)
filtered_emails.append(email)
logger.debug(f"Email {email.get('id', 'unknown')} has no folder info, including in results")
emails = filtered_emails
logger.info(f"Filtered {len(filtered_emails)} emails for folder '{folder}' (fallback filtering) from {len(search_data.get('value', []))} total results")
search_result = {
"query": query,
"results": emails,
"count": len(emails),
"folder": folder,
"limit": limit,
"apiResponse": search_data,
"searchParams": params
}
logger.info(f"Successfully searched emails with query '{query}', found {len(emails)} 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)
)
@action
async def listDrafts(self, parameters: Dict[str, Any]) -> ActionResult:
"""
List email drafts in Outlook
Parameters:
connectionReference (str): Reference to the Microsoft connection
folder (str, optional): Folder to search for drafts (default: "Drafts")
limit (int, optional): Maximum number of drafts to list (default: 20)
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
"""
try:
connectionReference = parameters.get("connectionReference")
folder = parameters.get("folder", "Drafts")
limit = parameters.get("limit", 20)
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"
)
# List drafts 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"
}
# Get the folder ID for the specified folder
folder_id = self._getFolderId(folder, connection)
if folder_id:
# List messages in the specific folder
api_url = f"{graph_url}/me/mailFolders/{folder_id}/messages"
logger.info(f"Listing messages in folder '{folder}' (ID: {folder_id})")
else:
# Fallback: list all messages (might include drafts)
api_url = f"{graph_url}/me/messages"
logger.warning(f"Could not find folder '{folder}', listing all messages")
params = {
"$top": limit,
"$orderby": "lastModifiedDateTime desc",
"$select": "id,subject,from,toRecipients,ccRecipients,bccRecipients,receivedDateTime,lastModifiedDateTime,parentFolderId,isDraft"
}
# Make the API call
response = requests.get(api_url, headers=headers, params=params)
response.raise_for_status()
messages_data = response.json()
messages = messages_data.get("value", [])
# Filter for drafts if we're looking at all messages
if not folder_id:
drafts = [msg for msg in messages if msg.get("isDraft", False)]
messages = drafts
logger.info(f"Filtered {len(drafts)} drafts from {len(messages_data.get('value', []))} total messages")
drafts_result = {
"folder": folder,
"folderId": folder_id,
"drafts": messages,
"count": len(messages),
"limit": limit,
"apiResponse": messages_data
}
logger.info(f"Successfully retrieved {len(messages)} drafts from folder '{folder}'")
except ImportError:
logger.error("requests module not available, falling back to simulation")
# Fallback to simulation
drafts_prompt = f"""
Simulate listing email drafts in Microsoft Outlook.
Connection: {connection['id']}
Folder: {folder}
Limit: {limit}
Please provide:
1. List of email drafts with subject, recipients, and modification date
2. Draft status and location information
3. Summary of draft statistics
"""
drafts_result = await self.service.interfaceAiCalls.callAiTextAdvanced(drafts_prompt)
except Exception as e:
logger.error(f"Error listing drafts via Microsoft Graph API: {str(e)}")
# Fallback to simulation on API error
drafts_prompt = f"""
Simulate listing email drafts in Microsoft Outlook.
Connection: {connection['id']}
Folder: {folder}
Limit: {limit}
Please provide:
1. List of email drafts with subject, recipients, and modification date
2. Draft status and location information
3. Summary of draft statistics
"""
drafts_result = await self.service.interfaceAiCalls.callAiTextAdvanced(drafts_prompt)
# Create result data
result_data = {
"connectionReference": connectionReference,
"folder": folder,
"limit": limit,
"draftsResult": drafts_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_drafts_list_{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 listing drafts: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)