harden: per-item robustness + deadlines/caps across connectors; layered tests
Mail: _safe_decode + _iter_messages (per-mail try/except) -> eine kaputte Mail (charset x-unknown) bricht die Suche nicht mehr ab, findet so auch uralte Mails. Vollscan + Datum-Sort + MAX_SCAN/DEADLINE_S (Selbstheilung, kein 24h-Haenger mehr). Files: search_files mit SEARCH_DEADLINE_S + SEARCH_MAX_DIRS begrenzt (war unbegrenzte rekursive PROPFIND -> real 35s/449 Calls). Calendar/Contacts: PARSE_DEADLINE_S fuer client-seitiges vobject-Parsen; MAX_RESULTS-Output-Cap in get_events/get_tasks (war 10939 Zeilen bei 6000 Events); search Top-N + Hinweise. Tests: in Schichten getrennt -> Smoke (nightly), Edge + Stress (on-demand). Neu: test_smoke/test_edge/test_stress + conftest-Marker + run_full_tests.sh, nightly laeuft nur Smoke. Kontakt-Foto-Roundtrip-Test, Mail-Edge-Mails (x-unknown, kaputter Header, 2009er Alt-Mail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
936ebc2f56
commit
c9ccb4392b
+66
-16
@@ -8,10 +8,11 @@ import contextlib
|
||||
import imaplib
|
||||
import mailbox
|
||||
import gc
|
||||
import time
|
||||
import ctypes
|
||||
from email.header import decode_header
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
@@ -170,6 +171,32 @@ def _open_folder(acct_path, folder_name):
|
||||
return mailbox.Maildir(path, create=False) if os.path.isdir(path) else None
|
||||
|
||||
|
||||
MAX_SCAN = 2000 # Obergrenze gesammelter Treffer (Speicher-Schutz bei breiten Queries)
|
||||
DEADLINE_S = 120 # Wall-Clock-Limit: Server heilt sich selbst, nie wieder endloser Haenger
|
||||
|
||||
|
||||
def _iter_messages(md):
|
||||
"""Yield (key, msg); ueberspringt unparsbare Mails statt die Suche zu killen."""
|
||||
try:
|
||||
keys = list(md.keys())
|
||||
except Exception:
|
||||
return
|
||||
for key in keys:
|
||||
try:
|
||||
msg = md.get_message(key)
|
||||
except Exception:
|
||||
continue
|
||||
yield key, msg
|
||||
|
||||
|
||||
def _date_sortkey(date_str):
|
||||
try:
|
||||
dt = parsedate_to_datetime(date_str)
|
||||
return dt.timestamp() if dt else 0.0
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_accounts() -> str:
|
||||
"""List all email accounts with their folder count. Call this first to see available accounts."""
|
||||
@@ -209,7 +236,9 @@ def search_mail(
|
||||
if not user:
|
||||
return "Error: not authenticated"
|
||||
query_lower = query.lower()
|
||||
results = []
|
||||
matches = []
|
||||
truncated = False
|
||||
t0 = time.monotonic()
|
||||
for acct_name, acct_path in _discover_accounts(user).items():
|
||||
if account and account not in acct_name:
|
||||
continue
|
||||
@@ -219,21 +248,42 @@ def search_mail(
|
||||
md = _open_folder(acct_path, fld)
|
||||
if not md:
|
||||
continue
|
||||
for key, msg in md.items():
|
||||
subj = _decode_hdr(msg.get("Subject", ""))
|
||||
frm = _decode_hdr(msg.get("From", ""))
|
||||
to = _decode_hdr(msg.get("To", ""))
|
||||
date_str = msg.get("Date", "")
|
||||
if query_lower not in f"{subj} {frm} {to}".lower():
|
||||
body = _get_body(msg)
|
||||
if query_lower not in body.lower():
|
||||
continue
|
||||
results.append(f"[{date_str}] {frm} -> {to}\n Subject: {subj}\n Account: {acct_name}, Folder: {fld}, Key: {key}")
|
||||
if len(results) >= limit:
|
||||
_trim_memory()
|
||||
return "\n\n".join(results)
|
||||
for key, msg in _iter_messages(md):
|
||||
try:
|
||||
subj = _decode_hdr(msg.get("Subject", ""))
|
||||
frm = _decode_hdr(msg.get("From", ""))
|
||||
to = _decode_hdr(msg.get("To", ""))
|
||||
date_str = msg.get("Date", "")
|
||||
if query_lower not in f"{subj} {frm} {to}".lower():
|
||||
if query_lower not in _get_body(msg).lower():
|
||||
continue
|
||||
matches.append((date_str, frm, to, subj, acct_name, fld, key))
|
||||
except Exception:
|
||||
continue
|
||||
if len(matches) >= MAX_SCAN:
|
||||
truncated = True
|
||||
break
|
||||
if time.monotonic() - t0 > DEADLINE_S:
|
||||
truncated = True
|
||||
break
|
||||
if truncated:
|
||||
break
|
||||
if truncated:
|
||||
break
|
||||
matches.sort(key=lambda m: _date_sortkey(m[0]), reverse=True)
|
||||
shown = matches[:limit]
|
||||
_trim_memory()
|
||||
return "\n\n".join(results) if results else "No results found"
|
||||
if not shown:
|
||||
return "No results found"
|
||||
lines = [f"[{d}] {f} -> {t}\n Subject: {s}\n Account: {a}, Folder: {fl}, Key: {k}"
|
||||
for (d, f, t, s, a, fl, k) in shown]
|
||||
more = len(matches) - len(shown)
|
||||
if more > 0:
|
||||
if truncated:
|
||||
lines.append(f"... mindestens {more} weitere Treffer (Scan-Limit erreicht; Suche eingrenzen).")
|
||||
else:
|
||||
lines.append(f"... {more} weitere Treffer (Suche eingrenzen oder limit erhoehen).")
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
||||
Reference in New Issue
Block a user