# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Folder Management helper for Outlook operations. Handles folder ID resolution and folder name lookups. """ import logging import requests from typing import Dict, Any, Optional, Tuple logger = logging.getLogger(__name__) # Microsoft Graph well-known folder path segments (always English in the URL; works for any mailbox UI language). # See https://learn.microsoft.com/en-us/graph/api/resources/mailfolder _graphWellKnownSegments = frozenset( { "inbox", "drafts", "sentitems", "deleteditems", "junkemail", "outbox", "archive", "clutter", "conflicts", "conversationhistory", "msgfolderroot", "recoverableitemsdeletions", "scheduled", "searchfolders", "syncissues", } ) # Map common user/tool labels (any language) -> Graph well-known segment _wellKnownAliases: Tuple[Tuple[str, str], ...] = ( ("inbox", "inbox"), ("posteingang", "inbox"), ("postfach", "inbox"), ("boîte de réception", "inbox"), ("boite de reception", "inbox"), ("drafts", "drafts"), ("draft", "drafts"), ("entwürfe", "drafts"), ("entwurfe", "drafts"), ("brouillons", "drafts"), ("brouillon", "drafts"), ("sent items", "sentitems"), ("sentitems", "sentitems"), ("gesendete elemente", "sentitems"), ("éléments envoyés", "sentitems"), ("elements envoyes", "sentitems"), ("deleted items", "deleteditems"), ("deleteditems", "deleteditems"), ("gelöschte elemente", "deleteditems"), ("geloschte elemente", "deleteditems"), ("éléments supprimés", "deleteditems"), ("junk email", "junkemail"), ("junkemail", "junkemail"), ("junk-e-mail", "junkemail"), ("junk e-mail", "junkemail"), ("courrier indésirable", "junkemail"), ("outbox", "outbox"), ("postausgang", "outbox"), ("out box", "outbox"), ("archive", "archive"), ("archiv", "archive"), ) def _wellKnownSegmentForName(folderName: str) -> Optional[str]: """Return Graph mailFolder segment if folderName is a known default folder alias.""" if not folderName or not str(folderName).strip(): return None key = str(folderName).strip().lower() if key in _graphWellKnownSegments: return key for alias, segment in _wellKnownAliases: if key == alias: return segment return None class FolderManagementHelper: """Helper for folder management operations""" def __init__(self, methodInstance): """ Initialize folder management helper. Args: methodInstance: Instance of MethodOutlook (for access to services) """ self.method = methodInstance self.services = methodInstance.services def getFolderId(self, folder_name: str, connection: Dict[str, Any]) -> Optional[str]: """ Get the folder ID for a given folder name or ID. Returns the input as-is if it already looks like a Microsoft Graph folder ID. """ if not folder_name or not str(folder_name).strip(): return None # Graph folder IDs are base64-like strings (e.g. AQMk...); return as-is s = str(folder_name).strip() if s.startswith("AQMk") and len(s) > 20 and " " not in s: return s try: graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Resolve default folders by Graph well-known name (locale-independent; avoids missing "Inbox" on paginated /mailFolders lists) wk = _wellKnownSegmentForName(folder_name) if wk: wk_url = f"{graph_url}/me/mailFolders/{wk}" wk_resp = requests.get(wk_url, headers=headers) if wk_resp.status_code == 200: wid = wk_resp.json().get("id") if wid: return wid logger.debug( f"Well-known folder '{wk}' lookup failed ({wk_resp.status_code}); falling back to folder list" ) # Get mail folders (first page only; subfolders / pagination may omit Inbox) 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", []) # Try exact match first for folder in all_folders: if folder.get("displayName", "").lower() == folder_name.lower(): 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): 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): 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 def getFolderNameById(self, folder_id: str, connection: Dict[str, Any]) -> str: """ Get the folder display name for a given folder ID """ try: graph_url = "https://graph.microsoft.com/v1.0" headers = { "Authorization": f"Bearer {connection['accessToken']}", "Content-Type": "application/json" } # Get folder by ID 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", folder_id) else: logger.warning(f"Could not retrieve folder name for ID {folder_id}: {response.status_code}") return folder_id except Exception as e: logger.warning(f"Error getting folder name for ID '{folder_id}': {str(e)}") return folder_id