fixed mailing
This commit is contained in:
parent
8592cdd790
commit
d642fa0f07
4 changed files with 508 additions and 34 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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={},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue