doc and mail final r1

This commit is contained in:
ValueOn AG 2025-08-20 01:04:53 +02:00
parent db13db0f83
commit f1e06553eb
8 changed files with 279 additions and 121 deletions

View file

@ -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}")

View file

@ -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

View file

@ -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('<html>') or cleaned_body.startswith('<body>') or '<br>' 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 <br> tags and wrap in proper HTML structure
html_body = cleaned_body.replace('\n', '<br>')
html_body = f"<html><body>{html_body}</body></html>"
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,

View file

@ -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

View file

@ -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

View file

@ -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}")

51
test_outlook_filters.py Normal file
View file

@ -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()

View file

@ -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()