2727from camel .toolkits import FunctionTool
2828from camel .toolkits .base import BaseToolkit
2929from camel .utils import MCPServer , api_keys_required
30+ from camel .utils .commons import run_async
3031
3132load_dotenv ()
3233logger = get_logger (__name__ )
@@ -73,8 +74,8 @@ class CustomAzureCredential:
7374 tenant_id (str): The Microsoft tenant ID.
7475 refresh_token (str): The refresh token from OAuth flow.
7576 scopes (List[str]): List of OAuth permission scopes.
76- token_file_path (Optional[Path]): Path to persist the refresh token.
77- If None, token persistence is disabled .
77+ refresh_token_file_path (Optional[Path]): File path of json file
78+ with refresh token.
7879 """
7980
8081 def __init__ (
@@ -84,14 +85,14 @@ def __init__(
8485 tenant_id : str ,
8586 refresh_token : str ,
8687 scopes : List [str ],
87- token_file_path : Optional [Path ],
88+ refresh_token_file_path : Optional [Path ],
8889 ):
8990 self .client_id = client_id
9091 self .client_secret = client_secret
9192 self .tenant_id = tenant_id
9293 self .refresh_token = refresh_token
9394 self .scopes = scopes
94- self .token_file_path = token_file_path
95+ self .refresh_token_file_path = refresh_token_file_path
9596
9697 self ._access_token = None
9798 self ._expires_at = 0
@@ -144,18 +145,20 @@ def _save_refresh_token(self, refresh_token: str):
144145 Args:
145146 refresh_token (str): The refresh token to save.
146147 """
147- if not self .token_file_path :
148+ if not self .refresh_token_file_path :
148149 logger .info ("Token file path not set, skipping token save" )
149150 return
150151
151152 token_data = {"refresh_token" : refresh_token }
152153
153154 try :
154155 # Create parent directories if they don't exist
155- self .token_file_path .parent .mkdir (parents = True , exist_ok = True )
156+ self .refresh_token_file_path .parent .mkdir (
157+ parents = True , exist_ok = True
158+ )
156159
157160 # Write new refresh token to file
158- with open (self .token_file_path , 'w' ) as f :
161+ with open (self .refresh_token_file_path , 'w' ) as f :
159162 json .dump (token_data , f , indent = 2 )
160163 except Exception as e :
161164 logger .warning (f"Failed to save refresh token: { e !s} " )
@@ -191,35 +194,38 @@ def get_token(self, *args, **kwargs):
191194
192195@MCPServer ()
193196class OutlookMailToolkit (BaseToolkit ):
194- """A class representing a toolkit for Microsoft Outlook operations.
197+ """A comprehensive toolkit for Microsoft Outlook Mail operations.
195198
196- This class provides methods for interacting with Microsoft Outlook via
197- the Microsoft Graph API.
199+ This class provides methods for Outlook Mail operations including sending
200+ emails, managing drafts, replying to mails, deleting mails, fetching
201+ mails and attachments and changing folder of mails.
202+ API keys can be accessed in the Azure portal (https://portal.azure.com/)
198203 """
199204
200205 def __init__ (
201206 self ,
202207 timeout : Optional [float ] = None ,
203- token_file_path : Optional [str ] = None ,
208+ refresh_token_file_path : Optional [str ] = None ,
204209 ):
205210 """Initializes a new instance of the OutlookMailToolkit.
206211
207212 Args:
208213 timeout (Optional[float]): The timeout value for API requests
209214 in seconds. If None, no timeout is applied.
210215 (default: :obj:`None`)
211- token_file_path (Optional[str]): The path to the file where the
212- refresh token will be stored. If None, token persistence is
213- disabled and browser authentication will be required on each
214- initialization. If provided, the token will be saved to and
215- loaded from this path. (default: :obj:`None`)
216+ refresh_token_file_path (Optional[str]): The path of json file
217+ where refresh token is stored. If None, authentication using
218+ web browser will be required on each initialization. If
219+ provided, the refresh token is read from the file, used, and
220+ automatically updated when it nears expiry.
221+ (default: :obj:`None`)
216222 """
217223 super ().__init__ (timeout = timeout )
218224
219225 self .scopes = ["Mail.Send" , "Mail.ReadWrite" ]
220226 self .redirect_uri = self ._get_dynamic_redirect_uri ()
221- self .token_file_path = (
222- Path (token_file_path ) if token_file_path else None
227+ self .refresh_token_file_path = (
228+ Path (refresh_token_file_path ) if refresh_token_file_path else None
223229 )
224230 self .credentials = self ._authenticate ()
225231 self .client = self ._get_graph_client (
@@ -272,20 +278,20 @@ def _load_token_from_file(self) -> Optional[str]:
272278 Returns:
273279 Optional[str]: Refresh token if file exists and valid, else None.
274280 """
275- if not self .token_file_path :
281+ if not self .refresh_token_file_path :
276282 return None
277283
278- if not self .token_file_path .exists ():
284+ if not self .refresh_token_file_path .exists ():
279285 return None
280286
281287 try :
282- with open (self .token_file_path , 'r' ) as f :
288+ with open (self .refresh_token_file_path , 'r' ) as f :
283289 token_data = json .load (f )
284290
285291 refresh_token = token_data .get ('refresh_token' )
286292 if refresh_token :
287293 logger .info (
288- f"Refresh token loaded from { self .token_file_path } "
294+ f"Refresh token loaded from { self .refresh_token_file_path } "
289295 )
290296 return refresh_token
291297
@@ -302,17 +308,21 @@ def _save_token_to_file(self, refresh_token: str):
302308 Args:
303309 refresh_token (str): The refresh token to save.
304310 """
305- if not self .token_file_path :
311+ if not self .refresh_token_file_path :
306312 logger .info ("Token file path not set, skipping token save" )
307313 return
308314
309315 try :
310316 # Create parent directories if they don't exist
311- self .token_file_path .parent .mkdir (parents = True , exist_ok = True )
317+ self .refresh_token_file_path .parent .mkdir (
318+ parents = True , exist_ok = True
319+ )
312320
313- with open (self .token_file_path , 'w' ) as f :
321+ with open (self .refresh_token_file_path , 'w' ) as f :
314322 json .dump ({"refresh_token" : refresh_token }, f , indent = 2 )
315- logger .info (f"Refresh token saved to { self .token_file_path } " )
323+ logger .info (
324+ f"Refresh token saved to { self .refresh_token_file_path } "
325+ )
316326 except Exception as e :
317327 logger .warning (f"Failed to save token to file: { e !s} " )
318328
@@ -342,7 +352,7 @@ def _authenticate_using_refresh_token(
342352 tenant_id = self .tenant_id ,
343353 refresh_token = refresh_token ,
344354 scopes = self .scopes ,
345- token_file_path = self .token_file_path ,
355+ refresh_token_file_path = self .refresh_token_file_path ,
346356 )
347357
348358 logger .info ("Authentication with saved token successful" )
@@ -426,7 +436,8 @@ def _authenticate(self):
426436 """Authenticates and creates credential for Microsoft Graph.
427437
428438 Implements two-stage authentication:
429- 1. Attempts to use saved refresh token if token_file_path is provided
439+ 1. Attempts to use saved refresh token if refresh_token_file_path is
440+ provided
430441 2. Falls back to browser OAuth if no token or token invalid
431442
432443 Returns:
@@ -443,7 +454,10 @@ def _authenticate(self):
443454 self .client_secret = os .getenv ("MICROSOFT_CLIENT_SECRET" )
444455
445456 # Try saved refresh token first if token file path is provided
446- if self .token_file_path and self .token_file_path .exists ():
457+ if (
458+ self .refresh_token_file_path
459+ and self .refresh_token_file_path .exists ()
460+ ):
447461 try :
448462 credentials : CustomAzureCredential = (
449463 self ._authenticate_using_refresh_token ()
@@ -781,16 +795,16 @@ def get_tools(self) -> List[FunctionTool]:
781795 representing the functions in the toolkit.
782796 """
783797 return [
784- FunctionTool (self .send_email ),
785- FunctionTool (self .create_draft_email ),
786- FunctionTool (self .send_draft_email ),
787- FunctionTool (self .delete_email ),
788- FunctionTool (self .move_message_to_folder ),
789- FunctionTool (self .get_attachments ),
790- FunctionTool (self .get_message ),
791- FunctionTool (self .list_messages ),
792- FunctionTool (self .reply_to_email ),
793- FunctionTool (self .update_draft_message ),
798+ FunctionTool (run_async ( self .send_email ) ),
799+ FunctionTool (run_async ( self .create_draft_email ) ),
800+ FunctionTool (run_async ( self .send_draft_email ) ),
801+ FunctionTool (run_async ( self .delete_email ) ),
802+ FunctionTool (run_async ( self .move_message_to_folder ) ),
803+ FunctionTool (run_async ( self .get_attachments ) ),
804+ FunctionTool (run_async ( self .get_message ) ),
805+ FunctionTool (run_async ( self .list_messages ) ),
806+ FunctionTool (run_async ( self .reply_to_email ) ),
807+ FunctionTool (run_async ( self .update_draft_message ) ),
794808 ]
795809
796810 async def create_draft_email (
0 commit comments