From f1e06553eb64820b8eaa5205dcedb795074a9cda Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Wed, 20 Aug 2025 01:04:53 +0200
Subject: [PATCH] doc and mail final r1
---
modules/interfaces/interfaceAppObjects.py | 12 +-
modules/methods/methodAi.py | 20 +-
modules/methods/methodOutlook.py | 231 +++++++++++++---------
modules/routes/routeSecurityGoogle.py | 4 +-
modules/routes/routeSecurityMsft.py | 8 +-
modules/security/tokenManager.py | 4 +-
test_outlook_filters.py | 51 +++++
test_outlook_filters_fixed.py | 70 +++++++
8 files changed, 279 insertions(+), 121 deletions(-)
create mode 100644 test_outlook_filters.py
create mode 100644 test_outlook_filters_fixed.py
diff --git a/modules/interfaces/interfaceAppObjects.py b/modules/interfaces/interfaceAppObjects.py
index 3eeccc84..be5b7288 100644
--- a/modules/interfaces/interfaceAppObjects.py
+++ b/modules/interfaces/interfaceAppObjects.py
@@ -541,7 +541,7 @@ class AppObjects:
tokens = self.db.getRecordset("tokens", recordFilter={"userId": userId})
for token in tokens:
self.db.recordDelete("tokens", token["id"])
- logger.debug(f"Deleted token {token['id']} for user {userId}")
+
# Delete user connections
connections = self.db.getRecordset("connections", recordFilter={"userId": userId})
@@ -771,7 +771,7 @@ class AppObjects:
# Clear cache to ensure fresh data
self._clearTableCache("tokens")
- logger.debug(f"Token saved for user {self.currentUser.id} with authority {token.authority}")
+
except Exception as e:
logger.error(f"Error saving token: {str(e)}")
@@ -796,7 +796,7 @@ class AppObjects:
# Check if token is expired
if latest_token.expiresAt and latest_token.expiresAt < datetime.now().timestamp():
if auto_refresh:
- logger.info(f"Token for {authority} is expired, attempting refresh...")
+
# Import TokenManager here to avoid circular imports
from modules.security.tokenManager import TokenManager
@@ -809,7 +809,7 @@ class AppObjects:
self.saveToken(refreshed_token)
self.deleteToken(authority)
- logger.info(f"Successfully refreshed token for {authority}")
+
return refreshed_token
else:
logger.warning(f"Failed to refresh expired token for {authority}")
@@ -843,7 +843,7 @@ class AppObjects:
# Check if token is expired
if latest_token.expiresAt and latest_token.expiresAt < datetime.now().timestamp():
if auto_refresh:
- logger.info(f"Token for connection {connectionId} is expired, attempting refresh...")
+
# Import TokenManager here to avoid circular imports
from modules.security.tokenManager import TokenManager
@@ -856,7 +856,7 @@ class AppObjects:
self.saveToken(refreshed_token)
self.deleteTokenByConnectionId(connectionId)
- logger.info(f"Successfully refreshed token for connection {connectionId}")
+
return refreshed_token
else:
logger.warning(f"Failed to refresh expired token for connection {connectionId}")
diff --git a/modules/methods/methodAi.py b/modules/methods/methodAi.py
index 3e5bcad7..8e18efd6 100644
--- a/modules/methods/methodAi.py
+++ b/modules/methods/methodAi.py
@@ -47,6 +47,16 @@ class MethodAi(MethodBase):
error="AI prompt is required"
)
+ # Determine output format first (needed for context building)
+ output_extension = ".txt" # Default
+ output_mime_type = "text/plain" # Default
+
+ if expectedDocumentFormats and len(expectedDocumentFormats) > 0:
+ expected_format = expectedDocumentFormats[0]
+ output_extension = expected_format.get("extension", ".txt")
+ output_mime_type = expected_format.get("mimeType", "text/plain")
+ logger.info(f"Using expected format: {output_extension} ({output_mime_type})")
+
# Build context from documents if provided
context = ""
if documentList:
@@ -128,16 +138,6 @@ class MethodAi(MethodBase):
context = context_header + "\n\n" + "\n\n".join(context_parts)
logger.info(f"Included {len(chatDocuments)} documents in AI context with task-specific extraction")
- # Determine output format
- output_extension = ".txt" # Default
- output_mime_type = "text/plain" # Default
-
- if expectedDocumentFormats and len(expectedDocumentFormats) > 0:
- expected_format = expectedDocumentFormats[0]
- output_extension = expected_format.get("extension", ".txt")
- output_mime_type = expected_format.get("mimeType", "text/plain")
- logger.info(f"Using expected format: {output_extension} ({output_mime_type})")
-
# Build enhanced prompt
enhanced_prompt = aiPrompt
diff --git a/modules/methods/methodOutlook.py b/modules/methods/methodOutlook.py
index 771e9ffb..8d694273 100644
--- a/modules/methods/methodOutlook.py
+++ b/modules/methods/methodOutlook.py
@@ -126,7 +126,7 @@ class MethodOutlook(MethodBase):
logger.error(f"Connection is not active: {userConnection.id}, status: {userConnection.status.value}")
return None
- logger.info(f"Successfully retrieved Microsoft connection: {userConnection.id}, status: {userConnection.status.value}, externalId: {userConnection.externalId}")
+
return {
"id": userConnection.id,
@@ -156,7 +156,7 @@ class MethodOutlook(MethodBase):
response = requests.get(test_url, headers=headers)
if response.status_code == 200:
- logger.info("✅ Permission check passed - connection has necessary mail permissions")
+
return True
elif response.status_code == 403:
logger.error("❌ Permission denied - connection lacks necessary mail permissions")
@@ -227,19 +227,18 @@ class MethodOutlook(MethodBase):
# 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 $orderby or $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")
+ # Folder filtering will be done after the API call
else:
# Use $filter for basic text search, but keep it simple to avoid "InefficientFilter" error
# Microsoft Graph API has limitations on complex filters
if len(clean_query) > 50:
# If query is too long, truncate it to avoid complex filter issues
clean_query = clean_query[:50]
- logger.info(f"Query truncated to avoid complex filter: {clean_query}")
+
# Use only subject search to keep filter simple
params["$filter"] = f"contains(subject,'{clean_query}')"
@@ -250,10 +249,36 @@ class MethodOutlook(MethodBase):
# Add orderby for basic queries
params["$orderby"] = "receivedDateTime desc"
- logger.info(f"Using simple text search filter: {clean_query}")
+
return params
+ def _buildGraphFilter(self, filter_text: str) -> Dict[str, str]:
+ """
+ Build proper Microsoft Graph API filter parameters based on filter text
+
+ Args:
+ filter_text (str): The filter text to process
+
+ Returns:
+ Dict[str, str]: Dictionary with either $filter or $search parameter
+ """
+ if not filter_text:
+ return {}
+
+ filter_text = filter_text.strip()
+
+ # Handle search queries (from:, to:, subject:, etc.) - check this FIRST
+ if any(filter_text.startswith(prefix) for prefix in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']):
+ return {"$search": f'"{filter_text}"'}
+
+ # Handle email address filters (only if it's NOT a search query)
+ if '@' in filter_text and '.' in filter_text and ' ' not in filter_text and not filter_text.startswith('from:'):
+ return {"$filter": f"from/fromAddress/address eq '{filter_text}'"}
+
+ # Handle text content - search in subject
+ return {"$filter": f"contains(subject,'{filter_text}')"}
+
def _getFolderId(self, folder_name: str, connection: Dict[str, Any]) -> Optional[str]:
"""
Get the folder ID for a given folder name
@@ -277,13 +302,12 @@ class MethodOutlook(MethodBase):
folders_data = response.json()
all_folders = folders_data.get("value", [])
- # Log all available folders for debugging
- logger.info(f"Available folders: {[f.get('displayName', 'Unknown') for f in all_folders]}")
+
# Try exact match first
for folder in all_folders:
if folder.get("displayName", "").lower() == folder_name.lower():
- logger.info(f"Found folder '{folder_name}' with ID: {folder.get('id')}")
+
return folder.get("id")
# Try common variations for Drafts folder
@@ -292,7 +316,7 @@ class MethodOutlook(MethodBase):
for folder in all_folders:
folder_display_name = folder.get("displayName", "").lower()
if any(variation in folder_display_name for variation in draft_variations):
- logger.info(f"Found Drafts folder variation '{folder.get('displayName')}' with ID: {folder.get('id')}")
+
return folder.get("id")
# Try common variations for other folders
@@ -301,7 +325,7 @@ class MethodOutlook(MethodBase):
for folder in all_folders:
folder_display_name = folder.get("displayName", "").lower()
if any(variation in folder_display_name for variation in sent_variations):
- logger.info(f"Found Sent Items folder variation '{folder.get('displayName')}' with ID: {folder.get('id')}")
+
return folder.get("id")
logger.warning(f"Folder '{folder_name}' not found. Available folders: {[f.get('displayName', 'Unknown') for f in all_folders]}")
@@ -323,7 +347,10 @@ class MethodOutlook(MethodBase):
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
+ filter (str, optional): Filter criteria for emails. Supports:
+ - Email address (e.g., "user@domain.com") - filters by sender
+ - Search queries (e.g., "from:user@domain.com", "subject:meeting")
+ - Text content (e.g., "project update") - searches in subject
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
"""
try:
@@ -336,6 +363,15 @@ class MethodOutlook(MethodBase):
if not connectionReference:
return ActionResult.failure(error="Connection reference is required")
+ # Validate filter parameter if provided
+ if filter:
+ # Remove any potentially dangerous characters that could break the filter
+ filter = filter.strip()
+ if len(filter) > 100:
+ logger.warning(f"Filter too long ({len(filter)} chars), truncating to 100 characters")
+ filter = filter[:100]
+
+
# Get Microsoft connection
connection = self._getMicrosoftConnection(connectionReference)
if not connection:
@@ -360,10 +396,27 @@ class MethodOutlook(MethodBase):
}
if filter:
- params["$filter"] = filter
+ # Build proper Graph API filter parameters
+ filter_params = self._buildGraphFilter(filter)
+ params.update(filter_params)
+
+ # If using $search, remove $orderby as they can't be combined
+ if "$search" in params:
+ params.pop("$orderby", None)
+
+ # Filter applied
# Make the API call
+
+
response = requests.get(api_url, headers=headers, params=params)
+
+ if response.status_code != 200:
+ logger.error(f"Graph API error: {response.status_code} - {response.text}")
+ logger.error(f"Request URL: {response.url}")
+ logger.error(f"Request headers: {headers}")
+ logger.error(f"Request params: {params}")
+
response.raise_for_status()
emails_data = response.json()
@@ -375,11 +428,24 @@ class MethodOutlook(MethodBase):
"apiResponse": emails_data
}
- logger.info(f"Successfully retrieved {len(emails_data.get('value', []))} emails from {folder}")
+
except ImportError:
logger.error("requests module not available")
return ActionResult.failure(error="requests module not available")
+ except requests.exceptions.HTTPError as e:
+ if e.response.status_code == 400:
+ logger.error(f"Bad Request (400) - Invalid filter or parameter: {e.response.text}")
+ return ActionResult.failure(error=f"Invalid filter syntax. Please check your filter parameter. Error: {e.response.text}")
+ elif e.response.status_code == 401:
+ logger.error("Unauthorized (401) - Access token may be expired or invalid")
+ return ActionResult.failure(error="Authentication failed. Please check your connection and try again.")
+ elif e.response.status_code == 403:
+ logger.error("Forbidden (403) - Insufficient permissions to access emails")
+ return ActionResult.failure(error="Insufficient permissions to read emails from this folder.")
+ else:
+ logger.error(f"HTTP Error {e.response.status_code}: {e.response.text}")
+ return ActionResult.failure(error=f"HTTP Error {e.response.status_code}: {e.response.text}")
except Exception as e:
logger.error(f"Error reading emails from Microsoft Graph API: {str(e)}")
return ActionResult.failure(error=f"Failed to read emails: {str(e)}")
@@ -468,19 +534,19 @@ class MethodOutlook(MethodBase):
return ActionResult.failure(error="Failed to get Microsoft connection")
# Get the composed email document
- logger.info(f"Getting composed email document: {composed_email_ref}")
+
composed_email_docs = self.service.getChatDocumentsFromDocumentList([composed_email_ref])
if not composed_email_docs or len(composed_email_docs) == 0:
logger.error(f"Could not find composed email document: {composed_email_ref}")
return ActionResult.failure(error=f"Could not find composed email document: {composed_email_ref}")
- logger.info(f"Found {len(composed_email_docs)} composed email documents")
+
composed_email_doc = composed_email_docs[0]
- logger.info(f"Composed email document: {composed_email_doc}")
+
# Extract email details from the composed email document
try:
- logger.info(f"Extracting email details from document...")
+
# Get the actual file content from the database
# The document object has fileId, but we need to read the actual file content
@@ -489,7 +555,7 @@ class MethodOutlook(MethodBase):
logger.error("Document has no fileId attribute")
return ActionResult.failure(error="Composed email document has no fileId")
- logger.info(f"Reading file content from fileId: {file_id}")
+
# Read the actual file content from the database
try:
@@ -499,19 +565,19 @@ class MethodOutlook(MethodBase):
logger.error(f"Failed to read file content for fileId: {file_id}")
return ActionResult.failure(error="Failed to read composed email file content")
- logger.info(f"Successfully read file content, length: {len(str(file_content))}")
+
# Convert bytes to string if needed
if isinstance(file_content, bytes):
email_data = file_content.decode('utf-8')
- logger.info(f"Converted bytes to string, content length: {len(email_data)}")
+
else:
email_data = str(file_content)
- logger.info(f"Content is already string, length: {len(email_data)}")
+
# Debug: show first 200 characters of content
preview = email_data[:200] + "..." if len(email_data) > 200 else email_data
- logger.info(f"Content preview: {repr(preview)}")
+
except Exception as e:
logger.error(f"Error reading file content: {str(e)}")
@@ -523,14 +589,14 @@ class MethodOutlook(MethodBase):
try:
# First try to parse as direct JSON
parsed_email_data = json.loads(email_data)
- logger.info("Successfully parsed email data as direct JSON")
+
email_data = parsed_email_data # Now email_data is the parsed dictionary
except json.JSONDecodeError as e:
logger.error(f"JSON parsing error: {str(e)}")
logger.error(f"Content that failed to parse: {repr(email_data[:500])}")
# If that fails, try to extract JSON from HTML content
- logger.info("Direct JSON parsing failed, trying to extract from HTML content...")
+
import re
# Look for JSON content within HTML tags or as a script
@@ -541,7 +607,7 @@ class MethodOutlook(MethodBase):
try:
extracted_json = json_match.group(0)
parsed_email_data = json.loads(extracted_json)
- logger.info("Successfully extracted and parsed JSON from HTML content")
+
email_data = parsed_email_data # Now email_data is the parsed dictionary
except json.JSONDecodeError as e2:
logger.error(f"Failed to parse extracted JSON: {str(e2)}")
@@ -555,9 +621,7 @@ class MethodOutlook(MethodBase):
return ActionResult.failure(error=f"Unexpected email data type: {type(email_data)}, expected string")
# At this point, email_data should be a parsed dictionary
- logger.info(f"Final email_data type: {type(email_data)}")
- if isinstance(email_data, dict):
- logger.info(f"Available keys: {list(email_data.keys())}")
+
# Extract email fields - now they should be at root level
to = email_data.get("to", [])
@@ -572,19 +636,19 @@ class MethodOutlook(MethodBase):
logger.error(f"Missing required fields. Available keys: {list(email_data.keys())}")
return ActionResult.failure(error="Composed email must contain 'to', 'subject', and 'body' fields")
- logger.info(f"Extracted email details: to={to}, subject='{subject}', body length={len(body)}, attachments={len(attachments)}")
+
except Exception as e:
logger.error(f"Error parsing composed email document: {str(e)}")
return ActionResult.failure(error=f"Failed to parse composed email document: {str(e)}")
# Check permissions before proceeding
- logger.info("Checking Microsoft Graph API permissions...")
+
permissions_ok = await self._checkPermissions(connection)
if not permissions_ok:
logger.error("Permission check failed")
return ActionResult.failure(error="Connection lacks necessary permissions for Outlook operations")
- logger.info("Permission check passed")
+
# Create email draft using Microsoft Graph API
try:
@@ -604,13 +668,13 @@ class MethodOutlook(MethodBase):
if cleaned_body.startswith('') or cleaned_body.startswith('') or '
' in cleaned_body:
# Body is already HTML, use as-is
html_body = cleaned_body
- logger.info("Body content is already HTML formatted")
+
else:
# Convert plain text to proper HTML formatting
# Replace newlines with
tags and wrap in proper HTML structure
html_body = cleaned_body.replace('\n', '
')
html_body = f"{html_body}"
- logger.info("Converted plain text to HTML format")
+
# Build the email message
message = {
@@ -628,13 +692,13 @@ class MethodOutlook(MethodBase):
if attachments:
message["attachments"] = []
for attachment_ref in attachments:
- logger.info(f"Processing attachment: {attachment_ref}")
+
# Get attachment document from service center
attachment_docs = self.service.getChatDocumentsFromDocumentList([attachment_ref])
if attachment_docs:
for doc in attachment_docs:
- logger.info(f"Found attachment document: {doc.filename}, fileId: {getattr(doc, 'fileId', 'None')}")
+
# Get the actual file content using fileId
file_id = getattr(doc, 'fileId', None)
@@ -660,7 +724,7 @@ class MethodOutlook(MethodBase):
"contentBytes": base64_content
}
message["attachments"].append(attachment)
- logger.info(f"✅ Successfully added attachment: {doc.filename} (size: {len(content_bytes)} bytes)")
+
else:
logger.warning(f"⚠️ No content found for attachment: {doc.filename}")
except Exception as e:
@@ -677,33 +741,22 @@ class MethodOutlook(MethodBase):
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})")
- logger.info(f"Target folder: Drafts (Entwürfe)")
- logger.info(f"Mailbox account: {connection.get('userEmail', 'Unknown')}")
+
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")
- logger.info(f"Mailbox account: {connection.get('userEmail', 'Unknown')}")
+
- logger.info(f"Creating draft with API URL: {api_url}")
- logger.info(f"Email body preview: {html_body[:200]}...")
- logger.info(f"Number of attachments: {len(message.get('attachments', []))}")
- if message.get('attachments'):
- for i, att in enumerate(message['attachments']):
- logger.info(f" Attachment {i+1}: {att['name']} ({att['contentType']}) - Content size: {len(att['contentBytes'])} chars")
- logger.info(f"Draft message data: {json.dumps(message, indent=2)}")
+
+
response = requests.post(api_url, headers=headers, json=message)
if response.status_code in [200, 201]:
draft_data = response.json()
draft_id = draft_data.get("id", "Unknown")
- logger.info(f"✅ Email draft created successfully!")
- logger.info(f"📧 Draft ID: {draft_id}")
- logger.info(f"📁 Stored in: Drafts folder (Entwürfe)")
- logger.info(f"📬 Mailbox: {connection.get('userEmail', 'Unknown')}")
- logger.info(f"🔗 Draft URL: {api_url}")
+
# Return success with draft information
# Create document reference in standard format
@@ -826,14 +879,13 @@ class MethodOutlook(MethodBase):
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
@@ -863,7 +915,7 @@ class MethodOutlook(MethodBase):
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:
@@ -877,7 +929,7 @@ class MethodOutlook(MethodBase):
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 = []
@@ -889,10 +941,10 @@ class MethodOutlook(MethodBase):
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,
@@ -904,7 +956,7 @@ class MethodOutlook(MethodBase):
"searchParams": params
}
- logger.info(f"Successfully searched emails with query '{query}', found {len(emails)} results")
+
except ImportError:
logger.error("requests module not available")
@@ -995,7 +1047,7 @@ class MethodOutlook(MethodBase):
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"
@@ -1018,7 +1070,7 @@ class MethodOutlook(MethodBase):
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,
@@ -1029,7 +1081,7 @@ class MethodOutlook(MethodBase):
"apiResponse": messages_data
}
- logger.info(f"Successfully retrieved {len(messages)} drafts from folder '{folder}'")
+
except ImportError:
logger.error("requests module not available")
@@ -1119,7 +1171,7 @@ class MethodOutlook(MethodBase):
"$filter": "isDraft eq true"
}
- logger.info(f"Searching for drafts across all folders")
+
# Make the API call
response = requests.get(api_url, headers=headers, params=params)
@@ -1141,7 +1193,7 @@ class MethodOutlook(MethodBase):
"apiResponse": messages_data
}
- logger.info(f"Successfully found {len(drafts)} drafts across all folders")
+
except ImportError:
logger.error("requests module not available")
@@ -1265,7 +1317,7 @@ class MethodOutlook(MethodBase):
"$orderby": "lastModifiedDateTime desc"
}
- logger.info(f"Checking Drafts folder directly: {api_url}")
+
# Make the API call
response = requests.get(api_url, headers=headers, params=params)
@@ -1274,9 +1326,7 @@ class MethodOutlook(MethodBase):
messages_data = response.json()
drafts = messages_data.get("value", [])
- # Log detailed information about each draft
- for i, draft in enumerate(drafts):
- logger.info(f"Draft {i+1}: ID={draft.get('id')}, Subject='{draft.get('subject')}', Modified={draft.get('lastModifiedDateTime')}")
+
drafts_result = {
"draftsFolderId": drafts_folder_id,
@@ -1287,7 +1337,7 @@ class MethodOutlook(MethodBase):
"apiUrl": api_url
}
- logger.info(f"Successfully checked Drafts folder: found {len(drafts)} drafts")
+
except ImportError:
logger.error("requests module not available")
@@ -1391,7 +1441,7 @@ class MethodOutlook(MethodBase):
composition_documents = []
if documentList:
- logger.info(f"Processing {len(documentList)} input documents for email composition context")
+
try:
# Get document content from service center
docs = self.service.getChatDocumentsFromDocumentList(documentList)
@@ -1421,15 +1471,11 @@ class MethodOutlook(MethodBase):
# Truncate content for AI context (avoid token limits)
content_preview = content_text[:1000] + "..." if len(content_text) > 1000 else content_text
document_content_summary += f"\nDocument: {doc.filename}\nContent Preview: {content_preview}\n"
- logger.info(f"Extracted content preview from {doc.filename}: {len(content_text)} characters")
- else:
- logger.info(f"No readable text content extracted from {doc.filename} (binary file)")
- else:
- logger.info(f"No content extracted from {doc.filename} (binary file)")
+ # No content to extract
except Exception as extract_error:
- # If content extraction fails, log info instead of warning (this is normal for binary files)
- logger.info(f"Could not extract text content from {doc.filename}: {str(extract_error)} (this is normal for binary files like PDFs)")
+ # Content extraction failed (normal for binary files)
+ pass
else:
logger.warning(f"Document {doc.filename} has no fileId")
except Exception as e:
@@ -1443,7 +1489,7 @@ class MethodOutlook(MethodBase):
all_attachments = []
if attachmentDocumentList:
- logger.info(f"Processing {len(attachmentDocumentList)} documents as email attachments")
+
try:
# Get attachment documents from service center
attachment_docs = self.service.getChatDocumentsFromDocumentList(attachmentDocumentList)
@@ -1451,7 +1497,7 @@ class MethodOutlook(MethodBase):
for doc in attachment_docs:
# Add to attachments list
all_attachments.append(f"docItem:{doc.id}:{doc.filename}")
- logger.info(f"Added attachment: {doc.filename}")
+
else:
logger.warning("No attachment documents found from attachmentDocumentList")
except Exception as e:
@@ -1460,7 +1506,7 @@ class MethodOutlook(MethodBase):
# Add any explicit attachments to the list
if attachments:
all_attachments.extend(attachments)
- logger.info(f"Added {len(attachments)} explicit attachments to email")
+
# Remove duplicates while preserving order
seen = set()
@@ -1470,11 +1516,7 @@ class MethodOutlook(MethodBase):
seen.add(att)
unique_attachments.append(att)
- logger.info(f"Total unique attachments for email: {len(unique_attachments)}")
- logger.info(f"Documents used for composition context: {len(composition_documents)}")
- if document_content_summary:
- logger.info(f"Document content summary length: {len(document_content_summary)} characters")
- logger.info(f"Document content preview: {document_content_summary[:200]}...")
+
# Build AI prompt for email composition
ai_prompt = f"""
@@ -1533,7 +1575,7 @@ class MethodOutlook(MethodBase):
if missing_fields:
raise ValueError(f"Missing required fields: {missing_fields}")
- logger.info("AI response successfully parsed and validated")
+
except json.JSONDecodeError as e:
logger.error(f"AI response is not valid JSON: {str(e)}")
@@ -1558,9 +1600,9 @@ class MethodOutlook(MethodBase):
# Ensure attachments are properly set from our processed list
if unique_attachments:
result_data["attachments"] = unique_attachments
- logger.info(f"Final email attachments: {unique_attachments}")
+
- logger.info(f"Email composition completed: {len(composition_documents)} documents used for context, {len(unique_attachments)} documents as attachments")
+
# Determine output format - ALWAYS use JSON for email composition
# This action must produce JSON for sendEmail to parse correctly
@@ -1568,12 +1610,7 @@ class MethodOutlook(MethodBase):
output_mime_type = "application/json"
# Ignore any expectedDocumentFormats - this action has a fixed output format
- if expectedDocumentFormats and len(expectedDocumentFormats) > 0:
- logger.info(f"Ignoring expected format '{expectedDocumentFormats[0].get('extension', 'unknown')}' - composeEmail always produces JSON")
-
- logger.info(f"composeEmail action always produces: {output_extension} ({output_mime_type})")
-
-
+ # This action always produces JSON format
return ActionResult(
success=True,
diff --git a/modules/routes/routeSecurityGoogle.py b/modules/routes/routeSecurityGoogle.py
index 17f5a994..6e601e2b 100644
--- a/modules/routes/routeSecurityGoogle.py
+++ b/modules/routes/routeSecurityGoogle.py
@@ -166,7 +166,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
"expires_in": token_data.get("expires_in", 0)
}
- logger.info("Successfully got token using OAuth2Session")
+
if not token_response.get("access_token"):
logger.error("Token acquisition failed: No access token received")
@@ -498,7 +498,7 @@ async def refresh_token(
detail="No Google token found for this connection"
)
- logger.debug(f"Found Google token: expires={current_token.expiresAt}, refresh_token_exists={bool(current_token.tokenRefresh)}")
+
# Always attempt refresh (as per your requirement)
from modules.security.tokenManager import TokenManager
diff --git a/modules/routes/routeSecurityMsft.py b/modules/routes/routeSecurityMsft.py
index 2154699f..62012d58 100644
--- a/modules/routes/routeSecurityMsft.py
+++ b/modules/routes/routeSecurityMsft.py
@@ -318,7 +318,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
rootInterface.db.clearTableCache("connections")
# Save token
- logger.info(f"Creating token for connection {connection_id}")
+
token = Token(
userId=user.id, # Use local user's ID
authority=AuthAuthority.MSFT,
@@ -330,9 +330,9 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
createdAt=datetime.now()
)
- logger.info(f"Saving token with connectionId: {token.connectionId}")
+
interface.saveToken(token)
- logger.info(f"Token saved successfully for connection {connection_id}")
+
# Return success page with connection data
return HTMLResponse(
@@ -484,7 +484,7 @@ async def refresh_token(
detail="No Microsoft token found for this connection"
)
- logger.debug(f"Found Microsoft token: expires={current_token.expiresAt}, refresh_token_exists={bool(current_token.tokenRefresh)}")
+
# Always attempt refresh (as per your requirement)
from modules.security.tokenManager import TokenManager
diff --git a/modules/security/tokenManager.py b/modules/security/tokenManager.py
index 09ac836e..3b0d12b9 100644
--- a/modules/security/tokenManager.py
+++ b/modules/security/tokenManager.py
@@ -64,7 +64,7 @@ class TokenManager:
createdAt=datetime.now()
)
- logger.info(f"Successfully refreshed Microsoft token for user {user_id}")
+
return new_token
else:
logger.error(f"Failed to refresh Microsoft token: {response.status_code} - {response.text}")
@@ -112,7 +112,7 @@ class TokenManager:
createdAt=datetime.now()
)
- logger.info(f"Successfully refreshed Google token for user {user_id}")
+
return new_token
else:
logger.error(f"Failed to refresh Google token: {response.status_code} - {response.text}")
diff --git a/test_outlook_filters.py b/test_outlook_filters.py
new file mode 100644
index 00000000..4a7c0a8b
--- /dev/null
+++ b/test_outlook_filters.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+"""
+Test script for Outlook filter logic
+"""
+
+def test_build_graph_filter():
+ """Test the filter building logic"""
+
+ # Mock the _buildGraphFilter method
+ def _buildGraphFilter(filter_text):
+ if not filter_text:
+ return {}
+
+ filter_text = filter_text.strip()
+
+ # Handle email address filters
+ if '@' in filter_text and '.' in filter_text and ' ' not in filter_text:
+ return {"$filter": f"from/fromAddress/address eq '{filter_text}'"}
+
+ # Handle search queries (from:, to:, subject:, etc.)
+ if any(filter_text.startswith(prefix) for prefix in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']):
+ return {"$search": f'"{filter_text}"'}
+
+ # Handle text content - search in subject
+ return {"$filter": f"contains(subject,'{filter_text}')"}
+
+ # Test cases
+ test_cases = [
+ ("peter.muster@domain.com", {"$filter": "from/fromAddress/address eq 'peter.muster@domain.com'"}),
+ ("from:user@example.com", {"$search": '"from:user@example.com"'}),
+ ("subject:meeting", {"$search": '"subject:meeting"'}),
+ ("project update", {"$filter": "contains(subject,'project update')"}),
+ ("", {}),
+ (" hello world ", {"$filter": "contains(subject,'hello world')"}),
+ ]
+
+ print("Testing Outlook filter logic:")
+ print("=" * 50)
+
+ for test_input, expected_output in test_cases:
+ result = _buildGraphFilter(test_input)
+ status = "✓ PASS" if result == expected_output else "✗ FAIL"
+ print(f"{status} | Input: '{test_input}'")
+ print(f" | Expected: {expected_output}")
+ print(f" | Got: {result}")
+ print()
+
+ print("Test completed!")
+
+if __name__ == "__main__":
+ test_build_graph_filter()
diff --git a/test_outlook_filters_fixed.py b/test_outlook_filters_fixed.py
new file mode 100644
index 00000000..ef9663f3
--- /dev/null
+++ b/test_outlook_filters_fixed.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+"""
+Test script for fixed Outlook filter logic
+"""
+
+def test_build_graph_filter():
+ """Test the corrected filter building logic"""
+
+ # Mock the corrected _buildGraphFilter method
+ def _buildGraphFilter(filter_text):
+ if not filter_text:
+ return {}
+
+ filter_text = filter_text.strip()
+
+ # Handle search queries (from:, to:, subject:, etc.) - check this FIRST
+ if any(filter_text.startswith(prefix) for prefix in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']):
+ return {"$search": f'"{filter_text}"'}
+
+ # Handle email address filters (only if it's NOT a search query)
+ if '@' in filter_text and '.' in filter_text and ' ' not in filter_text and not filter_text.startswith('from:'):
+ return {"$filter": f"from/fromAddress/address eq '{filter_text}'"}
+
+ # Handle text content - search in subject
+ return {"$filter": f"contains(subject,'{filter_text}')"}
+
+ # Test cases
+ test_cases = [
+ ("peter.muster@domain.com", {"$filter": "from/fromAddress/address eq 'peter.muster@domain.com'"}),
+ ("from:user@example.com", {"$search": '"from:user@example.com"'}),
+ ("subject:meeting", {"$search": '"subject:meeting"'}),
+ ("project update", {"$filter": "contains(subject,'project update')"}),
+ ("", {}),
+ (" hello world ", {"$filter": "contains(subject,'hello world')"}),
+ # Additional edge cases
+ ("to:manager@company.com", {"$search": '"to:manager@company.com"'}),
+ ("received:today", {"$search": '"received:today"'}),
+ ("hasattachment:true", {"$search": '"hasattachment:true"'}),
+ ("user@domain.com", {"$filter": "from/fromAddress/address eq 'user@domain.com'"}),
+ ("from:user@domain.com subject:budget", {"$search": '"from:user@domain.com subject:budget"'}),
+ ]
+
+ print("Testing FIXED Outlook filter logic:")
+ print("=" * 50)
+
+ passed = 0
+ failed = 0
+
+ for test_input, expected_output in test_cases:
+ result = _buildGraphFilter(test_input)
+ status = "✓ PASS" if result == expected_output else "✗ FAIL"
+ if result == expected_output:
+ passed += 1
+ else:
+ failed += 1
+
+ print(f"{status} | Input: '{test_input}'")
+ print(f" | Expected: {expected_output}")
+ print(f" | Got: {result}")
+ print()
+
+ print(f"Test completed! {passed} passed, {failed} failed")
+
+ if failed == 0:
+ print("🎉 All tests passed!")
+ else:
+ print("❌ Some tests failed. Please check the logic.")
+
+if __name__ == "__main__":
+ test_build_graph_filter()