diff --git a/camel/toolkits/microsoft_outlook_mail_toolkit.py b/camel/toolkits/microsoft_outlook_mail_toolkit.py index 1a8d24f69c..7545e578cd 100644 --- a/camel/toolkits/microsoft_outlook_mail_toolkit.py +++ b/camel/toolkits/microsoft_outlook_mail_toolkit.py @@ -12,9 +12,9 @@ # limitations under the License. # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. ========= -import asyncio import json import os +import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path @@ -64,8 +64,8 @@ def log_message(self, format, *args): pass -class AsyncCustomAzureCredential: - """Creates an async Azure credential to pass into MSGraph client. +class CustomAzureCredential: + """Creates a sync Azure credential to pass into MSGraph client. Implements Azure credential interface with automatic token refresh using a refresh token. Updates the refresh token file whenever Microsoft issues @@ -99,10 +99,10 @@ def __init__( self._access_token = None self._expires_at = 0 - self._lock = asyncio.Lock() + self._lock = threading.Lock() self._debug_claims_logged = False - async def _refresh_access_token(self): + def _refresh_access_token(self): """Refreshes the access token using the refresh token. Requests a new access token from Microsoft's token endpoint. @@ -112,8 +112,6 @@ async def _refresh_access_token(self): Raises: Exception: If token refresh fails or returns an error. """ - import httpx - token_url = ( f"https://login.microsoftonline.com/{self.tenant_id}" f"/oauth2/v2.0/token" @@ -126,9 +124,8 @@ async def _refresh_access_token(self): "scope": " ".join(self.scopes), } - async with httpx.AsyncClient(timeout=30) as client: - response = await client.post(token_url, data=data) - result = response.json() + response = requests.post(token_url, data=data, timeout=30) + result = response.json() # Raise exception if error in response if "error" in result: @@ -170,8 +167,8 @@ def _save_refresh_token(self, refresh_token: str): except Exception as e: logger.warning(f"Failed to save refresh token: {e!s}") - async def get_token(self, *args, **kwargs): - """Gets a valid AccessToken object for msgraph (async). + def get_token(self, *args, **kwargs): + """Gets a valid AccessToken object for msgraph (sync). Called by Microsoft Graph SDK when making API requests. Automatically refreshes the token if expired. @@ -188,9 +185,7 @@ async def get_token(self, *args, **kwargs): """ from azure.core.credentials import AccessToken - def _maybe_log_token_claims(token: Optional[str]) -> None: - if not token: - return + def _maybe_log_token_claims(token: str) -> None: if self._debug_claims_logged: return if os.getenv("CAMEL_OUTLOOK_DEBUG_TOKEN_CLAIMS") != "1": @@ -218,17 +213,14 @@ def _maybe_log_token_claims(token: Optional[str]) -> None: # Check if token needs refresh now = int(time.time()) if now >= self._expires_at: - async with self._lock: + with self._lock: # Double-check after lock (another thread may have refreshed) if now >= self._expires_at: - await self._refresh_access_token() + self._refresh_access_token() _maybe_log_token_claims(self._access_token) return AccessToken(self._access_token, self._expires_at) - async def close(self) -> None: - return None - @MCPServer() class OutlookMailToolkit(BaseToolkit): @@ -388,14 +380,14 @@ def _save_token_to_file(self, refresh_token: str): def _authenticate_using_refresh_token( self, - ) -> AsyncCustomAzureCredential: + ) -> CustomAzureCredential: """Authenticates using a saved refresh token. Loads the refresh token from disk and creates a credential object that will automatically refresh access tokens as needed. Returns: - _RefreshableCredential: Credential with auto-refresh capability. + CustomAzureCredential: Credential with auto-refresh capability. Raises: ValueError: If refresh token cannot be loaded or is invalid. @@ -406,7 +398,7 @@ def _authenticate_using_refresh_token( raise ValueError("No valid refresh token found in file") # Create credential with automatic refresh capability - credentials = AsyncCustomAzureCredential( + credentials = CustomAzureCredential( client_id=self.client_id, client_secret=self.client_secret, tenant_id=self.tenant_id, @@ -425,13 +417,13 @@ def _authenticate_using_browser(self): code for tokens, and saves refresh token for future use. Returns: - AsyncCustomAzureCredential or AuthorizationCodeCredential : + CustomAzureCredential or AuthorizationCodeCredential : Credential for Microsoft Graph API. Raises: ValueError: If authentication fails or no authorization code. """ - from azure.identity.aio import AuthorizationCodeCredential + from azure.identity import AuthorizationCodeCredential # offline_access scope is needed so the azure credential can refresh # internally after access token expires as azure handles it internally @@ -455,7 +447,7 @@ def _authenticate_using_browser(self): refresh_token = token_result.get("refresh_token") if refresh_token: self._save_token_to_file(refresh_token) - credentials = AsyncCustomAzureCredential( + credentials = CustomAzureCredential( client_id=self.client_id, client_secret=self.client_secret, tenant_id=self.tenant_id, @@ -578,12 +570,12 @@ def _authenticate(self): 2. Falls back to browser OAuth if no token or token invalid Returns: - AuthorizationCodeCredential or AsyncCustomAzureCredential + AuthorizationCodeCredential or CustomAzureCredential Raises: ValueError: If authentication fails through both methods. """ - from azure.identity.aio import AuthorizationCodeCredential + from azure.identity import AuthorizationCodeCredential try: self.tenant_id = os.getenv("MICROSOFT_TENANT_ID", "common") @@ -596,7 +588,7 @@ def _authenticate(self): and self.refresh_token_file_path.exists() ): try: - credentials: AsyncCustomAzureCredential = ( + credentials: CustomAzureCredential = ( self._authenticate_using_refresh_token() ) return credentials @@ -634,8 +626,7 @@ def _get_graph_client(self, credentials, scopes): from msgraph import GraphServiceClient try: - client = GraphServiceClient(credentials=credentials, scopes=scopes) - return client + return GraphServiceClient(credentials=credentials, scopes=scopes) except Exception as e: error_msg = f"Failed to create Graph client: {e!s}" logger.error(error_msg) @@ -708,11 +699,10 @@ def _create_attachments(self, file_paths: List[str]) -> List[Any]: file_name = os.path.basename(file_path) - # Create attachment with proper properties - attachment_obj = FileAttachment() - attachment_obj.odata_type = "#microsoft.graph.fileAttachment" - attachment_obj.name = file_name - attachment_obj.content_bytes = file_content + attachment_obj = FileAttachment( + name=file_name, + content_bytes=file_content, + ) attachment_list.append(attachment_obj) @@ -721,14 +711,14 @@ def _create_attachments(self, file_paths: List[str]) -> List[Any]: return attachment_list - def _create_recipients(self, email_list: Optional[List[str]]) -> List[Any]: + def _create_recipients(self, email_list: List[str]) -> List[Any]: """Creates Microsoft Graph Recipient objects from email addresses. Supports both simple email format ("email@example.com") and name-email format ("John Doe "). Args: - email_list (Optional[List[str]]): List of email addresses, + email_list (List[str]): List of email addresses, which can include display names. Returns: @@ -738,9 +728,6 @@ def _create_recipients(self, email_list: Optional[List[str]]) -> List[Any]: from msgraph.generated.models import email_address, recipient - if not email_list: - return [] - recipients: List[Any] = [] for email in email_list: # Extract email address from both formats: "Email", "Name " @@ -796,11 +783,11 @@ def _create_message( """ from msgraph.generated.models import body_type, item_body, message - # Determine content type - if is_content_html: - content_type = body_type.BodyType.Html - else: - content_type = body_type.BodyType.Text + content_type = ( + body_type.BodyType.Html + if is_content_html + else body_type.BodyType.Text + ) mail_message = message.Message() @@ -839,7 +826,7 @@ def _create_message( return mail_message - async def outlook_send_email( + def outlook_send_email( self, to_email: List[str], subject: str, @@ -875,7 +862,6 @@ async def outlook_send_email( Returns: Dict[str, Any]: A dictionary containing the result of the email sending operation. - """ from msgraph.generated.users.item.send_mail.send_mail_post_request_body import ( # noqa: E501 SendMailPostRequestBody, @@ -910,7 +896,7 @@ async def outlook_send_email( save_to_sent_items=save_to_sent_items, ) - await self.client.me.send_mail.post(request) + self.client.me.send_mail.post(request) logger.info("Email sent successfully.") return { @@ -923,7 +909,7 @@ async def outlook_send_email( logger.exception("Failed to send email") return {"error": f"Failed to send email: {e!s}"} - async def outlook_create_draft_email( + def outlook_create_draft_email( self, to_email: List[str], subject: str, @@ -982,7 +968,7 @@ async def outlook_create_draft_email( reply_to=reply_to, ) - result = await self.client.me.messages.post(request_body) + result = self.client.me.messages.post(request_body) logger.info("Draft email created successfully.") return { @@ -997,7 +983,7 @@ async def outlook_create_draft_email( logger.error(error_msg) return {"error": error_msg} - async def outlook_send_draft_email(self, draft_id: str) -> Dict[str, Any]: + def outlook_send_draft_email(self, draft_id: str) -> Dict[str, Any]: """Sends a draft email via Microsoft Outlook. Args: @@ -1009,10 +995,9 @@ async def outlook_send_draft_email(self, draft_id: str) -> Dict[str, Any]: Returns: Dict[str, Any]: A dictionary containing the result of the draft email sending operation. - """ try: - await self.client.me.messages.by_message_id(draft_id).send.post() + self.client.me.messages.by_message_id(draft_id).send.post() logger.info(f"Draft email with ID {draft_id} sent successfully.") return { @@ -1025,7 +1010,7 @@ async def outlook_send_draft_email(self, draft_id: str) -> Dict[str, Any]: logger.error(error_msg) return {"error": error_msg} - async def outlook_delete_email(self, message_id: str) -> Dict[str, Any]: + def outlook_delete_email(self, message_id: str) -> Dict[str, Any]: """Deletes an email from Microsoft Outlook. Args: @@ -1036,10 +1021,9 @@ async def outlook_delete_email(self, message_id: str) -> Dict[str, Any]: Returns: Dict[str, Any]: A dictionary containing the result of the email deletion operation. - """ try: - await self.client.me.messages.by_message_id(message_id).delete() + self.client.me.messages.by_message_id(message_id).delete() logger.info(f"Email with ID {message_id} deleted successfully.") return { 'status': 'success', @@ -1051,7 +1035,7 @@ async def outlook_delete_email(self, message_id: str) -> Dict[str, Any]: logger.error(error_msg) return {"error": error_msg} - async def outlook_move_message_to_folder( + def outlook_move_message_to_folder( self, message_id: str, destination_folder_id: str ) -> Dict[str, Any]: """Moves an email to a specified folder in Microsoft Outlook. @@ -1068,7 +1052,6 @@ async def outlook_move_message_to_folder( Returns: Dict[str, Any]: A dictionary containing the result of the email move operation. - """ from msgraph.generated.users.item.messages.item.move.move_post_request_body import ( # noqa: E501 MovePostRequestBody, @@ -1079,7 +1062,7 @@ async def outlook_move_message_to_folder( destination_id=destination_folder_id, ) message = self.client.me.messages.by_message_id(message_id) - await message.move.post(request_body) + message.move.post(request_body) logger.info( f"Email with ID {message_id} moved to folder " @@ -1096,7 +1079,7 @@ async def outlook_move_message_to_folder( logger.error(error_msg) return {"error": error_msg} - async def outlook_get_attachments( + def outlook_get_attachments( self, message_id: str, metadata_only: bool = True, @@ -1131,14 +1114,13 @@ async def outlook_get_attachments( Returns: Dict[str, Any]: A dictionary containing the attachment retrieval results - """ try: request_config = None if metadata_only: request_config = self._build_attachment_query() - attachments_response = await self._fetch_attachments( + attachments_response = self._fetch_attachments( message_id, request_config ) if not attachments_response: @@ -1173,11 +1155,7 @@ async def outlook_get_attachments( return {"error": error_msg} def _build_attachment_query(self): - """Constructs the query configuration for fetching attachments. - - Args: - metadata_only (bool): Whether to fetch only metadata or include - content bytes. + """Constructs the query configuration for fetching attachment metadata. Returns: AttachmentsRequestBuilderGetRequestConfiguration: Query config @@ -1202,7 +1180,7 @@ def _build_attachment_query(self): query_parameters=query_params ) - async def _fetch_attachments( + def _fetch_attachments( self, message_id: str, request_config: Optional[Any] = None ): """Fetches attachments from the Microsoft Graph API. @@ -1217,10 +1195,10 @@ async def _fetch_attachments( Attachments response from the Graph API. """ if not request_config: - return await self.client.me.messages.by_message_id( + return self.client.me.messages.by_message_id( message_id ).attachments.get() - return await self.client.me.messages.by_message_id( + return self.client.me.messages.by_message_id( message_id ).attachments.get(request_configuration=request_config) @@ -1242,6 +1220,7 @@ def _process_attachment( """ import base64 + last_modified = getattr(attachment, 'last_modified_date_time', None) info = { 'id': attachment.id, 'name': attachment.name, @@ -1249,14 +1228,14 @@ def _process_attachment( 'size': attachment.size, 'is_inline': getattr(attachment, 'is_inline', False), 'last_modified_date_time': ( - attachment.last_modified_date_time.isoformat() + last_modified.isoformat() if last_modified else None ), } if not metadata_only: content_bytes = getattr(attachment, 'content_bytes', None) if content_bytes: - # Decode once because bytes contain Base64 text ' + # Decode once because bytes contain Base64 text decoded_bytes = base64.b64decode(content_bytes) if save_path: @@ -1355,7 +1334,7 @@ def _get_recipients(self, recipient_list: Optional[List[Any]]): recipients.append({'address': email, 'name': name}) return recipients - async def _extract_message_details( + def _extract_message_details( self, message: Any, return_html_content: bool = False, @@ -1401,7 +1380,6 @@ async def _extract_message_details( body etc.) - Recipients (to_recipients, cc_recipients, bcc_recipients) - Attachment information (if requested) - """ try: # Validate message object @@ -1454,7 +1432,7 @@ async def _extract_message_details( if not include_attachments: return details - attachments_info = await self.outlook_get_attachments( + attachments_info = self.outlook_get_attachments( message_id=details['message_id'], metadata_only=attachment_metadata_only, include_inline_attachments=include_inline_attachments, @@ -1468,7 +1446,7 @@ async def _extract_message_details( logger.error(error_msg) raise ValueError(error_msg) - async def outlook_get_message( + def outlook_get_message( self, message_id: str, return_html_content: bool = False, @@ -1511,19 +1489,16 @@ async def outlook_get_message( cc_recipients, bcc_recipients, received_date_time, sent_date_time, body, body_type, has_attachments, importance, is_read, is_draft, body_preview, and optionally attachments. - """ try: - message = await self.client.me.messages.by_message_id( - message_id - ).get() + message = self.client.me.messages.by_message_id(message_id).get() if not message: error_msg = f"Message with ID {message_id} not found" logger.error(error_msg) return {"error": error_msg} - details = await self._extract_message_details( + details = self._extract_message_details( message=message, return_html_content=return_html_content, include_attachments=include_attachments, @@ -1543,7 +1518,7 @@ async def outlook_get_message( logger.error(error_msg) return {"error": error_msg} - async def _get_messages_from_folder( + def _get_messages_from_folder( self, folder_id: str, request_config, @@ -1558,7 +1533,7 @@ async def _get_messages_from_folder( Messages response from the Graph API, or None if folder not found. """ try: - messages = await self.client.me.mail_folders.by_mail_folder_id( + messages = self.client.me.mail_folders.by_mail_folder_id( folder_id ).messages.get(request_configuration=request_config) return messages @@ -1568,7 +1543,7 @@ async def _get_messages_from_folder( ) return None - async def outlook_list_messages( + def outlook_list_messages( self, folder_ids: Optional[List[str]] = None, filter_query: Optional[str] = None, @@ -1638,38 +1613,30 @@ async def outlook_list_messages( ) # Build query parameters - if order_by: - query_params = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters( # noqa: E501 - top=top, - skip=skip, - orderby=order_by, - ) - else: - query_params = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters( # noqa: E501 - top=top, - skip=skip, - ) - - if filter_query: - query_params.filter = filter_query + query_params = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters( # noqa: E501 + top=top, + skip=skip, + orderby=order_by, + filter=filter_query, + ) request_config = MessagesRequestBuilder.MessagesRequestBuilderGetRequestConfiguration( # noqa: E501 query_parameters=query_params ) if not folder_ids: # Search entire mailbox in a single API call - messages_response = await self.client.me.messages.get( + messages_response = self.client.me.messages.get( request_configuration=request_config ) all_messages = [] if messages_response and messages_response.value: for message in messages_response.value: - details = await self._extract_message_details( + details = self._extract_message_details( message=message, return_html_content=return_html_content, include_attachments=include_attachment_metadata, attachment_metadata_only=True, - include_inline_attachments=True, + include_inline_attachments=False, attachment_save_path=None, ) all_messages.append(details) @@ -1689,7 +1656,7 @@ async def outlook_list_messages( # Search specific folders (requires multiple API calls) all_messages = [] for folder_id in folder_ids: - messages_response = await self._get_messages_from_folder( + messages_response = self._get_messages_from_folder( folder_id=folder_id, request_config=request_config, ) @@ -1699,7 +1666,7 @@ async def outlook_list_messages( # Extract details from each message for message in messages_response.value: - details = await self._extract_message_details( + details = self._extract_message_details( message=message, return_html_content=return_html_content, include_attachments=include_attachment_metadata, @@ -1728,7 +1695,7 @@ async def outlook_list_messages( logger.error(error_msg) return {"error": error_msg} - async def outlook_reply_to_email( + def outlook_reply_to_email( self, message_id: str, content: str, @@ -1763,10 +1730,10 @@ async def outlook_reply_to_email( request_body_reply_all = ReplyAllPostRequestBody( comment=content ) - await message_request.reply_all.post(request_body_reply_all) + message_request.reply_all.post(request_body_reply_all) else: request_body = ReplyPostRequestBody(comment=content) - await message_request.reply.post(request_body) + message_request.reply.post(request_body) reply_type = "Reply All" if reply_all else "Reply" logger.info( @@ -1786,7 +1753,7 @@ async def outlook_reply_to_email( logger.error(error_msg) return {"error": error_msg} - async def outlook_update_draft_message( + def outlook_update_draft_message( self, message_id: str, subject: Optional[str] = None, @@ -1834,7 +1801,6 @@ async def outlook_update_draft_message( Returns: Dict[str, Any]: A dictionary containing the result of the update operation. - """ try: # Validate all email addresses if provided @@ -1861,7 +1827,7 @@ async def outlook_update_draft_message( ) # Update the message using PATCH - await self.client.me.messages.by_message_id(message_id).patch( + self.client.me.messages.by_message_id(message_id).patch( mail_message ) @@ -1869,20 +1835,19 @@ async def outlook_update_draft_message( f"Draft message with ID {message_id} updated successfully." ) - # Build dict of updated parameters - updated_params: Dict[str, Any] = dict() - if subject: - updated_params['subject'] = subject - if content: - updated_params['content'] = content - if to_email: - updated_params['to_email'] = to_email - if cc_recipients: - updated_params['cc_recipients'] = cc_recipients - if bcc_recipients: - updated_params['bcc_recipients'] = bcc_recipients - if reply_to: - updated_params['reply_to'] = reply_to + # Build dict of updated parameters (only include non-None values) + updated_params = { + k: v + for k, v in { + 'subject': subject, + 'content': content, + 'to_email': to_email, + 'cc_recipients': cc_recipients, + 'bcc_recipients': bcc_recipients, + 'reply_to': reply_to, + }.items() + if v + } return { 'status': 'success', diff --git a/test/toolkits/test_microsoft_outlook_mail_toolkit.py b/test/toolkits/test_microsoft_outlook_mail_toolkit.py index 8d5ba32d9c..02a6dcc62c 100644 --- a/test/toolkits/test_microsoft_outlook_mail_toolkit.py +++ b/test/toolkits/test_microsoft_outlook_mail_toolkit.py @@ -14,17 +14,15 @@ import json import os -from unittest.mock import AsyncMock, MagicMock, mock_open, patch +from unittest.mock import MagicMock, mock_open, patch import pytest from camel.toolkits import OutlookMailToolkit from camel.toolkits.microsoft_outlook_mail_toolkit import ( - AsyncCustomAzureCredential, + CustomAzureCredential, ) -pytestmark = pytest.mark.asyncio - @pytest.fixture def mock_graph_service(): @@ -44,13 +42,13 @@ def mock_graph_service(): # Mock send_mail endpoint mock_send_mail = MagicMock() mock_me.send_mail = mock_send_mail - mock_send_mail.post = AsyncMock() + mock_send_mail.post = MagicMock() # Mock messages.get for listing messages - mock_messages.get = AsyncMock() + mock_messages.get = MagicMock() # Mock messages.post for creating drafts - mock_messages.post = AsyncMock() + mock_messages.post = MagicMock() # Mock messages.by_message_id for specific message operations mock_by_message_id = MagicMock() @@ -59,38 +57,38 @@ def mock_graph_service(): ) # Mock get message by ID - mock_by_message_id.get = AsyncMock() + mock_by_message_id.get = MagicMock() # Mock send draft mock_send = MagicMock() mock_by_message_id.send = mock_send - mock_send.post = AsyncMock() + mock_send.post = MagicMock() # Mock delete message - mock_by_message_id.delete = AsyncMock() + mock_by_message_id.delete = MagicMock() # Mock move message mock_move = MagicMock() mock_by_message_id.move = mock_move - mock_move.post = AsyncMock() + mock_move.post = MagicMock() # Mock attachments mock_attachments = MagicMock() mock_by_message_id.attachments = mock_attachments - mock_attachments.get = AsyncMock() + mock_attachments.get = MagicMock() # Mock reply endpoint mock_reply = MagicMock() mock_by_message_id.reply = mock_reply - mock_reply.post = AsyncMock() + mock_reply.post = MagicMock() # Mock reply_all endpoint mock_reply_all = MagicMock() mock_by_message_id.reply_all = mock_reply_all - mock_reply_all.post = AsyncMock() + mock_reply_all.post = MagicMock() # Mock patch endpoint for updating messages - mock_by_message_id.patch = AsyncMock() + mock_by_message_id.patch = MagicMock() # Mock mail_folders for folder-specific operations mock_mail_folders = MagicMock() @@ -105,7 +103,7 @@ def mock_graph_service(): # Mock folder messages mock_folder_messages = MagicMock() mock_by_folder_id.messages = mock_folder_messages - mock_folder_messages.get = AsyncMock() + mock_folder_messages.get = MagicMock() yield mock_client @@ -147,11 +145,11 @@ def outlook_toolkit(mock_graph_service): yield toolkit -async def test_send_email(outlook_toolkit, mock_graph_service): +def test_send_email(outlook_toolkit, mock_graph_service): """Test sending an email successfully.""" mock_graph_service.me.send_mail.post.return_value = None - result = await outlook_toolkit.outlook_send_email( + result = outlook_toolkit.outlook_send_email( to_email=['test@example.com'], subject='Test Subject', content='Test Body', @@ -165,21 +163,18 @@ async def test_send_email(outlook_toolkit, mock_graph_service): mock_graph_service.me.send_mail.post.assert_called_once() -async def test_send_email_with_attachments( - outlook_toolkit, mock_graph_service -): +def test_send_email_with_attachments(outlook_toolkit, mock_graph_service): """Test sending an email with attachments.""" mock_graph_service.me.send_mail.post.return_value = None with ( patch('os.path.isfile', return_value=True), - patch('builtins.open', create=True) as mock_open, + patch('builtins.open', create=True) as mock_file_open, ): - mock_open.return_value.__enter__.return_value.read.return_value = ( - b'test content' - ) + mock_file = mock_file_open.return_value.__enter__.return_value + mock_file.read.return_value = b'test content' - result = await outlook_toolkit.outlook_send_email( + result = outlook_toolkit.outlook_send_email( to_email=['test@example.com'], subject='Test Subject', content='Test Body', @@ -190,9 +185,9 @@ async def test_send_email_with_attachments( mock_graph_service.me.send_mail.post.assert_called_once() -async def test_send_email_invalid_email(outlook_toolkit): +def test_send_email_invalid_email(outlook_toolkit): """Test sending email with invalid email address.""" - result = await outlook_toolkit.outlook_send_email( + result = outlook_toolkit.outlook_send_email( to_email=['invalid-email'], subject='Test Subject', content='Test Body', @@ -202,11 +197,11 @@ async def test_send_email_invalid_email(outlook_toolkit): assert 'Invalid email address' in result['error'] -async def test_send_email_failure(outlook_toolkit, mock_graph_service): +def test_send_email_failure(outlook_toolkit, mock_graph_service): """Test sending email failure.""" mock_graph_service.me.send_mail.post.side_effect = Exception("API Error") - result = await outlook_toolkit.outlook_send_email( + result = outlook_toolkit.outlook_send_email( to_email=['test@example.com'], subject='Test Subject', content='Test Body', @@ -216,13 +211,13 @@ async def test_send_email_failure(outlook_toolkit, mock_graph_service): assert 'Failed to send email' in result['error'] -async def test_create_email_draft(outlook_toolkit, mock_graph_service): +def test_create_email_draft(outlook_toolkit, mock_graph_service): """Test creating an email draft.""" mock_draft_result = MagicMock() mock_draft_result.id = 'draft123' mock_graph_service.me.messages.post.return_value = mock_draft_result - result = await outlook_toolkit.outlook_create_draft_email( + result = outlook_toolkit.outlook_create_draft_email( to_email=['test@example.com'], subject='Test Subject', content='Test Body', @@ -237,7 +232,7 @@ async def test_create_email_draft(outlook_toolkit, mock_graph_service): mock_graph_service.me.messages.post.assert_called_once() -async def test_browser_auth_persists_refresh_token(tmp_path): +def test_browser_auth_persists_refresh_token(tmp_path): toolkit = OutlookMailToolkit.__new__(OutlookMailToolkit) toolkit.scopes = ["Mail.Send", "Mail.ReadWrite"] toolkit.redirect_uri = "http://localhost:12345" @@ -265,7 +260,7 @@ async def test_browser_auth_persists_refresh_token(tmp_path): ): credentials = toolkit._authenticate_using_browser() - assert isinstance(credentials, AsyncCustomAzureCredential) + assert isinstance(credentials, CustomAzureCredential) assert credentials.refresh_token == "mock_refresh_token" assert credentials._access_token == "mock_access_token" @@ -274,7 +269,7 @@ async def test_browser_auth_persists_refresh_token(tmp_path): assert token_data["refresh_token"] == "mock_refresh_token" -async def test_create_email_draft_with_attachments( +def test_create_email_draft_with_attachments( outlook_toolkit, mock_graph_service ): """Test creating an email draft with attachments.""" @@ -284,13 +279,12 @@ async def test_create_email_draft_with_attachments( with ( patch('os.path.isfile', return_value=True), - patch('builtins.open', create=True) as mock_open, + patch('builtins.open', create=True) as mock_file_open, ): - mock_open.return_value.__enter__.return_value.read.return_value = ( - b'test content' - ) + mock_file = mock_file_open.return_value.__enter__.return_value + mock_file.read.return_value = b'test content' - result = await outlook_toolkit.outlook_create_draft_email( + result = outlook_toolkit.outlook_create_draft_email( to_email=['test@example.com'], subject='Test Subject', content='Test Body', @@ -302,9 +296,9 @@ async def test_create_email_draft_with_attachments( mock_graph_service.me.messages.post.assert_called_once() -async def test_create_email_draft_invalid_email(outlook_toolkit): +def test_create_email_draft_invalid_email(outlook_toolkit): """Test creating draft with invalid email address.""" - result = await outlook_toolkit.outlook_create_draft_email( + result = outlook_toolkit.outlook_create_draft_email( to_email=['invalid-email'], subject='Test Subject', content='Test Body', @@ -314,11 +308,11 @@ async def test_create_email_draft_invalid_email(outlook_toolkit): assert 'Invalid email address' in result['error'] -async def test_create_email_draft_failure(outlook_toolkit, mock_graph_service): +def test_create_email_draft_failure(outlook_toolkit, mock_graph_service): """Test creating email draft failure.""" mock_graph_service.me.messages.post.side_effect = Exception("API Error") - result = await outlook_toolkit.outlook_create_draft_email( + result = outlook_toolkit.outlook_create_draft_email( to_email=['test@example.com'], subject='Test Subject', content='Test Body', @@ -328,15 +322,13 @@ async def test_create_email_draft_failure(outlook_toolkit, mock_graph_service): assert 'Failed to create draft email' in result['error'] -async def test_send_draft_email(outlook_toolkit, mock_graph_service): +def test_send_draft_email(outlook_toolkit, mock_graph_service): """Test sending a draft email.""" mock_graph_service.me.messages.by_message_id().send.post.return_value = ( None ) - result = await outlook_toolkit.outlook_send_draft_email( - draft_id='draft123' - ) + result = outlook_toolkit.outlook_send_draft_email(draft_id='draft123') assert result['status'] == 'success' assert result['message'] == 'Draft email sent successfully' @@ -345,25 +337,23 @@ async def test_send_draft_email(outlook_toolkit, mock_graph_service): mock_graph_service.me.messages.by_message_id().send.post.assert_called_once() -async def test_send_draft_email_failure(outlook_toolkit, mock_graph_service): +def test_send_draft_email_failure(outlook_toolkit, mock_graph_service): """Test sending draft email failure.""" mock_graph_service.me.messages.by_message_id().send.post.side_effect = ( Exception("API Error") ) - result = await outlook_toolkit.outlook_send_draft_email( - draft_id='draft123' - ) + result = outlook_toolkit.outlook_send_draft_email(draft_id='draft123') assert 'error' in result assert 'Failed to send draft email' in result['error'] -async def test_delete_email(outlook_toolkit, mock_graph_service): +def test_delete_email(outlook_toolkit, mock_graph_service): """Test deleting an email successfully.""" mock_graph_service.me.messages.by_message_id().delete.return_value = None - result = await outlook_toolkit.outlook_delete_email(message_id='msg123') + result = outlook_toolkit.outlook_delete_email(message_id='msg123') assert result['status'] == 'success' assert result['message'] == 'Email deleted successfully' @@ -372,25 +362,25 @@ async def test_delete_email(outlook_toolkit, mock_graph_service): mock_graph_service.me.messages.by_message_id().delete.assert_called_once() -async def test_delete_email_failure(outlook_toolkit, mock_graph_service): +def test_delete_email_failure(outlook_toolkit, mock_graph_service): """Test deleting email failure.""" mock_graph_service.me.messages.by_message_id().delete.side_effect = ( Exception("API Error") ) - result = await outlook_toolkit.outlook_delete_email(message_id='msg123') + result = outlook_toolkit.outlook_delete_email(message_id='msg123') assert 'error' in result assert 'Failed to delete email' in result['error'] -async def test_move_message_to_folder(outlook_toolkit, mock_graph_service): +def test_move_message_to_folder(outlook_toolkit, mock_graph_service): """Test moving an email to a folder successfully.""" mock_graph_service.me.messages.by_message_id().move.post.return_value = ( None ) - result = await outlook_toolkit.outlook_move_message_to_folder( + result = outlook_toolkit.outlook_move_message_to_folder( message_id='msg123', destination_folder_id='inbox' ) @@ -402,15 +392,13 @@ async def test_move_message_to_folder(outlook_toolkit, mock_graph_service): mock_graph_service.me.messages.by_message_id().move.post.assert_called_once() -async def test_move_message_to_folder_failure( - outlook_toolkit, mock_graph_service -): +def test_move_message_to_folder_failure(outlook_toolkit, mock_graph_service): """Test moving email failure.""" mock_graph_service.me.messages.by_message_id().move.post.side_effect = ( Exception("API Error") ) - result = await outlook_toolkit.outlook_move_message_to_folder( + result = outlook_toolkit.outlook_move_message_to_folder( message_id='msg123', destination_folder_id='inbox' ) @@ -418,7 +406,7 @@ async def test_move_message_to_folder_failure( assert 'Failed to move email' in result['error'] -async def test_get_attachments(outlook_toolkit, mock_graph_service): +def test_get_attachments(outlook_toolkit, mock_graph_service): """Test getting attachments and saving to disk.""" import base64 @@ -437,7 +425,7 @@ async def test_get_attachments(outlook_toolkit, mock_graph_service): patch('os.path.exists', return_value=False), patch('builtins.open', create=True), ): - result = await outlook_toolkit.outlook_get_attachments( + result = outlook_toolkit.outlook_get_attachments( message_id='msg123', ) @@ -445,9 +433,7 @@ async def test_get_attachments(outlook_toolkit, mock_graph_service): assert result['total_count'] == 1 -async def test_get_attachments_exclude_inline( - outlook_toolkit, mock_graph_service -): +def test_get_attachments_exclude_inline(outlook_toolkit, mock_graph_service): """Test getting attachments excluding inline attachments (default).""" mock_attachment1 = MagicMock() mock_attachment1.name = 'image.png' @@ -458,7 +444,7 @@ async def test_get_attachments_exclude_inline( mock_attachments = mock_graph_service.me.messages.by_message_id() mock_attachments.attachments.get.return_value = mock_response - result = await outlook_toolkit.outlook_get_attachments( + result = outlook_toolkit.outlook_get_attachments( message_id='msg123', include_inline_attachments=False, ) @@ -468,9 +454,7 @@ async def test_get_attachments_exclude_inline( assert not result['attachments'] -async def test_get_attachments_include_inline( - outlook_toolkit, mock_graph_service -): +def test_get_attachments_include_inline(outlook_toolkit, mock_graph_service): """Test getting attachments including inline attachments.""" mock_attachment1 = MagicMock() mock_attachment1.name = 'document.pdf' @@ -485,7 +469,7 @@ async def test_get_attachments_include_inline( mock_attachments = mock_graph_service.me.messages.by_message_id() mock_attachments.attachments.get.return_value = mock_response - result = await outlook_toolkit.outlook_get_attachments( + result = outlook_toolkit.outlook_get_attachments( message_id='msg123', metadata_only=True, include_inline_attachments=True, @@ -497,18 +481,18 @@ async def test_get_attachments_include_inline( assert result['attachments'][1]['name'] == 'image.png' -async def test_get_attachments_failure(outlook_toolkit, mock_graph_service): +def test_get_attachments_failure(outlook_toolkit, mock_graph_service): """Test getting attachments failure.""" mock_attachments = mock_graph_service.me.messages.by_message_id() mock_attachments.attachments.get.side_effect = Exception("API Error") - result = await outlook_toolkit.outlook_get_attachments(message_id='msg123') + result = outlook_toolkit.outlook_get_attachments(message_id='msg123') assert 'error' in result assert 'Failed to get attachments' in result['error'] -async def test_get_attachments_with_content_and_save_path( +def test_get_attachments_with_content_and_save_path( outlook_toolkit, mock_graph_service ): """Test getting attachments with metadata_only=False and save_path.""" @@ -536,7 +520,7 @@ async def test_get_attachments_with_content_and_save_path( patch('os.path.exists', return_value=False), patch('builtins.open', m_open), ): - result = await outlook_toolkit.outlook_get_attachments( + result = outlook_toolkit.outlook_get_attachments( message_id='msg456', metadata_only=False, save_path=temp_dir, @@ -615,9 +599,7 @@ def create_mock_message(request): return mock_msg -async def test_get_message( - outlook_toolkit, mock_graph_service, create_mock_message -): +def test_get_message(outlook_toolkit, mock_graph_service, create_mock_message): """Test getting messages with different content types.""" with patch( 'camel.toolkits.microsoft_outlook_mail_toolkit.isinstance', @@ -627,7 +609,7 @@ async def test_get_message( create_mock_message ) - result = await outlook_toolkit.outlook_get_message(message_id='msg123') + result = outlook_toolkit.outlook_get_message(message_id='msg123') assert result['status'] == 'success' assert 'message' in result @@ -643,19 +625,19 @@ async def test_get_message( mock_graph_service.me.messages.by_message_id().get.assert_called_once() -async def test_get_message_failure(outlook_toolkit, mock_graph_service): +def test_get_message_failure(outlook_toolkit, mock_graph_service): """Test get_message handles API errors correctly.""" mock_graph_service.me.messages.by_message_id().get.side_effect = Exception( "API Error" ) - result = await outlook_toolkit.outlook_get_message(message_id='msg123') + result = outlook_toolkit.outlook_get_message(message_id='msg123') assert 'error' in result assert 'Failed to get message' in result['error'] -async def test_list_messages( +def test_list_messages( outlook_toolkit, mock_graph_service, create_mock_message ): """Test listing messages with different content types.""" @@ -667,7 +649,7 @@ async def test_list_messages( mock_response.value = [create_mock_message] mock_graph_service.me.messages.get.return_value = mock_response - result = await outlook_toolkit.outlook_list_messages() + result = outlook_toolkit.outlook_list_messages() assert result['status'] == 'success' assert 'messages' in result @@ -685,23 +667,23 @@ async def test_list_messages( mock_graph_service.me.messages.get.assert_called_once() -async def test_list_messages_failure(outlook_toolkit, mock_graph_service): +def test_list_messages_failure(outlook_toolkit, mock_graph_service): """Test list_messages handles API errors correctly.""" mock_graph_service.me.messages.get.side_effect = Exception("API Error") - result = await outlook_toolkit.outlook_list_messages() + result = outlook_toolkit.outlook_list_messages() assert 'error' in result assert 'Failed to list messages' in result['error'] -async def test_reply_to_email(outlook_toolkit, mock_graph_service): +def test_reply_to_email(outlook_toolkit, mock_graph_service): """Test replying to an email (reply to sender only).""" mock_graph_service.me.messages.by_message_id().reply.post.return_value = ( None ) - result = await outlook_toolkit.outlook_reply_to_email( + result = outlook_toolkit.outlook_reply_to_email( message_id='msg123', content='This is my reply', reply_all=False, @@ -716,12 +698,12 @@ async def test_reply_to_email(outlook_toolkit, mock_graph_service): mock_graph_service.me.messages.by_message_id().reply_all.post.assert_not_called() -async def test_reply_to_email_all(outlook_toolkit, mock_graph_service): +def test_reply_to_email_all(outlook_toolkit, mock_graph_service): """Test replying to all recipients of an email.""" mock_reply_all = mock_graph_service.me.messages.by_message_id().reply_all mock_reply_all.post.return_value = None - result = await outlook_toolkit.outlook_reply_to_email( + result = outlook_toolkit.outlook_reply_to_email( message_id='msg456', content='This is my reply to all', reply_all=True, @@ -736,13 +718,13 @@ async def test_reply_to_email_all(outlook_toolkit, mock_graph_service): mock_graph_service.me.messages.by_message_id().reply.post.assert_not_called() -async def test_reply_to_email_failure(outlook_toolkit, mock_graph_service): +def test_reply_to_email_failure(outlook_toolkit, mock_graph_service): """Test reply to email failure when using simple reply.""" mock_graph_service.me.messages.by_message_id().reply.post.side_effect = ( Exception("API Error: Unable to send reply") ) - result = await outlook_toolkit.outlook_reply_to_email( + result = outlook_toolkit.outlook_reply_to_email( message_id='msg123', content='This reply will fail', reply_all=False, @@ -753,14 +735,14 @@ async def test_reply_to_email_failure(outlook_toolkit, mock_graph_service): assert 'API Error: Unable to send reply' in result['error'] -async def test_reply_to_email_all_failure(outlook_toolkit, mock_graph_service): +def test_reply_to_email_all_failure(outlook_toolkit, mock_graph_service): """Test reply to email failure when using reply all.""" mock_reply_all = mock_graph_service.me.messages.by_message_id().reply_all mock_reply_all.post.side_effect = Exception( "API Error: Unable to send reply all" ) - result = await outlook_toolkit.outlook_reply_to_email( + result = outlook_toolkit.outlook_reply_to_email( message_id='msg456', content='This reply all will fail', reply_all=True, @@ -771,12 +753,12 @@ async def test_reply_to_email_all_failure(outlook_toolkit, mock_graph_service): assert 'API Error: Unable to send reply all' in result['error'] -async def test_update_draft_message(outlook_toolkit, mock_graph_service): +def test_update_draft_message(outlook_toolkit, mock_graph_service): """Test updating a draft message with all parameters.""" mock_by_message_id = mock_graph_service.me.messages.by_message_id() mock_by_message_id.patch.return_value = None - result = await outlook_toolkit.outlook_update_draft_message( + result = outlook_toolkit.outlook_update_draft_message( message_id='draft123', subject='Updated Subject', content='Updated content', @@ -800,14 +782,14 @@ async def test_update_draft_message(outlook_toolkit, mock_graph_service): mock_by_message_id.patch.assert_called_once() -async def test_update_draft_message_subject_only( +def test_update_draft_message_subject_only( outlook_toolkit, mock_graph_service ): """Test updating only the subject of a draft message.""" mock_by_message_id = mock_graph_service.me.messages.by_message_id() mock_by_message_id.patch.return_value = None - result = await outlook_toolkit.outlook_update_draft_message( + result = outlook_toolkit.outlook_update_draft_message( message_id='draft456', subject='New Subject Only', ) @@ -821,16 +803,14 @@ async def test_update_draft_message_subject_only( mock_by_message_id.patch.assert_called_once() -async def test_update_draft_message_failure( - outlook_toolkit, mock_graph_service -): +def test_update_draft_message_failure(outlook_toolkit, mock_graph_service): """Test update draft message failure.""" mock_by_message_id = mock_graph_service.me.messages.by_message_id() mock_by_message_id.patch.side_effect = Exception( "API Error: Unable to update draft" ) - result = await outlook_toolkit.outlook_update_draft_message( + result = outlook_toolkit.outlook_update_draft_message( message_id='draft123', subject='This update will fail', )