""" Outlook method module. Handles Outlook operations using the Outlook service. """ import logging from typing import Dict, Any, List, Optional from datetime import datetime, UTC import json import uuid from modules.chat.methodBase import MethodBase, ActionResult, action from modules.interfaces.interfaceAppModel import ConnectionStatus logger = logging.getLogger(__name__) class MethodOutlook(MethodBase): """Outlook method implementation for email operations""" def __init__(self, serviceCenter: Any): """Initialize the Outlook method""" super().__init__(serviceCenter) self.name = "outlook" self.description = "Handle Microsoft Outlook email operations" def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]: """ Helper function to get Microsoft connection details. """ try: # Get the connection from the service userConnection = self.service.getUserConnection(connectionReference) if not userConnection: logger.error(f"Connection not found: {connectionReference}") return None # Get the token for this connection token = self.service.getTokenForConnection(userConnection.id) if not token: logger.error(f"Token not found for connection: {userConnection.id}") return None # Check if token is valid if not token.isValid(): logger.error(f"Token is invalid for connection: {userConnection.id}") return None # Check if connection is active if userConnection.status != ConnectionStatus.active: 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, "accessToken": token.tokenAccess, "refreshToken": token.tokenRefresh, "scopes": ["Mail.ReadWrite", "Mail.Send", "Mail.ReadWrite.Shared", "User.Read"] # Valid Microsoft Graph API scopes } except Exception as e: logger.error(f"Error getting Microsoft connection: {str(e)}") return None async def _checkPermissions(self, connection: Dict[str, Any]) -> bool: """ Check if the current connection has the necessary permissions for Outlook operations. """ try: import requests graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Test permissions by trying to access the user's mail folder test_url = f"{graph_url}/me/mailFolders" 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") logger.error("Required scopes: Mail.ReadWrite, Mail.Send, Mail.ReadWrite.Shared") return False else: logger.warning(f"⚠️ Permission check returned status {response.status_code}") return False except Exception as e: logger.error(f"Error checking permissions: {str(e)}") return False def _sanitizeSearchQuery(self, query: str) -> str: """ Sanitize and validate search query for Microsoft Graph API Microsoft Graph API has specific requirements for search queries: - Escape special characters properly - Handle search operators correctly - Ensure query format is valid """ if not query: return "" # Clean the query clean_query = query.strip() # Remove any double quotes that might cause issues clean_query = clean_query.replace('"', '') # Handle common search operators if any(op in clean_query.lower() for op in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']): # This is an advanced search query, return as-is return clean_query # For basic text search, ensure it's safe for contains() filter # Remove any characters that might break the OData filter syntax import re # Remove or escape characters that could break OData filter syntax safe_query = re.sub(r'[\\\'"]', '', clean_query) return safe_query def _buildSearchParameters(self, query: str, folder: str, limit: int) -> Dict[str, Any]: """ Build search parameters for Microsoft Graph API This method handles the complexity of building search parameters while avoiding conflicts between $search and $filter parameters. """ params = { "$top": limit } if not query or not query.strip(): # No query specified, just get emails from folder if folder and folder.lower() != "all": params["$filter"] = f"parentFolderId eq '{folder}'" # Add orderby for basic queries params["$orderby"] = "receivedDateTime desc" return params clean_query = self._sanitizeSearchQuery(query) # Check if this is a complex search query with multiple operators if any(op in clean_query.lower() for op in ['from:', 'to:', 'subject:', 'received:', 'hasattachment:']): # This is an advanced search query, use $search # Microsoft Graph API supports complex search syntax params["$search"] = f'"{clean_query}"' logger.info(f"Using advanced search query: {clean_query}") # Note: When using $search, we cannot combine it with $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") 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}')" # Add folder filter if specified if folder and folder.lower() != "all": params["$filter"] = f"{params['$filter']} and parentFolderId eq '{folder}'" # Add orderby for basic queries params["$orderby"] = "receivedDateTime desc" logger.info(f"Using simple text search filter: {clean_query}") return params def _getFolderId(self, folder_name: str, connection: Dict[str, Any]) -> Optional[str]: """ Get the folder ID for a given folder name This is needed for proper filtering when using advanced search queries """ try: import requests graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Get mail folders api_url = f"{graph_url}/me/mailFolders" response = requests.get(api_url, headers=headers) if response.status_code == 200: folders_data = response.json() 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 if folder_name.lower() == "drafts": draft_variations = ["drafts", "draft", "entwürfe", "entwurf", "brouillons", "brouillon"] 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 if folder_name.lower() == "sent items": sent_variations = ["sent items", "sent", "gesendete elemente", "éléments envoyés"] 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]}") return None else: logger.warning(f"Could not retrieve folders: {response.status_code}") return None except Exception as e: logger.warning(f"Error getting folder ID for '{folder_name}': {str(e)}") return None @action async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult: """ Read emails from Outlook Parameters: 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 expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description """ try: connectionReference = parameters.get("connectionReference") folder = parameters.get("folder", "Inbox") limit = parameters.get("limit", 10) filter = parameters.get("filter") expectedDocumentFormats = parameters.get("expectedDocumentFormats", []) if not connectionReference: return self._createResult( success=False, data={}, error="Connection reference is required" ) # Get Microsoft connection connection = self._getMicrosoftConnection(connectionReference) if not connection: return self._createResult( success=False, data={}, error="No valid Microsoft connection found for the provided connection reference" ) # Read emails using Microsoft Graph API try: import requests # Microsoft Graph API endpoint for messages graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Build the API request api_url = f"{graph_url}/me/mailFolders/{folder}/messages" params = { "$top": limit, "$orderby": "receivedDateTime desc" } if filter: params["$filter"] = filter # Make the API call response = requests.get(api_url, headers=headers, params=params) response.raise_for_status() emails_data = response.json() email_data = { "emails": emails_data.get("value", []), "count": len(emails_data.get("value", [])), "folder": folder, "filter": filter, "apiResponse": emails_data } logger.info(f"Successfully retrieved {len(emails_data.get('value', []))} emails from {folder}") except ImportError: logger.error("requests module not available, falling back to simulation") # Fallback to simulation if requests module is not available email_prompt = f""" Simulate reading emails from Microsoft Outlook. Connection: {connection['id']} Folder: {folder} Limit: {limit} Filter: {filter or 'None'} Please provide: 1. List of emails with subject, sender, date, and content 2. Summary of email statistics 3. Important or urgent emails highlighted 4. Email categorization if possible """ email_data = await self.service.interfaceAiCalls.callAiTextAdvanced(email_prompt) except Exception as e: logger.error(f"Error reading emails from Microsoft Graph API: {str(e)}") # Fallback to simulation on API error email_prompt = f""" Simulate reading emails from Microsoft Outlook. Connection: {connection['id']} Folder: {folder} Limit: {limit} Filter: {filter or 'None'} Please provide: 1. List of emails with subject, sender, date, and content 2. Summary of email statistics 3. Important or urgent emails highlighted 4. Email categorization if possible """ email_data = await self.service.interfaceAiCalls.callAiTextAdvanced(email_prompt) # Create result data result_data = { "connectionReference": connectionReference, "folder": folder, "limit": limit, "filter": filter, "emails": email_data, "connection": { "id": connection["id"], "authority": "microsoft", "reference": connectionReference }, "timestamp": datetime.now(UTC).isoformat() } # Determine output format based on expected formats output_extension = ".json" # Default output_mime_type = "application/json" # Default if expectedDocumentFormats and len(expectedDocumentFormats) > 0: # Use the first expected format expected_format = expectedDocumentFormats[0] output_extension = expected_format.get("extension", ".json") output_mime_type = expected_format.get("mimeType", "application/json") logger.info(f"Using expected format: {output_extension} ({output_mime_type})") else: logger.info("No expected format specified, using default .json format") return self._createResult( success=True, data={ "documents": [ { "documentName": f"outlook_emails_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}", "documentData": result_data, "mimeType": output_mime_type } ] } ) except Exception as e: logger.error(f"Error reading emails: {str(e)}") return self._createResult( success=False, data={}, error=str(e) ) @action async def sendEmail(self, parameters: Dict[str, Any]) -> ActionResult: """ Create email draft in Outlook for sending out Parameters: connectionReference (str): Reference to the Microsoft connection to (List[str]): List of recipient email addresses subject (str): Email subject body (str): Email body content cc (List[str], optional): CC recipients bcc (List[str], optional): BCC recipients attachments (List[str], optional): List of document references to attach expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description """ try: connectionReference = parameters.get("connectionReference") to = parameters.get("to") subject = parameters.get("subject") body = parameters.get("body") cc = parameters.get("cc", []) bcc = parameters.get("bcc", []) attachments = parameters.get("attachments", []) expectedDocumentFormats = parameters.get("expectedDocumentFormats", []) if not connectionReference or not to or not subject or not body: return self._createResult( success=False, data={}, error="Connection reference, to, subject, and body are required" ) # Get Microsoft connection connection = self._getMicrosoftConnection(connectionReference) if not connection: return self._createResult( success=False, data={}, error="Failed to get Microsoft connection" ) # Check permissions before proceeding permissions_ok = await self._checkPermissions(connection) if not permissions_ok: return self._createResult( success=False, data={}, error="Connection lacks necessary permissions. Please re-authenticate with updated permissions." ) # Create email draft using Microsoft Graph API try: import requests # Microsoft Graph API endpoint for creating draft messages graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Build the email message message = { "subject": subject, "body": { "contentType": "HTML", "content": body }, "toRecipients": [{"emailAddress": {"address": email}} for email in to], "ccRecipients": [{"emailAddress": {"address": email}} for email in cc] if cc else [], "bccRecipients": [{"emailAddress": {"address": email}} for email in bcc] if bcc else [] } # Add attachments if provided if attachments: message["attachments"] = [] for attachment_ref in attachments: # Get attachment document from service center attachment_docs = self.service.getChatDocumentsFromDocumentList([attachment_ref]) if attachment_docs: for doc in attachment_docs: # Create attachment object for Graph API attachment = { "@odata.type": "#microsoft.graph.fileAttachment", "name": doc.filename, "contentType": doc.mimeType, "contentBytes": doc.data if hasattr(doc, 'data') else "" } message["attachments"].append(attachment) # Create the draft message # First, get the Drafts folder ID to ensure the draft is created there drafts_folder_id = self._getFolderId("Drafts", connection) if drafts_folder_id: # Create draft in the Drafts folder specifically api_url = f"{graph_url}/me/mailFolders/{drafts_folder_id}/messages" logger.info(f"Creating draft in Drafts folder (ID: {drafts_folder_id})") else: # Fallback: create in default location api_url = f"{graph_url}/me/messages" logger.warning("Could not find Drafts folder, creating draft in default location") logger.info(f"Creating draft with API URL: {api_url}") logger.info(f"Draft message data: {json.dumps(message, indent=2)}") response = requests.post(api_url, headers=headers, json=message) response.raise_for_status() draft_data = response.json() logger.info(f"Draft creation response: {json.dumps(draft_data, indent=2)}") # Verify the draft was created in the correct folder created_folder_id = draft_data.get("parentFolderId") if created_folder_id: if drafts_folder_id and created_folder_id == drafts_folder_id: logger.info(f"✅ Draft successfully created in Drafts folder (ID: {created_folder_id})") else: logger.warning(f"⚠️ Draft created in different folder than expected. Expected: {drafts_folder_id}, Actual: {created_folder_id}") else: logger.warning("⚠️ Draft created but no folder ID returned") # Get the actual folder information for the created draft actual_folder = "Drafts" if drafts_folder_id: actual_folder = "Drafts" else: # Try to determine where the draft was actually created if "parentFolderId" in draft_data: actual_folder = f"Folder ID: {draft_data['parentFolderId']}" else: actual_folder = "Default location" draft_result = { "status": "draft_created", "messageId": draft_data.get("id", "unknown"), "draftId": draft_data.get("id", "unknown"), "recipients": to, "cc": cc, "bcc": bcc, "attachments": len(attachments) if attachments else 0, "draftLocation": actual_folder, "draftsFolderId": drafts_folder_id, "createdFolderId": created_folder_id, "apiResponse": response.status_code, "draftData": draft_data } logger.info(f"Successfully created email draft for {len(to)} recipients with {len(attachments) if attachments else 0} attachments in {actual_folder}") # Additional verification: try to retrieve the draft to confirm it exists try: verify_url = f"{graph_url}/me/messages/{draft_data.get('id')}" verify_response = requests.get(verify_url, headers=headers) if verify_response.status_code == 200: verify_data = verify_response.json() logger.info(f"✅ Draft verification successful - Draft ID: {verify_data.get('id')}, Subject: {verify_data.get('subject')}") else: logger.warning(f"⚠️ Draft verification failed - Status: {verify_response.status_code}") except Exception as e: logger.warning(f"⚠️ Draft verification error: {str(e)}") except ImportError: logger.error("requests module not available, falling back to simulation") # Fallback to simulation if requests module is not available send_prompt = f""" Simulate creating an email draft in Microsoft Outlook. Connection: {connection['id']} To: {to} Subject: {subject} Body: {body} CC: {cc} BCC: {bcc} Attachments: {attachments if attachments else 'None'} Please provide: 1. Email composition details 2. Validation of email addresses 3. Email formatting and structure 4. Attachment processing and validation 5. Draft creation confirmation """ draft_result = await self.service.interfaceAiCalls.callAiTextAdvanced(send_prompt) except Exception as e: logger.error(f"Error creating email draft via Microsoft Graph API: {str(e)}") # Fallback to simulation on API error send_prompt = f""" Simulate creating an email draft in Microsoft Outlook. Connection: {connection['id']} To: {to} Subject: {subject} Body: {body} CC: {cc} BCC: {bcc} Attachments: {attachments if attachments else 'None'} Please provide: 1. Email composition details 2. Validation of email addresses 3. Email formatting and structure 4. Attachment processing and validation 5. Draft creation confirmation """ draft_result = await self.service.interfaceAiCalls.callAiTextAdvanced(send_prompt) # Create result data result_data = { "connectionReference": connectionReference, "to": to, "subject": subject, "body": body, "cc": cc, "bcc": bcc, "attachments": attachments, "draftResult": draft_result, "connection": { "id": connection["id"], "authority": "microsoft", "reference": connectionReference }, "timestamp": datetime.now(UTC).isoformat() } # Determine output format based on expected formats output_extension = ".json" # Default output_mime_type = "application/json" # Default if expectedDocumentFormats and len(expectedDocumentFormats) > 0: # Use the first expected format expected_format = expectedDocumentFormats[0] output_extension = expected_format.get("extension", ".json") output_mime_type = expected_format.get("mimeType", "application/json") logger.info(f"Using expected format: {output_extension} ({output_mime_type})") else: logger.info("No expected format specified, using default .json format") return self._createResult( success=True, data={ "documents": [ { "documentName": f"outlook_email_draft_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}", "documentData": result_data, "mimeType": output_mime_type } ] } ) except Exception as e: logger.error(f"Error creating email draft: {str(e)}") return self._createResult( success=False, data={}, error=str(e) ) @action async def searchEmails(self, parameters: Dict[str, Any]) -> ActionResult: """ Search emails in Outlook Parameters: connectionReference (str): Reference to the Microsoft connection query (str): Search query folder (str, optional): Folder to search in (default: "All") limit (int, optional): Maximum number of results (default: 20) expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description """ try: connectionReference = parameters.get("connectionReference") query = parameters.get("query") folder = parameters.get("folder", "All") limit = parameters.get("limit", 20) expectedDocumentFormats = parameters.get("expectedDocumentFormats", []) # Validate parameters if not connectionReference: return self._createResult( success=False, data={}, error="Connection reference is required" ) if not query or not query.strip(): return self._createResult( success=False, data={}, error="Search query is required and cannot be empty" ) # Validate limit try: limit = int(limit) if limit <= 0 or limit > 1000: # Microsoft Graph API has limits limit = 20 logger.warning(f"Limit {limit} is out of range, using default value 20") except (ValueError, TypeError): limit = 20 logger.warning(f"Invalid limit value, using default value 20") # Get Microsoft connection connection = self._getMicrosoftConnection(connectionReference) if not connection: return self._createResult( success=False, data={}, error="No valid Microsoft connection found for the provided connection reference" ) # Search emails using Microsoft Graph API try: import requests # Microsoft Graph API endpoint for searching messages graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Build the search API request 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 try: error_data = response.json() logger.error(f"Microsoft Graph API error: {response.status_code} - {error_data}") except: logger.error(f"Microsoft Graph API error: {response.status_code} - {response.text}") # Check for specific error types and provide helpful messages if response.status_code == 400: logger.error("Bad Request (400) - Check search query format and parameters") logger.error(f"Search query: '{query}'") logger.error(f"Search parameters: {params}") logger.error(f"API URL: {api_url}") elif response.status_code == 401: logger.error("Unauthorized (401) - Check access token and permissions") elif response.status_code == 403: logger.error("Forbidden (403) - Check API permissions and scopes") elif response.status_code == 429: logger.error("Too Many Requests (429) - Rate limit exceeded") # Fall back to simulation on API error raise Exception(f"Microsoft Graph API returned {response.status_code}: {response.text}") response.raise_for_status() search_data = response.json() emails = search_data.get("value", []) logger.info(f"Successfully retrieved {len(emails)} emails from Microsoft Graph API") # Apply folder filtering if needed and we used $search if folder and folder.lower() != "all" and "$search" in params: # Get the actual folder ID for proper filtering folder_id = self._getFolderId(folder, connection) if folder_id: # Filter results by folder ID filtered_emails = [] for email in emails: if email.get("parentFolderId") == folder_id: filtered_emails.append(email) emails = filtered_emails logger.info(f"Filtered {len(filtered_emails)} emails for folder '{folder}' (ID: {folder_id}) from {len(search_data.get('value', []))} total results") else: # Fallback: try to filter by folder name (less reliable) filtered_emails = [] for email in emails: # Check if email has folder information if hasattr(email, 'parentFolderId') and email.get('parentFolderId'): if email.get('parentFolderId') == folder: filtered_emails.append(email) else: # If no folder info, include the email (less strict filtering) filtered_emails.append(email) logger.debug(f"Email {email.get('id', 'unknown')} has no folder info, including in results") emails = filtered_emails logger.info(f"Filtered {len(filtered_emails)} emails for folder '{folder}' (fallback filtering) from {len(search_data.get('value', []))} total results") search_result = { "query": query, "results": emails, "count": len(emails), "folder": folder, "limit": limit, "apiResponse": search_data, "searchParams": params } logger.info(f"Successfully searched emails with query '{query}', found {len(emails)} results") except ImportError: logger.error("requests module not available, falling back to simulation") # Fallback to simulation if requests module is not available search_prompt = f""" Simulate searching emails in Microsoft Outlook. Connection: {connection['id']} Query: {query} Folder: {folder} Limit: {limit} Please provide: 1. Search results with relevant emails 2. Search statistics and relevance scores 3. Email previews and key information 4. Search suggestions and refinements """ search_result = await self.service.interfaceAiCalls.callAiTextAdvanced(search_prompt) except Exception as e: logger.error(f"Error searching emails via Microsoft Graph API: {str(e)}") # Fallback to simulation on API error search_prompt = f""" Simulate searching emails in Microsoft Outlook. Connection: {connection['id']} Query: {query} Folder: {folder} Limit: {limit} Please provide: 1. Search results with relevant emails 2. Search statistics and relevance scores 3. Email previews and key information 4. Search suggestions and refinements """ search_result = await self.service.interfaceAiCalls.callAiTextAdvanced(search_prompt) # Create result data result_data = { "connectionReference": connectionReference, "query": query, "folder": folder, "limit": limit, "searchResults": search_result, "connection": { "id": connection["id"], "authority": "microsoft", "reference": connectionReference }, "timestamp": datetime.now(UTC).isoformat() } # Determine output format based on expected formats output_extension = ".json" # Default output_mime_type = "application/json" # Default if expectedDocumentFormats and len(expectedDocumentFormats) > 0: # Use the first expected format expected_format = expectedDocumentFormats[0] output_extension = expected_format.get("extension", ".json") output_mime_type = expected_format.get("mimeType", "application/json") logger.info(f"Using expected format: {output_extension} ({output_mime_type})") else: logger.info("No expected format specified, using default .json format") return self._createResult( success=True, data={ "documents": [ { "documentName": f"outlook_email_search_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}", "documentData": result_data, "mimeType": output_mime_type } ] } ) except Exception as e: logger.error(f"Error searching emails: {str(e)}") return self._createResult( success=False, data={}, error=str(e) ) @action async def listDrafts(self, parameters: Dict[str, Any]) -> ActionResult: """ List email drafts in Outlook Parameters: connectionReference (str): Reference to the Microsoft connection folder (str, optional): Folder to search for drafts (default: "Drafts") limit (int, optional): Maximum number of drafts to list (default: 20) expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description """ try: connectionReference = parameters.get("connectionReference") folder = parameters.get("folder", "Drafts") limit = parameters.get("limit", 20) expectedDocumentFormats = parameters.get("expectedDocumentFormats", []) if not connectionReference: return self._createResult( success=False, data={}, error="Connection reference is required" ) # Get Microsoft connection connection = self._getMicrosoftConnection(connectionReference) if not connection: return self._createResult( success=False, data={}, error="No valid Microsoft connection found for the provided connection reference" ) # List drafts using Microsoft Graph API try: import requests # Microsoft Graph API endpoint for messages graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Get the folder ID for the specified folder folder_id = self._getFolderId(folder, connection) if folder_id: # List messages in the specific folder api_url = f"{graph_url}/me/mailFolders/{folder_id}/messages" logger.info(f"Listing messages in folder '{folder}' (ID: {folder_id})") else: # Fallback: list all messages (might include drafts) api_url = f"{graph_url}/me/messages" logger.warning(f"Could not find folder '{folder}', listing all messages") params = { "$top": limit, "$orderby": "lastModifiedDateTime desc", "$select": "id,subject,from,toRecipients,ccRecipients,bccRecipients,receivedDateTime,lastModifiedDateTime,parentFolderId,isDraft" } # Make the API call response = requests.get(api_url, headers=headers, params=params) response.raise_for_status() messages_data = response.json() messages = messages_data.get("value", []) # Filter for drafts if we're looking at all messages if not folder_id: drafts = [msg for msg in messages if msg.get("isDraft", False)] messages = drafts logger.info(f"Filtered {len(drafts)} drafts from {len(messages_data.get('value', []))} total messages") drafts_result = { "folder": folder, "folderId": folder_id, "drafts": messages, "count": len(messages), "limit": limit, "apiResponse": messages_data } logger.info(f"Successfully retrieved {len(messages)} drafts from folder '{folder}'") except ImportError: logger.error("requests module not available, falling back to simulation") # Fallback to simulation drafts_prompt = f""" Simulate listing email drafts in Microsoft Outlook. Connection: {connection['id']} Folder: {folder} Limit: {limit} Please provide: 1. List of email drafts with subject, recipients, and modification date 2. Draft status and location information 3. Summary of draft statistics """ drafts_result = await self.service.interfaceAiCalls.callAiTextAdvanced(drafts_prompt) except Exception as e: logger.error(f"Error listing drafts via Microsoft Graph API: {str(e)}") # Fallback to simulation on API error drafts_prompt = f""" Simulate listing email drafts in Microsoft Outlook. Connection: {connection['id']} Folder: {folder} Limit: {limit} Please provide: 1. List of email drafts with subject, recipients, and modification date 2. Draft status and location information 3. Summary of draft statistics """ drafts_result = await self.service.interfaceAiCalls.callAiTextAdvanced(drafts_prompt) # Create result data result_data = { "connectionReference": connectionReference, "folder": folder, "limit": limit, "draftsResult": drafts_result, "connection": { "id": connection["id"], "authority": "microsoft", "reference": connectionReference }, "timestamp": datetime.now(UTC).isoformat() } # Determine output format based on expected formats output_extension = ".json" # Default output_mime_type = "application/json" # Default if expectedDocumentFormats and len(expectedDocumentFormats) > 0: # Use the first expected format expected_format = expectedDocumentFormats[0] output_extension = expected_format.get("extension", ".json") output_mime_type = expected_format.get("mimeType", "application/json") logger.info(f"Using expected format: {output_extension} ({output_mime_type})") else: logger.info("No expected format specified, using default .json format") return self._createResult( success=True, data={ "documents": [ { "documentName": f"outlook_drafts_list_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}", "documentData": result_data, "mimeType": output_mime_type } ] } ) except Exception as e: logger.error(f"Error listing drafts: {str(e)}") return self._createResult( success=False, data={}, error=str(e) ) @action async def findDrafts(self, parameters: Dict[str, Any]) -> ActionResult: """ Find email drafts across all folders in Outlook Parameters: connectionReference (str): Reference to the Microsoft connection limit (int, optional): Maximum number of drafts to find (default: 50) expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description """ try: connectionReference = parameters.get("connectionReference") limit = parameters.get("limit", 50) expectedDocumentFormats = parameters.get("expectedDocumentFormats", []) if not connectionReference: return self._createResult( success=False, data={}, error="Connection reference is required" ) # Get Microsoft connection connection = self._getMicrosoftConnection(connectionReference) if not connection: return self._createResult( success=False, data={}, error="No valid Microsoft connection found for the provided connection reference" ) # Find drafts using Microsoft Graph API try: import requests # Microsoft Graph API endpoint for messages graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Get all messages and filter for drafts api_url = f"{graph_url}/me/messages" params = { "$top": limit, "$select": "id,subject,from,toRecipients,ccRecipients,bccRecipients,receivedDateTime,lastModifiedDateTime,parentFolderId,isDraft,webLink", "$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) response.raise_for_status() messages_data = response.json() drafts = messages_data.get("value", []) # Get folder information for each draft for draft in drafts: if "parentFolderId" in draft: folder_info = self._getFolderNameById(draft["parentFolderId"], connection) draft["folderName"] = folder_info drafts_result = { "totalDrafts": len(drafts), "drafts": drafts, "limit": limit, "apiResponse": messages_data } logger.info(f"Successfully found {len(drafts)} drafts across all folders") except ImportError: logger.error("requests module not available, falling back to simulation") # Fallback to simulation drafts_prompt = f""" Simulate finding email drafts in Microsoft Outlook. Connection: {connection['id']} Limit: {limit} Please provide: 1. List of email drafts with subject, recipients, and location 2. Draft status and folder information 3. Summary of draft statistics """ drafts_result = await self.service.interfaceAiCalls.callAiTextAdvanced(drafts_prompt) except Exception as e: logger.error(f"Error finding drafts via Microsoft Graph API: {str(e)}") # Fallback to simulation on API error drafts_prompt = f""" Simulate finding email drafts in Microsoft Outlook. Connection: {connection['id']} Limit: {limit} Please provide: 1. List of email drafts with subject, recipients, and location 2. Draft status and folder information 3. Summary of draft statistics """ drafts_result = await self.service.interfaceAiCalls.callAiTextAdvanced(drafts_prompt) # Create result data result_data = { "connectionReference": connectionReference, "limit": limit, "draftsResult": drafts_result, "connection": { "id": connection["id"], "authority": "microsoft", "reference": connectionReference }, "timestamp": datetime.now(UTC).isoformat() } # Determine output format based on expected formats output_extension = ".json" # Default output_mime_type = "application/json" # Default if expectedDocumentFormats and len(expectedDocumentFormats) > 0: # Use the first expected format expected_format = expectedDocumentFormats[0] output_extension = expected_format.get("extension", ".json") output_mime_type = expected_format.get("mimeType", "application/json") logger.info(f"Using expected format: {output_extension} ({output_mime_type})") else: logger.info("No expected format specified, using default .json format") return self._createResult( success=True, data={ "documents": [ { "documentName": f"outlook_drafts_found_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}", "documentData": result_data, "mimeType": output_mime_type } ] } ) except Exception as e: logger.error(f"Error finding drafts: {str(e)}") return self._createResult( success=False, data={}, error=str(e) ) def _getFolderNameById(self, folder_id: str, connection: Dict[str, Any]) -> str: """ Get folder name by folder ID This is a helper method to identify which folder a draft is in """ try: import requests graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Get folder information api_url = f"{graph_url}/me/mailFolders/{folder_id}" response = requests.get(api_url, headers=headers) if response.status_code == 200: folder_data = response.json() return folder_data.get("displayName", f"Unknown Folder ({folder_id})") else: return f"Unknown Folder ({folder_id})" except Exception as e: logger.warning(f"Error getting folder name for ID '{folder_id}': {str(e)}") return f"Unknown Folder ({folder_id})" @action async def checkDraftsFolder(self, parameters: Dict[str, Any]) -> ActionResult: """ Check the contents of the Drafts folder directly Parameters: connectionReference (str): Reference to the Microsoft connection limit (int, optional): Maximum number of drafts to check (default: 20) expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description """ try: connectionReference = parameters.get("connectionReference") limit = parameters.get("limit", 20) expectedDocumentFormats = parameters.get("expectedDocumentFormats", []) if not connectionReference: return self._createResult( success=False, data={}, error="Connection reference is required" ) # Get Microsoft connection connection = self._getMicrosoftConnection(connectionReference) if not connection: return self._createResult( success=False, data={}, error="No valid Microsoft connection found for the provided connection reference" ) # Check Drafts folder directly try: import requests # Microsoft Graph API endpoint for messages graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Get the Drafts folder ID drafts_folder_id = self._getFolderId("Drafts", connection) if not drafts_folder_id: return self._createResult( success=False, data={}, error="Could not find Drafts folder" ) # Get messages directly from Drafts folder api_url = f"{graph_url}/me/mailFolders/{drafts_folder_id}/messages" params = { "$top": limit, "$select": "id,subject,from,toRecipients,ccRecipients,bccRecipients,receivedDateTime,lastModifiedDateTime,isDraft,webLink", "$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) response.raise_for_status() 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, "totalDrafts": len(drafts), "drafts": drafts, "limit": limit, "apiResponse": messages_data, "apiUrl": api_url } logger.info(f"Successfully checked Drafts folder: found {len(drafts)} drafts") except ImportError: logger.error("requests module not available, falling back to simulation") # Fallback to simulation drafts_prompt = f""" Simulate checking Drafts folder in Microsoft Outlook. Connection: {connection['id']} Limit: {limit} Please provide: 1. List of email drafts in the Drafts folder 2. Draft details and status 3. Summary of draft contents """ drafts_result = await self.service.interfaceAiCalls.callAiTextAdvanced(drafts_prompt) except Exception as e: logger.error(f"Error checking Drafts folder via Microsoft Graph API: {str(e)}") # Fallback to simulation on API error drafts_prompt = f""" Simulate checking Drafts folder in Microsoft Outlook. Connection: {connection['id']} Limit: {limit} Please provide: 1. List of email drafts in the Drafts folder 2. Draft details and status 3. Summary of draft contents """ drafts_result = await self.service.interfaceAiCalls.callAiTextAdvanced(drafts_prompt) # Create result data result_data = { "connectionReference": connectionReference, "limit": limit, "draftsResult": drafts_result, "connection": { "id": connection["id"], "authority": "microsoft", "reference": connectionReference }, "timestamp": datetime.now(UTC).isoformat() } # Determine output format based on expected formats output_extension = ".json" # Default output_mime_type = "application/json" # Default if expectedDocumentFormats and len(expectedDocumentFormats) > 0: # Use the first expected format expected_format = expectedDocumentFormats[0] output_extension = expected_format.get("extension", ".json") output_mime_type = expected_format.get("mimeType", "application/json") logger.info(f"Using expected format: {output_extension} ({output_mime_type})") else: logger.info("No expected format specified, using default .json format") return self._createResult( success=True, data={ "documents": [ { "documentName": f"outlook_drafts_folder_check_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}", "documentData": result_data, "mimeType": output_mime_type } ] } ) except Exception as e: logger.error(f"Error checking Drafts folder: {str(e)}") return self._createResult( success=False, data={}, error=str(e) ) @action async def checkPermissions(self, parameters: Dict[str, Any]) -> ActionResult: """ Check if the current Microsoft connection has the necessary permissions for Outlook operations. Parameters: connectionReference (str): Reference to the Microsoft connection to check """ try: connectionReference = parameters.get("connectionReference") if not connectionReference: return self._createResult( success=False, data={}, error="Connection reference is required" ) # Get Microsoft connection connection = self._getMicrosoftConnection(connectionReference) if not connection: return self._createResult( success=False, data={}, error="Failed to get Microsoft connection" ) # Check permissions permissions_ok = await self._checkPermissions(connection) if permissions_ok: return self._createResult( success=True, data={ "permissions": "✅ All necessary permissions are available", "scopes": connection.get("scopes", []), "connectionId": connection.get("id"), "status": "ready" } ) else: return self._createResult( success=False, data={ "permissions": "❌ Missing necessary permissions", "requiredScopes": ["Mail.ReadWrite", "Mail.Send", "Mail.ReadWrite.Shared", "User.Read"], "currentScopes": connection.get("scopes", []), "connectionId": connection.get("id"), "status": "needs_reauthentication", "message": "Please re-authenticate your Microsoft connection to get updated permissions." }, error="Connection lacks necessary permissions for Outlook operations" ) except Exception as e: logger.error(f"Error checking permissions: {str(e)}") return self._createResult( success=False, data={}, error=str(e) )