gateway/modules/workflows/methods/methodOutlook/helpers/folderManagement.py
2026-03-28 21:46:55 +01:00

199 lines
7.3 KiB
Python

# 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