960 lines
No EOL
43 KiB
Python
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)
|
|
) |