Skip to main content
Glama
utils.py10.8 kB
""" Utility functions for Outlook MCP server session management. This module provides utility functions for handling Outlook sessions, COM objects, and common operations that don't fit into the main classes. """ # Standard library imports import time from functools import wraps from typing import Any, Dict, List, Optional, Union # Third-party imports import pythoncom import win32com.client # Local application imports from ..logging_config import get_logger logger = get_logger(__name__) def safe_com_call(func): """ Decorator to safely handle COM object calls with proper error handling. This decorator: - Handles COM errors gracefully - Ensures COM objects are properly released - Provides detailed logging for debugging """ @wraps(func) def wrapper(*args, **kwargs): try: result = func(*args, **kwargs) logger.debug(f"COM call successful: {func.__name__}") return result except pythoncom.com_error as com_err: logger.error(f"COM error in {func.__name__}: {com_err}") raise except AttributeError as attr_err: logger.error(f"Attribute error in {func.__name__}: {attr_err}") raise except Exception as e: logger.error(f"Unexpected error in {func.__name__}: {e}") raise return wrapper def retry_on_com_error(max_attempts: int = 3, initial_delay: float = 1.0, backoff_factor: float = 2.0): """ Decorator to retry COM operations on failure with exponential backoff. Args: max_attempts: Maximum number of retry attempts initial_delay: Initial delay between retries in seconds backoff_factor: Factor to multiply delay by for each retry """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): delay = initial_delay last_exception = None for attempt in range(max_attempts): try: result = func(*args, **kwargs) if attempt > 0: logger.info(f"COM operation succeeded on attempt {attempt + 1}") return result except pythoncom.com_error as com_err: last_exception = com_err logger.warning(f"COM operation failed on attempt {attempt + 1}: {com_err}") if attempt < max_attempts - 1: logger.info(f"Retrying in {delay} seconds...") time.sleep(delay) delay *= backoff_factor else: logger.error(f"COM operation failed after {max_attempts} attempts") raise except Exception as e: # Don't retry on non-COM errors logger.error(f"Non-COM error in {func.__name__}: {e}") raise # This should never be reached, but just in case if last_exception: raise last_exception return wrapper return decorator def get_outlook_version() -> Optional[str]: """ Get the version of Outlook installed on the system. Returns: str: Outlook version string, or None if Outlook is not available """ try: pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application") version = outlook.Version logger.info(f"Detected Outlook version: {version}") return version except Exception as e: logger.error(f"Failed to get Outlook version: {e}") return None finally: try: pythoncom.CoUninitialize() except: pass def validate_outlook_installation() -> bool: """ Validate that Outlook is properly installed and accessible. Returns: bool: True if Outlook is accessible, False otherwise """ try: pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application") namespace = outlook.GetNamespace("MAPI") # Try to access a basic folder to verify functionality inbox = namespace.GetDefaultFolder(6) # olFolderInbox logger.info("Outlook installation validated successfully") return True except Exception as e: logger.error(f"Outlook validation failed: {e}") return False finally: try: pythoncom.CoUninitialize() except: pass def format_com_error(error: pythoncom.com_error) -> str: """ Format a COM error into a readable string. Args: error: The COM error object Returns: str: Formatted error message """ try: if hasattr(error, 'excepinfo') and error.excepinfo: return f"COM Error: {error.excepinfo[2]} (0x{error.excepinfo[5]:08X})" elif hasattr(error, 'strerror'): return f"COM Error: {error.strerror}" else: return f"COM Error: {str(error)}" except: return f"COM Error: {str(error)}" def safe_release_com_object(obj: Any) -> None: """ Safely release a COM object. Args: obj: The COM object to release """ try: if obj is not None: # Release COM object obj = None logger.debug("COM object released") except Exception as e: logger.warning(f"Error releasing COM object: {e}") def get_available_folders() -> List[Dict[str, Any]]: """ Get a list of available default folder types in Outlook. Returns: List[Dict[str, Any]]: List of folder information dictionaries """ return [ {"name": "Inbox", "constant": 6, "description": "Inbox folder"}, {"name": "Sent Items", "constant": 5, "description": "Sent items folder"}, {"name": "Deleted Items", "constant": 3, "description": "Deleted items folder"}, {"name": "Drafts", "constant": 16, "description": "Drafts folder"}, {"name": "Outbox", "constant": 4, "description": "Outbox folder"}, {"name": "Junk Email", "constant": 23, "description": "Junk email folder"}, {"name": "Calendar", "constant": 9, "description": "Calendar folder"}, {"name": "Contacts", "constant": 10, "description": "Contacts folder"}, {"name": "Tasks", "constant": 13, "description": "Tasks folder"}, {"name": "Notes", "constant": 12, "description": "Notes folder"}, {"name": "Journal", "constant": 11, "description": "Journal folder"}, ] def parse_folder_path(folder_path: str) -> Dict[str, Any]: """ Parse a folder path string into components. Args: folder_path: The folder path string (e.g., "Inbox/Subfolder" or "mailbox@domain.com/Inbox") Returns: Dict[str, Any]: Parsed path information """ if not folder_path or folder_path.lower() == "inbox": return { "is_default": True, "default_type": "Inbox", "path_parts": [], "mailbox": None } # Check if it's a mailbox-specific path if "/" in folder_path: parts = folder_path.split("/") # Check if first part looks like an email address if "@" in parts[0] and "." in parts[0]: return { "is_default": False, "default_type": None, "path_parts": parts[1:], "mailbox": parts[0] } else: return { "is_default": False, "default_type": None, "path_parts": parts, "mailbox": None } else: # Single folder name return { "is_default": False, "default_type": None, "path_parts": [folder_path], "mailbox": None } def sanitize_folder_name(name: str) -> str: """ Sanitize a folder name for use in Outlook. Args: name: Folder name to sanitize Returns: str: Sanitized folder name """ if not name: return "" # Remove leading/trailing whitespace name = name.strip() # Remove invalid characters for folder names invalid_chars = ['\\', '/', ':', '*', '?', '"', '<', '>', '|'] for char in invalid_chars: name = name.replace(char, '_') return name def convert_com_time_to_string(com_time) -> Optional[str]: """ Convert COM time object to string format. Args: com_time: COM time object Returns: str: Time string in ISO format, or None if conversion fails """ try: if com_time is None: return None # Convert COM time to Python datetime import datetime if hasattr(com_time, 'year') and hasattr(com_time, 'month'): dt = datetime.datetime(com_time.year, com_time.month, com_time.day, com_time.hour, com_time.minute, com_time.second) return dt.isoformat() else: return str(com_time) except Exception as e: logger.warning(f"Failed to convert COM time: {e}") return None class COMObjectWrapper: """ Wrapper class for safe COM object handling. This class provides a context manager for COM objects with automatic cleanup and error handling. """ def __init__(self, com_object: Any): """Initialize with a COM object.""" self.com_object = com_object self._released = False def __enter__(self): """Enter context manager.""" return self def __exit__(self, exc_type, exc_val, exc_tb): """Exit context manager and cleanup.""" self.release() return False # Don't suppress exceptions def release(self): """Release the COM object.""" if not self._released and self.com_object is not None: safe_release_com_object(self.com_object) self._released = True def __getattr__(self, name): """Delegate attribute access to the wrapped COM object.""" if self.com_object is None: raise RuntimeError("COM object has been released") try: return getattr(self.com_object, name) except AttributeError as e: logger.error(f"Attribute '{name}' not found on COM object") raise def __bool__(self): """Check if COM object is valid.""" return self.com_object is not None and not self._released

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/marlonluo2018/outlook-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server