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
+79 -50
View File
@@ -8,10 +8,10 @@ import json
from mcp.server.fastmcp import Context
from owa_mcp.server import mcp, AppContext
from owa_mcp.owa_client import OWAClient, SessionExpiredError
from owa_mcp.server import AppContext, mcp
from owa_mcp.server_lifecycle import require_client
from owa_mcp.utils import html_to_text, extract_links_from_html
from owa_mcp.utils import extract_links_from_html, html_to_text
def _get_client(ctx: Context) -> OWAClient:
@@ -215,7 +215,12 @@ def _get_item_details(client: OWAClient, item_id: str) -> dict:
item_type = item.get("__type", "")
if any(
t in item_type
for t in ("MeetingRequest", "MeetingResponse", "MeetingCancellation", "CalendarItem")
for t in (
"MeetingRequest",
"MeetingResponse",
"MeetingCancellation",
"CalendarItem",
)
):
result["location"] = item.get(
"Location",
@@ -331,9 +336,7 @@ def get_emails(
find_body = {
"__type": "FindItemRequest:#Exchange",
"ItemShape": item_shape,
"ParentFolderIds": [
{"__type": "FolderId:#Exchange", "Id": folder_id}
],
"ParentFolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
"Traversal": "Shallow",
"Paging": {
"__type": "IndexedPageView:#Exchange",
@@ -392,18 +395,19 @@ def get_emails(
if not items:
return json.dumps(
{"item_ids": [], "count": 0} if ids_only
else {"emails": [], "count": 0}
{"item_ids": [], "count": 0} if ids_only else {"emails": [], "count": 0}
)
if ids_only:
result = []
for item in items:
result.append({
"item_id": item.get("ItemId", {}).get("Id", ""),
"date": item.get("DateTimeReceived", ""),
"subject": item.get("Subject", ""),
})
result.append(
{
"item_id": item.get("ItemId", {}).get("Id", ""),
"date": item.get("DateTimeReceived", ""),
"subject": item.get("Subject", ""),
}
)
return json.dumps({"item_ids": result, "count": len(result)})
emails = []
@@ -763,9 +767,7 @@ def move_email(
if not folder_id:
return json.dumps({"error": f"Folder '{target_folder}' not found."})
items = [
{"__type": "ItemId:#Exchange", "Id": iid} for iid in item_ids
]
items = [{"__type": "ItemId:#Exchange", "Id": iid} for iid in item_ids]
payload = {
"__type": "MoveItemJsonRequest:#Exchange",
@@ -824,9 +826,7 @@ def delete_email(
try:
client = _get_client(ctx)
items = [
{"__type": "ItemId:#Exchange", "Id": iid} for iid in item_ids
]
items = [{"__type": "ItemId:#Exchange", "Id": iid} for iid in item_ids]
payload = {
"__type": "DeleteItemJsonRequest:#Exchange",
@@ -887,18 +887,31 @@ def download_attachments(
attachments = details.get("attachments", [])
if not attachments:
return json.dumps({"success": True, "downloaded": [], "count": 0,
"message": "No attachments found."})
return json.dumps(
{
"success": True,
"downloaded": [],
"count": 0,
"message": "No attachments found.",
}
)
# Filter to non-inline file attachments with IDs
file_attachments = [
a for a in attachments
a
for a in attachments
if a.get("attachment_id") and not a.get("is_inline", False)
]
if not file_attachments:
return json.dumps({"success": True, "downloaded": [], "count": 0,
"message": "No downloadable file attachments."})
return json.dumps(
{
"success": True,
"downloaded": [],
"count": 0,
"message": "No downloadable file attachments.",
}
)
os.makedirs(target_folder, exist_ok=True)
@@ -938,17 +951,21 @@ def download_attachments(
with open(filepath, "wb") as f:
f.write(content)
downloaded.append({
"name": filename,
"path": filepath,
"size": len(content),
"content_type": content_type,
})
downloaded.append(
{
"name": filename,
"path": filepath,
"size": len(content),
"content_type": content_type,
}
)
except Exception as e:
errors.append({
"name": att.get("name", "unknown"),
"error": str(e),
})
errors.append(
{
"name": att.get("name", "unknown"),
"error": str(e),
}
)
result = {
"success": len(errors) == 0,
@@ -1021,12 +1038,14 @@ def get_email_links(
links = extract_links_from_html(body_val)
break
return json.dumps({
"item_id": item_id,
"subject": subject,
"links": links,
"count": len(links),
})
return json.dumps(
{
"item_id": item_id,
"subject": subject,
"links": links,
"count": len(links),
}
)
except SessionExpiredError as e:
return json.dumps({"error": str(e)})
@@ -1107,16 +1126,22 @@ async def search_emails(
max_results = max(1, min(max_results, 100))
if search_scope not in ("all", "subject", "body", "from"):
return json.dumps({"error": f"invalid search_scope: {search_scope}", "results": []})
return json.dumps(
{"error": f"invalid search_scope: {search_scope}", "results": []}
)
restriction = _build_search_restriction(query.strip(), search_scope)
if restriction is None:
return json.dumps({"error": f"unsupported search_scope: {search_scope}", "results": []})
return json.dumps(
{"error": f"unsupported search_scope: {search_scope}", "results": []}
)
if folder_id:
parent_folder = [{"__type": "FolderId:#Exchange", "Id": folder_id}]
else:
parent_folder = [{"__type": "DistinguishedFolderId:#Exchange", "Id": "msgfolderroot"}]
parent_folder = [
{"__type": "DistinguishedFolderId:#Exchange", "Id": "msgfolderroot"}
]
payload = {
"__type": "FindItemJsonRequest:#Exchange",
@@ -1153,6 +1178,7 @@ async def search_emails(
body_type = item.get("Body", {}).get("BodyType", "HTML")
if body_type == "HTML" and body_html:
import re
body_preview = re.sub(r"<[^>]+>", " ", body_html)
body_preview = " ".join(body_preview.split())[:200]
else:
@@ -1163,13 +1189,16 @@ async def search_emails(
if len(results) >= max_results:
break
return json.dumps({
"query": query.strip(),
"search_scope": search_scope,
"folder_id": folder_id or "all",
"total_results": len(results),
"results": results,
}, ensure_ascii=False)
return json.dumps(
{
"query": query.strip(),
"search_scope": search_scope,
"folder_id": folder_id or "all",
"total_results": len(results),
"results": results,
},
ensure_ascii=False,
)
except SessionExpiredError as e:
return json.dumps({"error": str(e)})
except Exception as e: