mail: robustes Decoding gegen unbekannte Charsets (x-unknown); OAuth-Token 30 Tage

- _safe_decode() faengt LookupError bei unbekannten/kaputten Mail-Charsets
  (z.B. 'x-unknown') ab, Fallback utf-8 -> latin-1. Verhindert Crash der
  Mail-Suche/Read bei einzelnen kaputt-kodierten Mails.
- common.py: OAuth access_token Lifetime 24h -> 30 Tage (weniger Re-Auth in claude.ai)
- .gitignore: *.before-* Backups ausschliessen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-06-17 09:37:15 +02:00
parent a811e87029
commit 56d92c153b
3 changed files with 21 additions and 5 deletions
+1
View File
@@ -4,3 +4,4 @@ __pycache__/
tokens.json tokens.json
config.json config.json
.active_tokens.json .active_tokens.json
*.before-*
+1 -1
View File
@@ -193,7 +193,7 @@ async def oauth_token(request: Request):
return JSONResponse({"error": "unsupported_grant_type"}, status_code=400) return JSONResponse({"error": "unsupported_grant_type"}, status_code=400)
access_token = secrets.token_urlsafe(48) access_token = secrets.token_urlsafe(48)
expires_in = 86400 expires_in = 2592000 # 30 days
tokens = _load_access_tokens() tokens = _load_access_tokens()
# Cleanup expired # Cleanup expired
now = time.time() now = time.time()
+19 -4
View File
@@ -46,6 +46,21 @@ mcp = FastMCP("Mail", stateless_http=True,
transport_security={"enable_dns_rebinding_protection": False}) transport_security={"enable_dns_rebinding_protection": False})
def _safe_decode(payload, charset):
"""Decode bytes robust gegen unbekannte/kaputte Charsets (z.B. 'x-unknown')."""
if not isinstance(payload, bytes):
return str(payload)
for cs in (charset, "utf-8", "latin-1"):
if not cs:
continue
try:
return payload.decode(cs, errors="replace")
except (LookupError, TypeError):
continue
# Letzter Fallback: latin-1 akzeptiert jedes Byte
return payload.decode("latin-1", errors="replace")
def _decode_hdr(raw): def _decode_hdr(raw):
if not raw: if not raw:
return "" return ""
@@ -53,7 +68,7 @@ def _decode_hdr(raw):
decoded = [] decoded = []
for data, charset in parts: for data, charset in parts:
if isinstance(data, bytes): if isinstance(data, bytes):
decoded.append(data.decode(charset or "utf-8", errors="replace")) decoded.append(_safe_decode(data, charset))
else: else:
decoded.append(str(data)) decoded.append(str(data))
return " ".join(decoded) return " ".join(decoded)
@@ -65,16 +80,16 @@ def _get_body(msg):
if part.get_content_type() == "text/plain": if part.get_content_type() == "text/plain":
payload = part.get_payload(decode=True) payload = part.get_payload(decode=True)
if payload: if payload:
return payload.decode(part.get_content_charset() or "utf-8", errors="replace") return _safe_decode(payload, part.get_content_charset())
for part in msg.walk(): for part in msg.walk():
if part.get_content_type() == "text/html": if part.get_content_type() == "text/html":
payload = part.get_payload(decode=True) payload = part.get_payload(decode=True)
if payload: if payload:
return "[HTML] " + payload.decode(part.get_content_charset() or "utf-8", errors="replace") return "[HTML] " + _safe_decode(payload, part.get_content_charset())
else: else:
payload = msg.get_payload(decode=True) payload = msg.get_payload(decode=True)
if payload: if payload:
return payload.decode(msg.get_content_charset() or "utf-8", errors="replace") return _safe_decode(payload, msg.get_content_charset())
return "" return ""