fixed mailing
This commit is contained in:
parent
8592cdd790
commit
d642fa0f07
4 changed files with 508 additions and 34 deletions
|
|
@ -594,6 +594,23 @@ class HandlingTasks:
|
||||||
action.setError(result.error or "Action execution failed")
|
action.setError(result.error or "Action execution failed")
|
||||||
logger.error(f"✗ Action failed: {result.error}")
|
logger.error(f"✗ Action failed: {result.error}")
|
||||||
|
|
||||||
|
# ⚠️ IMPORTANT: Create error message for failed actions so user can see what went wrong
|
||||||
|
await self.createActionMessage(action, result, workflow, result_label, [], task_step, task_index)
|
||||||
|
|
||||||
|
# Create database log entry for action failure
|
||||||
|
if total_actions is not None:
|
||||||
|
self.chatInterface.createWorkflowLog({
|
||||||
|
"workflowId": workflow.id,
|
||||||
|
"message": f"❌ Task {task_num} - Action {action_num}/{total_actions} failed: {result.error}",
|
||||||
|
"type": "error"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.chatInterface.createWorkflowLog({
|
||||||
|
"workflowId": workflow.id,
|
||||||
|
"message": f"❌ Task {task_num} - Action {action_num}/? failed: {result.error}",
|
||||||
|
"type": "error"
|
||||||
|
})
|
||||||
|
|
||||||
# Extract document filenames for the ActionResult
|
# Extract document filenames for the ActionResult
|
||||||
document_filenames = []
|
document_filenames = []
|
||||||
for doc in created_documents:
|
for doc in created_documents:
|
||||||
|
|
@ -670,15 +687,20 @@ class HandlingTasks:
|
||||||
# Create a more meaningful message that includes task context
|
# Create a more meaningful message that includes task context
|
||||||
task_objective = task_step.objective if task_step else 'Unknown task'
|
task_objective = task_step.objective if task_step else 'Unknown task'
|
||||||
|
|
||||||
# Build a user-friendly message
|
# Build a user-friendly message based on success/failure
|
||||||
if created_documents and len(created_documents) > 0:
|
if result.success:
|
||||||
doc_names = [doc.filename if hasattr(doc, 'filename') else str(doc) for doc in created_documents[:3]]
|
if created_documents and len(created_documents) > 0:
|
||||||
if len(created_documents) > 3:
|
doc_names = [doc.filename if hasattr(doc, 'filename') else str(doc) for doc in created_documents[:3]]
|
||||||
doc_names.append(f"... and {len(created_documents) - 3} more")
|
if len(created_documents) > 3:
|
||||||
|
doc_names.append(f"... and {len(created_documents) - 3} more")
|
||||||
|
|
||||||
message_text = f"✅ Task {task_index or '?'} - Action {action.execMethod}.{action.execAction} completed\n\nObjective: {task_objective}\n\nGenerated {len(created_documents)} document(s): {', '.join(doc_names)}"
|
message_text = f"✅ Task {task_index or '?'} - Action {action.execMethod}.{action.execAction} completed\n\nObjective: {task_objective}\n\nGenerated {len(created_documents)} document(s): {', '.join(doc_names)}"
|
||||||
|
else:
|
||||||
|
message_text = f"✅ Task {task_index or '?'} - Action {action.execMethod}.{action.execAction} completed\n\nObjective: {task_objective}\n\nAction executed successfully"
|
||||||
else:
|
else:
|
||||||
message_text = f"✅ Task {task_index or '?'} - Action {action.execMethod}.{action.execAction} completed\n\nObjective: {task_objective}\n\nAction executed successfully"
|
# ⚠️ FAILURE MESSAGE - Show error details to user
|
||||||
|
error_details = result.error if result.error else "Unknown error occurred"
|
||||||
|
message_text = f"❌ Task {task_index or '?'} - Action {action.execMethod}.{action.execAction} failed\n\nObjective: {task_objective}\n\nError: {error_details}\n\nPlease check the connection and try again."
|
||||||
|
|
||||||
message_data = {
|
message_data = {
|
||||||
"workflowId": workflow.id,
|
"workflowId": workflow.id,
|
||||||
|
|
@ -694,6 +716,11 @@ class HandlingTasks:
|
||||||
"documents": created_documents
|
"documents": created_documents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add debugging for error messages
|
||||||
|
if not result.success:
|
||||||
|
logger.info(f"Creating ERROR message: {message_text}")
|
||||||
|
logger.info(f"Message data: {message_data}")
|
||||||
|
|
||||||
message = self.chatInterface.createWorkflowMessage(message_data)
|
message = self.chatInterface.createWorkflowMessage(message_data)
|
||||||
if message:
|
if message:
|
||||||
workflow.messages.append(message)
|
workflow.messages.append(message)
|
||||||
|
|
|
||||||
|
|
@ -441,24 +441,53 @@ class ServiceCenter:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def getConnectionReferenceList(self) -> List[str]:
|
def getConnectionReferenceList(self) -> List[str]:
|
||||||
"""Get list of all UserConnection objects as references"""
|
"""Get list of all UserConnection objects as references with enhanced state information"""
|
||||||
connections = []
|
connections = []
|
||||||
# Get user connections through AppObjects interface
|
# Get user connections through AppObjects interface
|
||||||
user_connections = self.interfaceApp.getUserConnections(self.user.id)
|
user_connections = self.interfaceApp.getUserConnections(self.user.id)
|
||||||
for conn in user_connections:
|
for conn in user_connections:
|
||||||
connections.append(self.getConnectionReferenceFromUserConnection(conn))
|
# Get enhanced connection reference with state information
|
||||||
|
enhanced_ref = self.getConnectionReferenceFromUserConnection(conn)
|
||||||
|
connections.append(enhanced_ref)
|
||||||
# Sort by connection reference
|
# Sort by connection reference
|
||||||
return sorted(connections)
|
return sorted(connections)
|
||||||
|
|
||||||
def getConnectionReferenceFromUserConnection(self, connection: UserConnection) -> str:
|
def getConnectionReferenceFromUserConnection(self, connection: UserConnection) -> str:
|
||||||
"""Get connection reference from UserConnection"""
|
"""Get connection reference from UserConnection with enhanced state information"""
|
||||||
return f"connection:{connection.authority.value}:{connection.externalUsername}:{connection.id}"
|
# Get token information to check if it's expired
|
||||||
|
token = None
|
||||||
|
token_status = "unknown"
|
||||||
|
try:
|
||||||
|
token = self.interfaceApp.getToken(connection.authority.value)
|
||||||
|
if token:
|
||||||
|
if hasattr(token, 'expiresAt') and token.expiresAt:
|
||||||
|
import time
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time > token.expiresAt:
|
||||||
|
token_status = "expired"
|
||||||
|
else:
|
||||||
|
token_status = "valid"
|
||||||
|
else:
|
||||||
|
token_status = "no_expiration"
|
||||||
|
else:
|
||||||
|
token_status = "no_token"
|
||||||
|
except Exception as e:
|
||||||
|
token_status = f"error: {str(e)}"
|
||||||
|
|
||||||
|
# Build enhanced reference with state information
|
||||||
|
base_ref = f"connection:{connection.authority.value}:{connection.externalUsername}:{connection.id}"
|
||||||
|
state_info = f" [status:{connection.status.value}, token:{token_status}]"
|
||||||
|
|
||||||
|
return base_ref + state_info
|
||||||
|
|
||||||
def getUserConnectionFromConnectionReference(self, connectionReference: str) -> Optional[UserConnection]:
|
def getUserConnectionFromConnectionReference(self, connectionReference: str) -> Optional[UserConnection]:
|
||||||
"""Get UserConnection from reference string"""
|
"""Get UserConnection from reference string (handles both old and enhanced formats)"""
|
||||||
try:
|
try:
|
||||||
# Parse reference format: connection:{authority}:{username}:{id}
|
# Parse reference format: connection:{authority}:{username}:{id} [status:..., token:...]
|
||||||
parts = connectionReference.split(':')
|
# Remove state information if present
|
||||||
|
base_reference = connectionReference.split(' [')[0]
|
||||||
|
|
||||||
|
parts = base_reference.split(':')
|
||||||
if len(parts) != 4 or parts[0] != "connection":
|
if len(parts) != 4 or parts[0] != "connection":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,17 @@ class MethodOutlook(MethodBase):
|
||||||
"""Get Microsoft connection from connection reference"""
|
"""Get Microsoft connection from connection reference"""
|
||||||
try:
|
try:
|
||||||
userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
|
userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
|
||||||
if not userConnection or userConnection.authority.value != "msft" or userConnection.status.value != "active":
|
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
|
return None
|
||||||
|
|
||||||
# Get the corresponding token for this user and authority
|
# Get the corresponding token for this user and authority
|
||||||
|
|
@ -35,6 +45,16 @@ class MethodOutlook(MethodBase):
|
||||||
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority.value}")
|
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority.value}")
|
||||||
return None
|
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 {
|
return {
|
||||||
"id": userConnection.id,
|
"id": userConnection.id,
|
||||||
"accessToken": token.tokenAccess,
|
"accessToken": token.tokenAccess,
|
||||||
|
|
@ -45,6 +65,118 @@ class MethodOutlook(MethodBase):
|
||||||
logger.error(f"Error getting Microsoft connection: {str(e)}")
|
logger.error(f"Error getting Microsoft connection: {str(e)}")
|
||||||
return None
|
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
|
@action
|
||||||
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
|
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||||
"""
|
"""
|
||||||
|
|
@ -284,11 +416,34 @@ class MethodOutlook(MethodBase):
|
||||||
message["attachments"].append(attachment)
|
message["attachments"].append(attachment)
|
||||||
|
|
||||||
# Create the draft message
|
# Create the draft message
|
||||||
api_url = f"{graph_url}/me/messages"
|
# 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 = requests.post(api_url, headers=headers, json=message)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
draft_data = response.json()
|
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 = {
|
draft_result = {
|
||||||
"status": "draft_created",
|
"status": "draft_created",
|
||||||
"messageId": draft_data.get("id", "unknown"),
|
"messageId": draft_data.get("id", "unknown"),
|
||||||
|
|
@ -297,12 +452,13 @@ class MethodOutlook(MethodBase):
|
||||||
"cc": cc,
|
"cc": cc,
|
||||||
"bcc": bcc,
|
"bcc": bcc,
|
||||||
"attachments": len(attachments) if attachments else 0,
|
"attachments": len(attachments) if attachments else 0,
|
||||||
"draftLocation": "Drafts folder",
|
"draftLocation": actual_folder,
|
||||||
|
"draftsFolderId": drafts_folder_id,
|
||||||
"apiResponse": response.status_code,
|
"apiResponse": response.status_code,
|
||||||
"draftData": draft_data
|
"draftData": draft_data
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"Successfully created email draft for {len(to)} recipients with {len(attachments) if attachments else 0} attachments")
|
logger.info(f"Successfully created email draft for {len(to)} recipients with {len(attachments) if attachments else 0} attachments in {actual_folder}")
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.error("requests module not available, falling back to simulation")
|
logger.error("requests module not available, falling back to simulation")
|
||||||
|
|
@ -420,13 +576,31 @@ class MethodOutlook(MethodBase):
|
||||||
limit = parameters.get("limit", 20)
|
limit = parameters.get("limit", 20)
|
||||||
expectedDocumentFormats = parameters.get("expectedDocumentFormats", [])
|
expectedDocumentFormats = parameters.get("expectedDocumentFormats", [])
|
||||||
|
|
||||||
if not connectionReference or not query:
|
# Validate parameters
|
||||||
|
if not connectionReference:
|
||||||
return self._createResult(
|
return self._createResult(
|
||||||
success=False,
|
success=False,
|
||||||
data={},
|
data={},
|
||||||
error="Connection reference and query are required"
|
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
|
# Get Microsoft connection
|
||||||
connection = self._getMicrosoftConnection(connectionReference)
|
connection = self._getMicrosoftConnection(connectionReference)
|
||||||
if not connection:
|
if not connection:
|
||||||
|
|
@ -449,31 +623,88 @@ class MethodOutlook(MethodBase):
|
||||||
|
|
||||||
# Build the search API request
|
# Build the search API request
|
||||||
api_url = f"{graph_url}/me/messages"
|
api_url = f"{graph_url}/me/messages"
|
||||||
params = {
|
params = self._buildSearchParameters(query, folder, limit)
|
||||||
"$top": limit,
|
|
||||||
"$orderby": "receivedDateTime desc",
|
|
||||||
"$search": f'"{query}"'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add folder filter if specified
|
logger.info(f"Search API parameters: {params}")
|
||||||
if folder and folder.lower() != "all":
|
|
||||||
params["$filter"] = f"parentFolderId eq '{folder}'"
|
|
||||||
|
|
||||||
# Make the API call
|
# Make the API call
|
||||||
response = requests.get(api_url, headers=headers, params=params)
|
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()
|
response.raise_for_status()
|
||||||
|
|
||||||
search_data = response.json()
|
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 = {
|
search_result = {
|
||||||
"query": query,
|
"query": query,
|
||||||
"results": search_data.get("value", []),
|
"results": emails,
|
||||||
"count": len(search_data.get("value", [])),
|
"count": len(emails),
|
||||||
"folder": folder,
|
"folder": folder,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"apiResponse": search_data
|
"apiResponse": search_data,
|
||||||
|
"searchParams": params
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"Successfully searched emails with query '{query}', found {len(search_data.get('value', []))} results")
|
logger.info(f"Successfully searched emails with query '{query}', found {len(emails)} results")
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.error("requests module not available, falling back to simulation")
|
logger.error("requests module not available, falling back to simulation")
|
||||||
|
|
@ -560,3 +791,170 @@ class MethodOutlook(MethodBase):
|
||||||
data={},
|
data={},
|
||||||
error=str(e)
|
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)
|
||||||
|
)
|
||||||
|
|
@ -28,7 +28,17 @@ class MethodSharepoint(MethodBase):
|
||||||
"""Get Microsoft connection from connection reference"""
|
"""Get Microsoft connection from connection reference"""
|
||||||
try:
|
try:
|
||||||
userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
|
userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
|
||||||
if not userConnection or userConnection.authority.value != "msft" or userConnection.status.value != "active":
|
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
|
return None
|
||||||
|
|
||||||
# Get the corresponding token for this user and authority
|
# Get the corresponding token for this user and authority
|
||||||
|
|
@ -37,6 +47,16 @@ class MethodSharepoint(MethodBase):
|
||||||
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority.value}")
|
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority.value}")
|
||||||
return None
|
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 {
|
return {
|
||||||
"id": userConnection.id,
|
"id": userConnection.id,
|
||||||
"accessToken": token.tokenAccess,
|
"accessToken": token.tokenAccess,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue