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