auth scopes
This commit is contained in:
parent
d642fa0f07
commit
2d269137a5
3 changed files with 574 additions and 42 deletions
|
|
@ -643,9 +643,16 @@ class MethodDocument(MethodBase):
|
|||
|
||||
# Clean up the AI response and ensure it's valid HTML
|
||||
if not aiReport.strip().startswith('<html'):
|
||||
# Check if AI response already contains a title/header
|
||||
has_title = any(title.lower() in aiReport.lower() for title in [title, "outlook", "report", "status"])
|
||||
|
||||
# Wrap the AI content in proper HTML structure
|
||||
html = ["<html><head><meta charset='utf-8'><title>" + title + "</title></head><body>"]
|
||||
html.append(f"<h1>{title}</h1>")
|
||||
|
||||
# Only add the title if the AI response doesn't already have one
|
||||
if not has_title:
|
||||
html.append(f"<h1>{title}</h1>")
|
||||
|
||||
html.append(f"<p><b>Generated:</b> {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}</p>")
|
||||
html.append(f"<p><b>Total Documents Analyzed:</b> {len(validDocuments)}</p>")
|
||||
html.append("<hr>")
|
||||
|
|
@ -666,7 +673,25 @@ class MethodDocument(MethodBase):
|
|||
Generate a basic HTML report as fallback when AI generation fails.
|
||||
"""
|
||||
html = ["<html><head><meta charset='utf-8'><title>" + title + "</title></head><body>"]
|
||||
html.append(f"<h1>{title}</h1>")
|
||||
|
||||
# Check if any document content already contains a title/header
|
||||
has_title = False
|
||||
for doc in chatDocuments:
|
||||
if hasattr(doc, 'fileId') and doc.fileId:
|
||||
try:
|
||||
file_data = self.service.getFileData(doc.fileId)
|
||||
if file_data:
|
||||
content = file_data.decode('utf-8') if isinstance(file_data, bytes) else str(file_data)
|
||||
if any(title.lower() in content.lower() for title in [title, "outlook", "report", "status"]):
|
||||
has_title = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Only add the title if no document content already has one
|
||||
if not has_title:
|
||||
html.append(f"<h1>{title}</h1>")
|
||||
|
||||
html.append(f"<p><b>Generated:</b> {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}</p>")
|
||||
html.append(f"<p><b>Total Documents:</b> {len(chatDocuments)}</p>")
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import json
|
|||
import uuid
|
||||
|
||||
from modules.chat.methodBase import MethodBase, ActionResult, action
|
||||
from modules.interfaces.interfaceAppModel import ConnectionStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -23,35 +24,31 @@ class MethodOutlook(MethodBase):
|
|||
self.description = "Handle Microsoft Outlook email operations"
|
||||
|
||||
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get Microsoft connection from connection reference"""
|
||||
"""
|
||||
Helper function to get Microsoft connection details.
|
||||
"""
|
||||
try:
|
||||
userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
|
||||
# Get the connection from the service
|
||||
userConnection = self.service.getUserConnection(connectionReference)
|
||||
if not userConnection:
|
||||
logger.warning(f"No user connection found for reference: {connectionReference}")
|
||||
return None
|
||||
|
||||
if userConnection.authority.value != "msft":
|
||||
logger.warning(f"Connection {userConnection.id} is not Microsoft (authority: {userConnection.authority.value})")
|
||||
logger.error(f"Connection not found: {connectionReference}")
|
||||
return None
|
||||
|
||||
# Check if connection is active or pending (pending means OAuth in progress)
|
||||
if userConnection.status.value not in ["active", "pending"]:
|
||||
logger.warning(f"Connection {userConnection.id} status is not active/pending: {userConnection.status.value}")
|
||||
return None
|
||||
|
||||
# Get the corresponding token for this user and authority
|
||||
token = self.service.interfaceApp.getToken(userConnection.authority.value)
|
||||
# Get the token for this connection
|
||||
token = self.service.getTokenForConnection(userConnection.id)
|
||||
if not token:
|
||||
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority.value}")
|
||||
logger.error(f"Token not found for connection: {userConnection.id}")
|
||||
return None
|
||||
|
||||
# Check if token is expired
|
||||
if hasattr(token, 'expiresAt') and token.expiresAt:
|
||||
import time
|
||||
current_time = time.time()
|
||||
if current_time > token.expiresAt:
|
||||
logger.warning(f"Token for connection {userConnection.id} is expired (expiresAt: {token.expiresAt}, current: {current_time})")
|
||||
return None
|
||||
# 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}")
|
||||
|
||||
|
|
@ -59,11 +56,43 @@ class MethodOutlook(MethodBase):
|
|||
"id": userConnection.id,
|
||||
"accessToken": token.tokenAccess,
|
||||
"refreshToken": token.tokenRefresh,
|
||||
"scopes": ["Mail.ReadWrite", "User.Read"] # Default Microsoft scopes
|
||||
"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:
|
||||
"""
|
||||
|
|
@ -104,14 +133,15 @@ class MethodOutlook(MethodBase):
|
|||
while avoiding conflicts between $search and $filter parameters.
|
||||
"""
|
||||
params = {
|
||||
"$top": limit,
|
||||
"$orderby": "receivedDateTime desc"
|
||||
"$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)
|
||||
|
|
@ -123,22 +153,28 @@ class MethodOutlook(MethodBase):
|
|||
params["$search"] = f'"{clean_query}"'
|
||||
logger.info(f"Using advanced search query: {clean_query}")
|
||||
|
||||
# Note: When using $search, we cannot combine it with $filter for folder
|
||||
# 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 in subject and body
|
||||
# Microsoft Graph API supports contains() for text search
|
||||
filter_parts = [f"contains(subject,'{clean_query}') or contains(body/content,'{clean_query}')"]
|
||||
# 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":
|
||||
filter_parts.append(f"parentFolderId eq '{folder}'")
|
||||
params["$filter"] = f"{params['$filter']} and parentFolderId eq '{folder}'"
|
||||
|
||||
# Combine all filter parts
|
||||
params["$filter"] = " and ".join(f"({part})" for part in filter_parts)
|
||||
logger.info(f"Using basic text search filter: {clean_query}")
|
||||
# Add orderby for basic queries
|
||||
params["$orderby"] = "receivedDateTime desc"
|
||||
logger.info(f"Using simple text search filter: {clean_query}")
|
||||
|
||||
return params
|
||||
|
||||
|
|
@ -163,11 +199,36 @@ class MethodOutlook(MethodBase):
|
|||
|
||||
if response.status_code == 200:
|
||||
folders_data = response.json()
|
||||
for folder in folders_data.get("value", []):
|
||||
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")
|
||||
|
||||
logger.warning(f"Folder '{folder_name}' not found")
|
||||
# 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}")
|
||||
|
|
@ -372,7 +433,16 @@ class MethodOutlook(MethodBase):
|
|||
return self._createResult(
|
||||
success=False,
|
||||
data={},
|
||||
error="No valid Microsoft connection found for the provided connection reference"
|
||||
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
|
||||
|
|
@ -428,10 +498,24 @@ class MethodOutlook(MethodBase):
|
|||
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"
|
||||
|
|
@ -454,12 +538,25 @@ class MethodOutlook(MethodBase):
|
|||
"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
|
||||
|
|
@ -953,6 +1050,410 @@ class MethodOutlook(MethodBase):
|
|||
|
||||
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={},
|
||||
|
|
|
|||
|
|
@ -39,7 +39,12 @@ CLIENT_SECRET = APP_CONFIG.get("Service_MSFT_CLIENT_SECRET")
|
|||
TENANT_ID = APP_CONFIG.get("Service_MSFT_TENANT_ID", "common")
|
||||
REDIRECT_URI = APP_CONFIG.get("Service_MSFT_REDIRECT_URI")
|
||||
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
|
||||
SCOPES = ["Mail.ReadWrite", "User.Read"]
|
||||
SCOPES = [
|
||||
"Mail.ReadWrite", # Read and write mail
|
||||
"Mail.Send", # Send mail
|
||||
"Mail.ReadWrite.Shared", # Access shared mailboxes
|
||||
"User.Read" # Read user profile
|
||||
]
|
||||
|
||||
@router.get("/login")
|
||||
@limiter.limit("5/minute")
|
||||
|
|
@ -69,8 +74,9 @@ async def login(
|
|||
"connectionId": connectionId
|
||||
})
|
||||
|
||||
# MSAL automatically adds openid, profile, offline_access - we just need to provide our business scopes
|
||||
auth_url = msal_app.get_authorization_request_url(
|
||||
scopes=SCOPES,
|
||||
scopes=SCOPES, # Only our business scopes - MSAL adds the required ones automatically
|
||||
redirect_uri=REDIRECT_URI,
|
||||
state=state_param,
|
||||
prompt="select_account" # Force account selection screen
|
||||
|
|
@ -104,10 +110,10 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
|
|||
client_credential=CLIENT_SECRET
|
||||
)
|
||||
|
||||
# Get token from code
|
||||
# Get token from code - MSAL automatically handles the required scopes
|
||||
token_response = msal_app.acquire_token_by_authorization_code(
|
||||
code,
|
||||
scopes=SCOPES,
|
||||
scopes=SCOPES, # Only our business scopes - MSAL adds the required ones automatically
|
||||
redirect_uri=REDIRECT_URI
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue