From d642fa0f071830791c23073a32b1acde3ead5ff5 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 17 Aug 2025 00:19:55 +0200
Subject: [PATCH] fixed mailing
---
modules/chat/handling/handlingTasks.py | 43 ++-
modules/chat/serviceCenter.py | 43 ++-
modules/methods/methodOutlook.py | 434 ++++++++++++++++++++++++-
modules/methods/methodSharepoint.py | 22 +-
4 files changed, 508 insertions(+), 34 deletions(-)
diff --git a/modules/chat/handling/handlingTasks.py b/modules/chat/handling/handlingTasks.py
index c04658f7..c588e0a7 100644
--- a/modules/chat/handling/handlingTasks.py
+++ b/modules/chat/handling/handlingTasks.py
@@ -593,6 +593,23 @@ class HandlingTasks:
else:
action.setError(result.error or "Action execution failed")
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
document_filenames = []
@@ -670,15 +687,20 @@ class HandlingTasks:
# Create a more meaningful message that includes task context
task_objective = task_step.objective if task_step else 'Unknown task'
- # Build a user-friendly message
- if created_documents and len(created_documents) > 0:
- doc_names = [doc.filename if hasattr(doc, 'filename') else str(doc) for doc in created_documents[:3]]
- 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)}"
+ # Build a user-friendly message based on success/failure
+ if result.success:
+ if created_documents and len(created_documents) > 0:
+ doc_names = [doc.filename if hasattr(doc, 'filename') else str(doc) for doc in created_documents[:3]]
+ 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)}"
+ else:
+ message_text = f"✅ Task {task_index or '?'} - Action {action.execMethod}.{action.execAction} completed\n\nObjective: {task_objective}\n\nAction executed successfully"
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 = {
"workflowId": workflow.id,
@@ -694,6 +716,11 @@ class HandlingTasks:
"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)
if message:
workflow.messages.append(message)
diff --git a/modules/chat/serviceCenter.py b/modules/chat/serviceCenter.py
index b5595ba6..3e3d6aa1 100644
--- a/modules/chat/serviceCenter.py
+++ b/modules/chat/serviceCenter.py
@@ -441,24 +441,53 @@ class ServiceCenter:
return []
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 = []
# Get user connections through AppObjects interface
user_connections = self.interfaceApp.getUserConnections(self.user.id)
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
return sorted(connections)
def getConnectionReferenceFromUserConnection(self, connection: UserConnection) -> str:
- """Get connection reference from UserConnection"""
- return f"connection:{connection.authority.value}:{connection.externalUsername}:{connection.id}"
+ """Get connection reference from UserConnection with enhanced state information"""
+ # 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]:
- """Get UserConnection from reference string"""
+ """Get UserConnection from reference string (handles both old and enhanced formats)"""
try:
- # Parse reference format: connection:{authority}:{username}:{id}
- parts = connectionReference.split(':')
+ # Parse reference format: connection:{authority}:{username}:{id} [status:..., token:...]
+ # Remove state information if present
+ base_reference = connectionReference.split(' [')[0]
+
+ parts = base_reference.split(':')
if len(parts) != 4 or parts[0] != "connection":
return None
diff --git a/modules/methods/methodOutlook.py b/modules/methods/methodOutlook.py
index 6c9ed260..e051b631 100644
--- a/modules/methods/methodOutlook.py
+++ b/modules/methods/methodOutlook.py
@@ -26,7 +26,17 @@ class MethodOutlook(MethodBase):
"""Get Microsoft connection from connection reference"""
try:
userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
- if not userConnection or userConnection.authority.value != "msft" or userConnection.status.value != "active":
+ 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
@@ -35,6 +45,16 @@ class MethodOutlook(MethodBase):
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,
@@ -45,6 +65,118 @@ class MethodOutlook(MethodBase):
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:
"""
@@ -284,11 +416,34 @@ class MethodOutlook(MethodBase):
message["attachments"].append(attachment)
# 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.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"),
@@ -297,12 +452,13 @@ class MethodOutlook(MethodBase):
"cc": cc,
"bcc": bcc,
"attachments": len(attachments) if attachments else 0,
- "draftLocation": "Drafts folder",
+ "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")
+ 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")
@@ -420,13 +576,31 @@ class MethodOutlook(MethodBase):
limit = parameters.get("limit", 20)
expectedDocumentFormats = parameters.get("expectedDocumentFormats", [])
- if not connectionReference or not query:
+ # Validate parameters
+ if not connectionReference:
return self._createResult(
success=False,
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
connection = self._getMicrosoftConnection(connectionReference)
if not connection:
@@ -449,31 +623,88 @@ class MethodOutlook(MethodBase):
# Build the search API request
api_url = f"{graph_url}/me/messages"
- params = {
- "$top": limit,
- "$orderby": "receivedDateTime desc",
- "$search": f'"{query}"'
- }
+ params = self._buildSearchParameters(query, folder, limit)
- # Add folder filter if specified
- if folder and folder.lower() != "all":
- params["$filter"] = f"parentFolderId eq '{folder}'"
+ 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": search_data.get("value", []),
- "count": len(search_data.get("value", [])),
+ "results": emails,
+ "count": len(emails),
"folder": folder,
"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:
logger.error("requests module not available, falling back to simulation")
@@ -555,6 +786,173 @@ class MethodOutlook(MethodBase):
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={},
diff --git a/modules/methods/methodSharepoint.py b/modules/methods/methodSharepoint.py
index 884974c3..7205676d 100644
--- a/modules/methods/methodSharepoint.py
+++ b/modules/methods/methodSharepoint.py
@@ -28,7 +28,17 @@ class MethodSharepoint(MethodBase):
"""Get Microsoft connection from connection reference"""
try:
userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
- if not userConnection or userConnection.authority.value != "msft" or userConnection.status.value != "active":
+ 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
@@ -37,6 +47,16 @@ class MethodSharepoint(MethodBase):
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,