This commit is contained in:
2026-07-06 02:05:16 +03:00
parent 40dea9a2c2
commit 4207b38b16
14 changed files with 823 additions and 607 deletions
+20 -24
View File
@@ -10,14 +10,15 @@ mirroring what the OWA web frontend sends.
import json
import time
from urllib.parse import quote, urlparse
from pathlib import Path
from urllib.parse import quote, urlparse
import requests
class SessionExpiredError(Exception):
"""Raised when the OWA session has expired (HTTP 401/440 or HTML redirect)."""
pass
@@ -53,14 +54,11 @@ class OWAClient:
owa_url: str | None = None,
):
self.cookie_file = Path(
cookie_file
or str(Path(__file__).parent.parent / "session-cookies.txt")
cookie_file or str(Path(__file__).parent.parent / "session-cookies.txt")
)
self.owa_url = (owa_url or "").rstrip("/")
if not self.owa_url:
raise ValueError(
"OWA URL must be provided via --owa-url or owa_url=…"
)
raise ValueError("OWA URL must be provided via --owa-url or owa_url=…")
self._session = requests.Session()
self._loaded = False
self.user_email: str = ""
@@ -81,9 +79,10 @@ class OWAClient:
"""
now_ms = int(time.time() * 1000)
client_req_id = f"{self._client_id}_{now_ms}"
now_str = time.strftime(
"%Y-%m-%dT%H:%M:%S.", time.gmtime(now_ms / 1000)
) + f"{now_ms % 1000:03d}"
now_str = (
time.strftime("%Y-%m-%dT%H:%M:%S.", time.gmtime(now_ms / 1000))
+ f"{now_ms % 1000:03d}"
)
return {
# Basic
"accept": "*/*",
@@ -129,9 +128,7 @@ class OWAClient:
@property
def _client_id(self) -> str:
"""Extract ClientId from session cookies."""
cid = next(
(c.value for c in self._session.cookies if c.name == "ClientId"), ""
)
cid = next((c.value for c in self._session.cookies if c.name == "ClientId"), "")
return cid if cid else "00000000000000000000000000000000"
# ------------------------------------------------------------------
@@ -153,7 +150,9 @@ class OWAClient:
cookies[name] = value
if not cookies:
raise SessionExpiredError("Cookie file is empty. Use the login MCP tool to authenticate.")
raise SessionExpiredError(
"Cookie file is empty. Use the login MCP tool to authenticate."
)
self._session = requests.Session()
self._session.cookies.update(cookies)
@@ -229,12 +228,11 @@ class OWAClient:
set_cookie = resp.headers.get("Set-Cookie", "")
if "X-OWA-CANARY=" in set_cookie:
import re
m = re.search(r'X-OWA-CANARY=([^;]+)', set_cookie)
m = re.search(r"X-OWA-CANARY=([^;]+)", set_cookie)
if m:
host = urlparse(self.owa_url).hostname or "owa.b1.ru"
self._session.cookies.set(
"X-OWA-CANARY", m.group(1), domain=host
)
self._session.cookies.set("X-OWA-CANARY", m.group(1), domain=host)
# Persist any cookie changes
self._save_cookies()
except Exception:
@@ -273,7 +271,9 @@ class OWAClient:
# Detect session expiry
if resp.status_code in (401, 440):
raise SessionExpiredError("Session expired (HTTP {}).".format(resp.status_code))
raise SessionExpiredError(
f"Session expired (HTTP {resp.status_code})."
)
if "text/html" in resp.headers.get("Content-Type", ""):
body_snippet = resp.text[:300] if resp.text else ""
@@ -335,9 +335,7 @@ class OWAClient:
raise SessionExpiredError(f"Download failed: {exc}") from exc
if resp.status_code in (401, 440):
raise SessionExpiredError(
f"Session expired (HTTP {resp.status_code})."
)
raise SessionExpiredError(f"Session expired (HTTP {resp.status_code}).")
if "text/html" in resp.headers.get("Content-Type", ""):
raise SessionExpiredError(
@@ -469,9 +467,7 @@ class OWAClient:
# ResolveNames (directory search / attendee resolution)
# ------------------------------------------------------------------
def resolve_names(
self, query: str, *, full_contact: bool = True
) -> list[dict]:
def resolve_names(self, query: str, *, full_contact: bool = True) -> list[dict]:
"""Call ResolveNames to search the directory.
Returns the list of Resolution dicts from the API, each containing