From fdf2bc095afc82aaefb6aff7d8b109dd24febe58 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Mar 2026 15:25:29 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20E-Mail=20IMAP=20Client=20=E2=80=94=20Zu?= =?UTF-8?q?sammenfassung,=20Suche,=20Benachrichtigung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homelab-ai-bot/context.py | 43 ++- .../core/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 148 bytes .../core/__pycache__/config.cpython-313.pyc | Bin 0 -> 9547 bytes .../__pycache__/loki_client.cpython-313.pyc | Bin 0 -> 7044 bytes .../__pycache__/mail_client.cpython-313.pyc | Bin 0 -> 13308 bytes .../proxmox_client.cpython-313.pyc | Bin 0 -> 7536 bytes homelab-ai-bot/core/mail_client.py | 244 ++++++++++++++++++ homelab-ai-bot/llm.py | 39 +++ homelab-ai-bot/monitor.py | 11 +- homelab.conf | 6 + 10 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 homelab-ai-bot/core/__pycache__/__init__.cpython-313.pyc create mode 100644 homelab-ai-bot/core/__pycache__/config.cpython-313.pyc create mode 100644 homelab-ai-bot/core/__pycache__/loki_client.cpython-313.pyc create mode 100644 homelab-ai-bot/core/__pycache__/mail_client.cpython-313.pyc create mode 100644 homelab-ai-bot/core/__pycache__/proxmox_client.cpython-313.pyc create mode 100644 homelab-ai-bot/core/mail_client.py diff --git a/homelab-ai-bot/context.py b/homelab-ai-bot/context.py index f7269bed..dd45234d 100644 --- a/homelab-ai-bot/context.py +++ b/homelab-ai-bot/context.py @@ -7,7 +7,7 @@ import re sys.path.insert(0, os.path.dirname(__file__)) from core import config, loki_client, proxmox_client, wordpress_client, prometheus_client -from core import forgejo_client, seafile_client, pbs_client +from core import forgejo_client, seafile_client, pbs_client, mail_client def _load_config(): @@ -184,6 +184,43 @@ def _tool_get_backup_status() -> str: return pbs_client.format_overview() +def _tool_get_mail_summary() -> str: + cfg = _load_config() + mail_client.init(cfg) + return mail_client.format_summary() + + +def _tool_get_mail_count() -> str: + cfg = _load_config() + mail_client.init(cfg) + counts = mail_client.get_mail_count() + if "error" in counts: + return f"Mail-Fehler: {counts['error']}" + return f"E-Mails: {counts['total']} gesamt, {counts['unread']} ungelesen ({counts['account']})" + + +def _tool_search_mail(query: str, days: int = 30) -> str: + cfg = _load_config() + mail_client.init(cfg) + results = mail_client.search_mail(query, days=days) + return mail_client.format_search_results(results) + + +def _tool_get_todays_mails() -> str: + cfg = _load_config() + mail_client.init(cfg) + mails = mail_client.get_todays_mails() + if not mails: + return "Heute keine Mails eingegangen." + if "error" in mails[0]: + return f"Mail-Fehler: {mails[0]['error']}" + lines = [f"{len(mails)} Mail(s) heute:\n"] + for r in mails: + lines.append(f" {r['date_str']} | {r['from'][:35]}") + lines.append(f" → {r['subject'][:70]}") + return "\n".join(lines) + + def _tool_get_feed_stats() -> str: cfg = _load_config() ct_109 = config.get_container(cfg, vmid=109) @@ -221,4 +258,8 @@ def get_tool_handlers() -> dict: "close_issue": lambda number: _tool_close_issue(number), "get_seafile_status": lambda: _tool_get_seafile_status(), "get_backup_status": lambda: _tool_get_backup_status(), + "get_mail_summary": lambda: _tool_get_mail_summary(), + "get_mail_count": lambda: _tool_get_mail_count(), + "search_mail": lambda query, days=30: _tool_search_mail(query, days=days), + "get_todays_mails": lambda: _tool_get_todays_mails(), } diff --git a/homelab-ai-bot/core/__pycache__/__init__.cpython-313.pyc b/homelab-ai-bot/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd239739862688dada85bcede22bce8536505fe5 GIT binary patch literal 148 zcmey&%ge<81Y5$^W-_I|p@<121QNextY4I$U!tFp zpPQPKn53Ijl$eda^ P3}Sp_W@Kb6Vg|ARVcjCs literal 0 HcmV?d00001 diff --git a/homelab-ai-bot/core/__pycache__/config.cpython-313.pyc b/homelab-ai-bot/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88f39297c6d32eb73d1a7b74c4566ddfd3378ed7 GIT binary patch literal 9547 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 literal 0 HcmV?d00001 diff --git a/homelab-ai-bot/core/__pycache__/loki_client.cpython-313.pyc b/homelab-ai-bot/core/__pycache__/loki_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..056e5ba7ad0f92e1b9c64d73ebd51856c79e8ceb GIT binary patch literal 7044 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 aBgpKAvrc^8x{e^b%F^Vq^aoO+yYk<_%N)4? literal 0 HcmV?d00001 diff --git a/homelab-ai-bot/core/__pycache__/mail_client.cpython-313.pyc b/homelab-ai-bot/core/__pycache__/mail_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6fd8722f385716fc85e712e18814a1b7cdd6006 GIT binary patch literal 13308 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 IFY?FwAD%WNWdHyG literal 0 HcmV?d00001 diff --git a/homelab-ai-bot/core/__pycache__/proxmox_client.cpython-313.pyc b/homelab-ai-bot/core/__pycache__/proxmox_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74663dd071900ef0a7f2e3883017b8df60d82a96 GIT binary patch literal 7536 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 literal 0 HcmV?d00001 diff --git a/homelab-ai-bot/core/mail_client.py b/homelab-ai-bot/core/mail_client.py new file mode 100644 index 00000000..db9daf28 --- /dev/null +++ b/homelab-ai-bot/core/mail_client.py @@ -0,0 +1,244 @@ +"""IMAP Mail Client — Liest E-Mails vom All-Inkl Spiegel-Postfach (Read-Only).""" + +import imaplib +import email +from email.header import decode_header +from email.utils import parsedate_to_datetime +from datetime import datetime, timedelta, timezone +from typing import Optional + +IMAP_SERVER = "" +IMAP_PORT = 993 +MAIL_USER = "" +MAIL_PASS = "" + +IMPORTANT_SENDERS = [ + "paypal", "bank", "sparkasse", "postbank", "dkb", + "hetzner", "all-inkl", "kasserver", "cloudflare", + "proxmox", "synology", "tailscale", + "finanzamt", "elster", "bundesnetzagentur", +] + + +def init(cfg): + global IMAP_SERVER, IMAP_PORT, MAIL_USER, MAIL_PASS + IMAP_SERVER = cfg.raw.get("MAIL_IMAP_SERVER", "") + IMAP_PORT = int(cfg.raw.get("MAIL_IMAP_PORT", "993")) + MAIL_USER = cfg.raw.get("MAIL_USER", "") + MAIL_PASS = cfg.raw.get("MAIL_PASS", "") + + +def _connect() -> Optional[imaplib.IMAP4_SSL]: + if not IMAP_SERVER or not MAIL_USER or not MAIL_PASS: + return None + try: + m = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT) + m.login(MAIL_USER, MAIL_PASS) + return m + except Exception: + return None + + +def _decode_header_value(raw: str) -> str: + if not raw: + return "" + parts = decode_header(raw) + decoded = "" + for part, enc in parts: + if isinstance(part, bytes): + decoded += part.decode(enc or "utf-8", errors="replace") + else: + decoded += part + return decoded.strip() + + +def _parse_mail(msg_data) -> Optional[dict]: + try: + raw = msg_data[0][1] + msg = email.message_from_bytes(raw) + subj = _decode_header_value(msg.get("Subject", "")) + frm = _decode_header_value(msg.get("From", "")) + date_str = msg.get("Date", "") + try: + dt = parsedate_to_datetime(date_str) + except Exception: + dt = None + return { + "subject": subj[:120], + "from": frm[:80], + "date": dt, + "date_str": dt.strftime("%d.%m.%Y %H:%M") if dt else date_str[:20], + } + except Exception: + return None + + +def get_mail_count() -> dict: + """Anzahl Mails total und ungelesen.""" + m = _connect() + if not m: + return {"error": "IMAP-Verbindung fehlgeschlagen"} + try: + m.select("INBOX", readonly=True) + _, data = m.search(None, "ALL") + total = len(data[0].split()) if data[0] else 0 + _, data = m.search(None, "UNSEEN") + unread = len(data[0].split()) if data[0] else 0 + return {"total": total, "unread": unread, "account": MAIL_USER} + except Exception as e: + return {"error": str(e)} + finally: + m.logout() + + +def get_recent_mails(count: int = 10) -> list[dict]: + """Letzte N Mails (neueste zuerst).""" + m = _connect() + if not m: + return [{"error": "IMAP-Verbindung fehlgeschlagen"}] + try: + m.select("INBOX", readonly=True) + _, data = m.search(None, "ALL") + ids = data[0].split() if data[0] else [] + if not ids: + return [] + recent_ids = ids[-count:] + recent_ids.reverse() + results = [] + for mid in recent_ids: + _, msg_data = m.fetch(mid, "(BODY.PEEK[HEADER])") + parsed = _parse_mail(msg_data) + if parsed: + results.append(parsed) + return results + except Exception as e: + return [{"error": str(e)}] + finally: + m.logout() + + +def get_todays_mails() -> list[dict]: + """Alle Mails von heute.""" + m = _connect() + if not m: + return [{"error": "IMAP-Verbindung fehlgeschlagen"}] + try: + m.select("INBOX", readonly=True) + today = datetime.now().strftime("%d-%b-%Y") + _, data = m.search(None, f'(SINCE "{today}")') + ids = data[0].split() if data[0] else [] + results = [] + for mid in ids: + _, msg_data = m.fetch(mid, "(BODY.PEEK[HEADER])") + parsed = _parse_mail(msg_data) + if parsed: + results.append(parsed) + results.reverse() + return results + except Exception as e: + return [{"error": str(e)}] + finally: + m.logout() + + +def search_mail(query: str, days: int = 30, limit: int = 15) -> list[dict]: + """Suche nach Mails per Absender oder Betreff.""" + m = _connect() + if not m: + return [{"error": "IMAP-Verbindung fehlgeschlagen"}] + try: + m.select("INBOX", readonly=True) + since = (datetime.now() - timedelta(days=days)).strftime("%d-%b-%Y") + + all_results = [] + for criteria in [f'FROM "{query}"', f'SUBJECT "{query}"']: + try: + _, data = m.search(None, f'(SINCE "{since}" {criteria})') + ids = data[0].split() if data[0] else [] + for mid in ids: + _, msg_data = m.fetch(mid, "(BODY.PEEK[HEADER])") + parsed = _parse_mail(msg_data) + if parsed: + all_results.append(parsed) + except Exception: + continue + + seen = set() + unique = [] + for r in all_results: + key = f"{r['date_str']}|{r['subject'][:40]}" + if key not in seen: + seen.add(key) + unique.append(r) + unique.sort(key=lambda x: x.get("date") or datetime.min.replace(tzinfo=timezone.utc), reverse=True) + return unique[:limit] + except Exception as e: + return [{"error": str(e)}] + finally: + m.logout() + + +def get_important_mails(hours: int = 24) -> list[dict]: + """Mails von wichtigen Absendern (Bank, Hoster, etc.).""" + m = _connect() + if not m: + return [{"error": "IMAP-Verbindung fehlgeschlagen"}] + try: + m.select("INBOX", readonly=True) + since = (datetime.now() - timedelta(hours=hours)).strftime("%d-%b-%Y") + _, data = m.search(None, f'(SINCE "{since}")') + ids = data[0].split() if data[0] else [] + results = [] + for mid in ids: + _, msg_data = m.fetch(mid, "(BODY.PEEK[HEADER])") + parsed = _parse_mail(msg_data) + if parsed: + frm_lower = parsed["from"].lower() + if any(s in frm_lower for s in IMPORTANT_SENDERS): + results.append(parsed) + results.reverse() + return results + except Exception as e: + return [{"error": str(e)}] + finally: + m.logout() + + +def format_summary() -> str: + """Komplett-Übersicht: Counts + letzte Mails + wichtige.""" + counts = get_mail_count() + if "error" in counts: + return f"Mail-Fehler: {counts['error']}" + + lines = [f"E-Mail ({counts['account']})"] + lines.append(f" Gesamt: {counts['total']}, Ungelesen: {counts['unread']}\n") + + recent = get_recent_mails(5) + if recent and "error" not in recent[0]: + lines.append("Letzte 5 Mails:") + for r in recent: + lines.append(f" {r['date_str']} | {r['from'][:30]}") + lines.append(f" → {r['subject'][:70]}") + + important = get_important_mails(48) + if important and "error" not in important[0]: + lines.append(f"\nWichtige Mails (48h): {len(important)}") + for r in important: + lines.append(f" {r['date_str']} | {r['from'][:30]}") + lines.append(f" → {r['subject'][:70]}") + else: + lines.append("\nKeine wichtigen Mails in den letzten 48h.") + + return "\n".join(lines) + + +def format_search_results(results: list[dict]) -> str: + if not results: + return "Keine Mails gefunden." + if "error" in results[0]: + return f"Suche fehlgeschlagen: {results[0]['error']}" + lines = [f"{len(results)} Mail(s) gefunden:\n"] + for r in results: + lines.append(f" {r['date_str']} | {r['from'][:35]}") + lines.append(f" → {r['subject'][:70]}") + return "\n".join(lines) diff --git a/homelab-ai-bot/llm.py b/homelab-ai-bot/llm.py index 3efe0a03..80d23153 100644 --- a/homelab-ai-bot/llm.py +++ b/homelab-ai-bot/llm.py @@ -175,6 +175,45 @@ TOOLS = [ "parameters": {"type": "object", "properties": {}, "required": []}, }, }, + { + "type": "function", + "function": { + "name": "get_mail_summary", + "description": "E-Mail Übersicht: Anzahl Mails, ungelesene, letzte Mails, wichtige Absender (Bank, Hoster, etc.)", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, + { + "type": "function", + "function": { + "name": "get_mail_count", + "description": "Anzahl E-Mails (gesamt und ungelesen)", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, + { + "type": "function", + "function": { + "name": "search_mail", + "description": "E-Mails durchsuchen nach Absender oder Betreff", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Suchbegriff (Absender oder Betreff, z.B. 'PayPal', 'Rechnung', 'Hetzner')"}, + "days": {"type": "integer", "description": "Zeitraum in Tagen (default: 30)", "default": 30}, + }, + "required": ["query"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_todays_mails", + "description": "Alle E-Mails von heute", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, ] diff --git a/homelab-ai-bot/monitor.py b/homelab-ai-bot/monitor.py index aaa2c982..bc02a4cd 100644 --- a/homelab-ai-bot/monitor.py +++ b/homelab-ai-bot/monitor.py @@ -5,7 +5,7 @@ import os import requests sys.path.insert(0, os.path.dirname(__file__)) -from core import config, loki_client, proxmox_client +from core import config, loki_client, proxmox_client, mail_client def _get_tokens(cfg): @@ -77,6 +77,15 @@ def check_all() -> list[str]: if names: alerts.append(f"⚠️ Keine Logs seit 35+ Min: {', '.join(names)}") + try: + mail_client.init(cfg) + important = mail_client.get_important_mails(hours=1) + if important and "error" not in important[0]: + senders = [m["from"][:30] for m in important] + alerts.append(f"📧 {len(important)} wichtige Mail(s) (letzte Stunde): {', '.join(senders)}") + except Exception: + pass + return alerts diff --git a/homelab.conf b/homelab.conf index 35530207..97f1fb0d 100644 --- a/homelab.conf +++ b/homelab.conf @@ -163,6 +163,12 @@ MCP_PATH="/root/homelab-mcp" MCP_VENV="/root/homelab-mcp/.venv" MCP_TOOLS="homelab_overview,homelab_all_containers,homelab_container_status,homelab_query_logs,homelab_get_errors,homelab_check_silence,homelab_host_health,homelab_metrics,homelab_get_config,homelab_loki_labels,homelab_prometheus_targets" +# --- E-MAIL (All-Inkl IMAP-Spiegel von GMX) --- +MAIL_IMAP_SERVER="w0206aa8.kasserver.com" +MAIL_IMAP_PORT="993" +MAIL_USER="info@orbitalo.info" +MAIL_PASS="Astral-66" + # --- LOKI --- LOKI_URL="http://100.109.206.43:3100" LOKI_CT="110"