From 488612a95d4fc983eb97cc6a59278a4157807fae Mon Sep 17 00:00:00 2001 From: Hans-Christian Payer Date: Sun, 31 May 2026 09:27:40 +0200 Subject: [PATCH] =?UTF-8?q?Nur=20mails=20mit=20ics=20an=C3=A4ngen=20werden?= =?UTF-8?q?=20als=20gelesen=20markiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 166 +++++++++++++++------------- ics_importer.log | 10 ++ ics_mail_importer_env_v2.py | 210 ++++++++++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+), 74 deletions(-) create mode 100644 ics_mail_importer_env_v2.py diff --git a/README.md b/README.md index aaba766..2db81ee 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,28 @@ # ICS-Importer für mailbox.org unter Linux -Dieses Projekt durchsucht ein mailbox.org-Postfach per IMAP nach E-Mails mit `.ics`-Anhängen und importiert enthaltene Termine automatisch per CalDAV in einen mailbox.org-Kalender.[cite:55][cite:45] +Dieses Projekt durchsucht ein mailbox.org-Postfach per IMAP nach E-Mails mit `.ics`-Anhängen und importiert enthaltene Termine automatisch per CalDAV in einen mailbox.org-Kalender.[cite:45][cite:55] ## Überblick -Der technische Ablauf ist bewusst einfach gehalten: +Der Importer ist für ein einfaches, robustes Linux-Setup gedacht: -1. Das Python-Skript verbindet sich per IMAP mit mailbox.org und durchsucht einen Ordner, standardmäßig `INBOX`.[cite:56][cite:55] -2. Es erkennt `.ics`-Anhänge, berechnet einen Hash als Duplikat-Schutz und verarbeitet nur neue Anhänge. -3. Die Termine werden per CalDAV in den gewünschten mailbox.org-Kalender geschrieben, dessen URL in mailbox.org über die Kalendereigenschaften ermittelt wird.[cite:45][cite:51] -4. Die Ausführung erfolgt manuell oder stündlich per Cron. +1. Das Skript verbindet sich per IMAP mit mailbox.org und durchsucht einen Ordner, standardmäßig `INBOX`.[cite:55][cite:338] +2. Nachrichten werden mit `BODY.PEEK[]` gelesen, damit sie beim Abruf nicht automatisch als gelesen markiert werden.[cite:338][cite:350] +3. `.ics`-Anhänge werden erkannt, per Hash gegen Doppelverarbeitung abgesichert und anschließend per CalDAV importiert.[cite:45][cite:162] +4. Nur Mails mit mindestens einem **erfolgreich importierten** ICS-Anhang werden optional als gelesen markiert; fehlgeschlagene oder irrelevante Mails bleiben ungelesen.[cite:340][cite:357] + +## Projektstruktur + +```text +ics-importer/ +├── ics_mail_importer_env_v2.py +├── .env.example +├── .env +├── imported_uids.txt +├── ics_importer.log +├── cron.log +└── README.md +``` ## Voraussetzungen @@ -19,37 +32,22 @@ Benötigt werden: - Python 3 - `pipenv` - Ein mailbox.org-Konto -- Zugriff auf IMAP und CalDAV -- Bei aktiver Zwei-Faktor-Authentifizierung ein App- bzw. Applikationspasswort für externe Anwendungen.[cite:63][cite:45] - -## Projektstruktur - -```text -ics-importer/ -├── ics_mail_importer_env.py -├── .env.example -├── .env -├── imported_uids.txt -├── ics_importer.log -├── cron.log -└── README.md -``` +- IMAP- und CalDAV-Zugang +- Bei aktiver Zwei-Faktor-Authentifizierung ein App- oder Applikationspasswort für externe Anwendungen.[cite:45][cite:55] ## Installation -Projektverzeichnis anlegen und Abhängigkeiten installieren: - ```bash mkdir -p ~/ics-importer cd ~/ics-importer pipenv install python-dotenv caldav icalendar ``` -`python-dotenv` liest Schlüssel-Wert-Paare aus einer `.env`-Datei und stellt sie als Umgebungsvariablen bereit.[cite:162][cite:164] +`python-dotenv` liest Schlüssel-Wert-Paare aus einer `.env`-Datei und stellt sie dem Skript als Umgebungsvariablen bereit.[cite:162][cite:164] ## Konfiguration mit `.env` -Eine `.env.example` kann als Vorlage verwendet werden: +Beispiel für `.env.example`: ```dotenv IMAP_HOST=imap.mailbox.org @@ -58,18 +56,18 @@ IMAP_USERNAME=ihr-name@mailbox.org IMAP_PASSWORD=IHR_PASSWORT_ODER_APP_PASSWORT IMAP_FOLDER=INBOX IMAP_UNSEEN_ONLY=true -IMAP_MARK_AS_READ=false +IMAP_MARK_AS_READ=true CALDAV_URL=https://dav.mailbox.org/caldav/IHR_KALENDER_ID CALDAV_USERNAME=ihr-name@mailbox.org CALDAV_PASSWORD=IHR_PASSWORT_ODER_APP_PASSWORT ``` -Die IMAP-Standardwerte für mailbox.org sind `imap.mailbox.org`, Port `993`, SSL/TLS und die vollständige E-Mail-Adresse als Benutzername.[cite:56][cite:55] +Die IMAP-Standardwerte für mailbox.org sind `imap.mailbox.org` und Port `993`; die vollständige mailbox.org-E-Mail-Adresse wird als Benutzername verwendet.[cite:55] -Die vollständige CalDAV-URL des Zielkalenders wird in mailbox.org im Kalender über **Eigenschaften** angezeigt; für Thunderbird und andere Clients wird `https://dav.mailbox.org/caldav/XXX` verwendet, wobei `XXX` die individuelle Kalender-ID ist.[cite:45][cite:51] +Die vollständige CalDAV-URL des Zielkalenders wird in mailbox.org im Kalender über **Eigenschaften** angezeigt; mailbox.org verwendet dafür Adressen im Format `https://dav.mailbox.org/caldav/XXX`.[cite:45][cite:355] -`.env` lokal anlegen und schützen: +Datei anlegen und schützen: ```bash cp .env.example .env @@ -80,65 +78,81 @@ nano .env ## Bedeutung der Variablen | Variable | Bedeutung | -| --- | --- | -| `IMAP_HOST` | mailbox.org IMAP-Server, normalerweise `imap.mailbox.org`.[cite:56] | -| `IMAP_PORT` | IMAP-SSL-Port, normalerweise `993`.[cite:56] | +|---|---| +| `IMAP_HOST` | mailbox.org IMAP-Server, normalerweise `imap.mailbox.org`.[cite:55] | +| `IMAP_PORT` | IMAP über SSL/TLS, normalerweise `993`.[cite:55] | | `IMAP_USERNAME` | mailbox.org-E-Mail-Adresse.[cite:55] | -| `IMAP_PASSWORD` | Passwort oder E-Mail-App-Passwort bei aktiver 2FA.[cite:63] | -| `IMAP_FOLDER` | Zu durchsuchender Ordner, meist `INBOX`. | -| `IMAP_UNSEEN_ONLY` | Wenn `true`, werden nur ungelesene Mails geprüft. | -| `IMAP_MARK_AS_READ` | Wenn `true`, werden verarbeitete Mails als gelesen markiert. | +| `IMAP_PASSWORD` | Passwort oder App-Passwort. | +| `IMAP_FOLDER` | Zu prüfender Ordner, meist `INBOX`. | +| `IMAP_UNSEEN_ONLY` | Wenn `true`, werden nur ungelesene Nachrichten gesucht. | +| `IMAP_MARK_AS_READ` | Wenn `true`, werden nur erfolgreich verarbeitete ICS-Mails als gelesen markiert. | | `CALDAV_URL` | Vollständige CalDAV-URL des Zielkalenders.[cite:45] | | `CALDAV_USERNAME` | mailbox.org-E-Mail-Adresse als CalDAV-Benutzername.[cite:45] | -| `CALDAV_PASSWORD` | Passwort oder Applikationspasswort bei aktiver 2FA.[cite:63][cite:45] | +| `CALDAV_PASSWORD` | Passwort oder Applikationspasswort. | + +## Verhalten beim Markieren als gelesen + +Die aktuelle Skriptversion ist bewusst so gebaut, dass **nicht alle geprüften Mails** als gelesen markiert werden. Stattdessen gilt: + +- Mails ohne ICS-Anhang bleiben unverändert. +- Mails mit ICS-Anhang bleiben ungelesen, wenn kein Termin erfolgreich importiert wurde. +- Nur Mails mit mindestens einem erfolgreich importierten ICS-Inhalt werden per `UID STORE` mit `\\Seen` markiert, sofern `IMAP_MARK_AS_READ=true` gesetzt ist.[cite:353][cite:357][cite:340] + +Das Abrufen über `BODY.PEEK[]` verhindert, dass der Fetch selbst schon den `Seen`-Status setzt.[cite:350][cite:338] ## Python-Skript -Das Skript `ics_mail_importer_env.py` nutzt `python-dotenv`, `imaplib`, `email`, `icalendar` und `caldav`. `python-dotenv` ist speziell dafür gedacht, Werte aus einer `.env`-Datei zu laden und als Umgebungsvariablen verfügbar zu machen.[cite:162][cite:164] +Das Hauptskript heißt `ics_mail_importer_env_v2.py`. Es nutzt: -Beispielhafter Programmstart: +- `python-dotenv` für `.env` +- `imaplib` für IMAP +- `email` zum Parsen von Nachrichten und Anhängen +- `icalendar` zum Parsen von ICS-Dateien +- `caldav` für den Kalendereintrag auf mailbox.org.[cite:162][cite:338] + +Beispielstart: ```bash cd ~/ics-importer -pipenv run python3 ics_mail_importer_env.py +pipenv run python3 ics_mail_importer_env_v2.py ``` -Das Skript erzeugt oder nutzt dabei unter anderem diese Dateien: - -- `ics_importer.log` für die Programmausgabe -- `imported_uids.txt` für den Duplikat-Schutz -- optional `cron.log`, wenn die Cron-Ausgabe dorthin umgeleitet wird - ## Erster Testlauf -Vor dem Cron-Einsatz sollte das Skript immer einmal manuell gestartet werden: +Vor dem Einsatz mit Cron sollte das Skript immer einmal manuell getestet werden: ```bash -pipenv run python3 ics_mail_importer_env.py +pipenv run python3 ics_mail_importer_env_v2.py ``` -Wenn die Zugangsdaten korrekt sind, verbindet sich das Skript mit IMAP, liest passende Anhänge und importiert neue Termine in den angegebenen CalDAV-Kalender.[cite:55][cite:45] +Dabei sollte geprüft werden: + +- IMAP-Anmeldung funktioniert +- der richtige Ordner wird gelesen +- ICS-Anhänge werden erkannt +- Termine landen im gewünschten mailbox.org-Kalender +- nur erfolgreich verarbeitete ICS-Mails werden als gelesen markiert ## Cron-Einrichtung -Den Python-Pfad des Pipenv-Umfelds ermitteln: +Interpreter-Pfad aus `pipenv` ermitteln: ```bash cd ~/ics-importer pipenv --py ``` -Stündlicher Cron-Eintrag: +Beispiel für stündliche Ausführung: ```cron -0 * * * * /home/hans/.local/share/virtualenvs/ics-importer-wOz4rK-o/bin/python /home/hans/ics-importer/ics_mail_importer_env.py >> /home/hans/ics-importer/cron.log 2>&1 +0 * * * * /home/hans/.local/share/virtualenvs/ics-importer-wOz4rK-o/bin/python /home/hans/ics-importer/ics_mail_importer_env_v2.py >> /home/hans/ics-importer/cron.log 2>&1 ``` -Die User-Crontab wird mit `crontab -e` gepflegt; die direkte Bearbeitung der Crontab-Dateien ist nicht empfehlenswert. Für stündliche Jobs sind sowohl `0 * * * *` als auch `@hourly` übliche Varianten.[cite:321][cite:55] +Die User-Crontab wird mit `crontab -e` gepflegt. Für stündliche Jobs sind `0 * * * *` oder `@hourly` die üblichen Varianten. ## Logrotation -Wenn `cron.log` mitgeschrieben wird, sollte die Datei per `logrotate` rotiert werden. Eine typische Konfiguration unter `/etc/logrotate.d/ics-importer` sieht so aus: +Wenn `cron.log` verwendet wird, sollte die Datei per `logrotate` rotiert werden. Beispiel für `/etc/logrotate.d/ics-importer`: ```conf /home/hans/ics-importer/cron.log { @@ -152,32 +166,32 @@ Wenn `cron.log` mitgeschrieben wird, sollte die Datei per `logrotate` rotiert we } ``` -Damit wird das Log wöchentlich rotiert, vier Versionen bleiben erhalten und ältere Logs werden komprimiert. +Diese Konfiguration rotiert wöchentlich, hält vier alte Versionen vor und komprimiert ältere Logs. -## mailbox.org in Thunderbird +## Thunderbird und mailbox.org -Für das eigentliche Projekt ist Thunderbird nicht zwingend nötig, aber mailbox.org empfiehlt für E-Mail in Thunderbird IMAP und für Kalender CalDAV.[cite:55][cite:45] +Thunderbird ist für dieses Projekt nicht zwingend nötig, aber mailbox.org empfiehlt IMAP für Mail und CalDAV für Kalender.[cite:45][cite:55] ### IMAP in Thunderbird -Thunderbird erkennt mailbox.org in der Regel automatisch; empfohlen wird IMAP statt POP3.[cite:55][cite:63] +Thunderbird erkennt mailbox.org häufig automatisch; IMAP ist der empfohlene Modus.[cite:55] ### CalDAV in Thunderbird -Kalender lassen sich in Thunderbird über **Datei → Neu → Kalender → Im Netzwerk → CalDAV** einbinden. Als URL dient die vollständige mailbox.org-CalDAV-Adresse des Kalenders.[cite:45][cite:51] +Kalender lassen sich in Thunderbird über **Datei → Neu → Kalender → Im Netzwerk → CalDAV** einbinden. Die vollständige CalDAV-Adresse ist in mailbox.org beim Kalender über **Eigenschaften** sichtbar.[cite:45][cite:355] -In manchen Setups muss in Thunderbird zusätzlich `calendar.network.multirealm` auf `true` gesetzt werden, damit die Authentifizierung sauber funktioniert.[cite:45][cite:51] +Falls erforderlich, kann in Thunderbird zusätzlich `calendar.network.multirealm=true` gesetzt werden, um Authentifizierungsprobleme zu vermeiden.[cite:45][cite:64] ## Sicherheit -`.env` enthält Zugangsdaten im Klartext und darf nicht in ein öffentliches Repository eingecheckt werden. `python-dotenv` ist für lokale Konfigurationsdateien gedacht, ersetzt aber kein Secret-Management-System.[cite:162][cite:164] +`.env` enthält Zugangsdaten im Klartext und darf nicht öffentlich geteilt oder in Git eingecheckt werden. `python-dotenv` ist für lokale Konfiguration gedacht, ersetzt aber kein zentrales Secret-Management.[cite:162][cite:164] Empfehlungen: - `.env` nur lokal speichern - `chmod 600 .env` setzen - `.env` in `.gitignore` eintragen -- Bei aktiver 2FA App-/Applikationspasswörter statt des Hauptpassworts verwenden.[cite:63][cite:45] +- bei aktiver 2FA App-/Applikationspasswörter verwenden.[cite:45][cite:55] ## Fehlersuche @@ -188,25 +202,29 @@ Prüfen: - `IMAP_HOST=imap.mailbox.org` - `IMAP_PORT=993` - vollständige E-Mail-Adresse als Benutzername -- korrektes Passwort oder App-Passwort bei aktiver 2FA.[cite:56][cite:63] +- App-Passwort bei aktiver 2FA +- richtiger Ordner in `IMAP_FOLDER`.[cite:55] ### CalDAV funktioniert nicht Prüfen: -- vollständige CalDAV-URL aus den Kalendereigenschaften in mailbox.org -- korrekter Benutzername -- korrektes Passwort bzw. Applikationspasswort bei aktiver 2FA.[cite:45][cite:51] +- vollständige CalDAV-URL aus den Kalendereigenschaften +- richtiger Benutzername +- Passwort oder Applikationspasswort +- Schreibrechte auf den Zielkalender.[cite:45][cite:355] -### Cron führt das Skript nicht aus +### Mail wird zu früh als gelesen markiert -Prüfen: - -- absoluter Python-Pfad aus `pipenv --py` -- absoluter Skriptpfad -- Schreibrechte für `cron.log` -- manueller Testlauf funktioniert bereits +Die Version `ics_mail_importer_env_v2.py` verwendet `BODY.PEEK[]`, damit das bloße Abrufen den `Seen`-Status nicht verändert. Das Lesen-Markieren erfolgt erst danach gezielt per `UID STORE`, und nur wenn der Import erfolgreich war.[cite:338][cite:350][cite:357] ## Empfohlener Betriebsmodus -Für die meisten Setups ist `IMAP_UNSEEN_ONLY=true` sinnvoll, damit nur neue bzw. ungelesene Mails geprüft werden. In Verbindung mit dem Hash-basierten Duplikat-Schutz verhindert das unnötige Doppelimporte und reduziert die Last auf dem Postfach. \ No newline at end of file +Für die meisten Setups ist diese Kombination sinnvoll: + +- `IMAP_UNSEEN_ONLY=true` +- `IMAP_MARK_AS_READ=true` +- stündliche Ausführung per Cron +- Duplikat-Schutz über `imported_uids.txt` + +Damit werden nur neue Nachrichten geprüft, unnötige Doppelimporte vermieden und nur tatsächlich erfolgreich verarbeitete ICS-Mails als gelesen markiert. diff --git a/ics_importer.log b/ics_importer.log index 7a19b9a..3b8c311 100644 --- a/ics_importer.log +++ b/ics_importer.log @@ -28,3 +28,13 @@ 2026-05-31 09:12:17,331 [INFO] 0 .ics-Anhang/-Anhänge gefunden. 2026-05-31 09:12:17,331 [INFO] Fertig. 0 Termine insgesamt importiert. 2026-05-31 09:12:17,331 [INFO] ============================================================ +2026-05-31 09:24:11,380 [INFO] ============================================================ +2026-05-31 09:24:11,381 [INFO] ICS-Importer gestartet (2026-05-31T07:24:11.381101+00:00) +2026-05-31 09:24:11,381 [INFO] Verbinde mit IMAP imap.mailbox.org:993 als minitux@mailbox.org ... +2026-05-31 09:24:11,719 [INFO] 3 Nachricht(en) zur Prüfung gefunden. +2026-05-31 09:24:11,980 [INFO] Prüfe Mail UID 2781 – Betreff: Bilder Filter +2026-05-31 09:24:12,015 [INFO] Prüfe Mail UID 2782 – Betreff: AW: Bilder Filter +2026-05-31 09:24:12,050 [INFO] Prüfe Mail UID 2804 – Betreff: =?UTF-8?Q?Kurze_Frage_zu_easycosmetic=E2=84=A2?= +2026-05-31 09:24:12,116 [INFO] Fertig. 0 Termin(e) importiert. +2026-05-31 09:24:12,116 [INFO] 0 Mail(s) als gelesen markiert. +2026-05-31 09:24:12,116 [INFO] ============================================================ diff --git a/ics_mail_importer_env_v2.py b/ics_mail_importer_env_v2.py new file mode 100644 index 0000000..e31ca74 --- /dev/null +++ b/ics_mail_importer_env_v2.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +""" +ics_mail_importer_env_v2.py +--------------------------- +Sucht in einem mailbox.org IMAP-Postfach nach E-Mails mit .ics-Anhängen +und importiert die enthaltenen Termine automatisch in einen CalDAV-Kalender. + +Wichtige Eigenschaften: +- Konfiguration über .env-Datei mit python-dotenv +- Duplikat-Schutz über Hash der ICS-Anhänge +- E-Mails werden beim Abrufen nicht automatisch als gelesen markiert +- Nur E-Mails mit erfolgreich verarbeitetem ICS-Anhang werden optional als gelesen markiert +""" + +import imaplib +import email +import logging +import hashlib +import os +from pathlib import Path +from datetime import datetime, timezone + +from dotenv import load_dotenv +import caldav +from icalendar import Calendar + +BASE_DIR = Path(__file__).resolve().parent +load_dotenv(BASE_DIR / ".env") + +LOG_FILE = BASE_DIR / "ics_importer.log" +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()], +) +log = logging.getLogger(__name__) + +SEEN_FILE = BASE_DIR / "imported_uids.txt" + + +def load_seen_uids(): + if not SEEN_FILE.exists(): + return set() + return set(SEEN_FILE.read_text().splitlines()) + + +def save_uid(uid: str): + with SEEN_FILE.open("a") as f: + f.write(uid + "\n") + + +def env_bool(name: str, default: str = "false") -> bool: + return os.getenv(name, default).strip().lower() in {"1", "true", "yes", "on"} + + +def import_ics_to_caldav(ics_bytes: bytes, uid_hash: str) -> int: + caldav_url = os.getenv("CALDAV_URL") + username = os.getenv("CALDAV_USERNAME") + password = os.getenv("CALDAV_PASSWORD") + + if not caldav_url or not username or not password: + raise RuntimeError( + "CALDAV_URL, CALDAV_USERNAME oder CALDAV_PASSWORD fehlt in .env" + ) + + client = caldav.DAVClient(url=caldav_url, username=username, password=password) + calendar = client.calendar(url=caldav_url) + cal = Calendar.from_ical(ics_bytes) + + imported = 0 + for component in cal.walk(): + if component.name != "VEVENT": + continue + + single_cal = Calendar() + single_cal.add("prodid", "-//ics_mail_importer//DE") + single_cal.add("version", "2.0") + single_cal.add_component(component) + + uid = str(component.get("uid", uid_hash + f"-{imported}")) + try: + calendar.add_event(single_cal.to_ical().decode("utf-8")) + log.info( + f" ✓ Termin importiert: {component.get('summary', '(kein Titel)')} [{uid[:20]}]" + ) + imported += 1 + except Exception as e: + log.warning(f" ✗ Fehler beim Import von {uid[:20]}: {e}") + + return imported + + +def process_mailbox(): + host = os.getenv("IMAP_HOST", "imap.mailbox.org") + port = int(os.getenv("IMAP_PORT", "993")) + username = os.getenv("IMAP_USERNAME") + password = os.getenv("IMAP_PASSWORD") + folder = os.getenv("IMAP_FOLDER", "INBOX") + + if not username or not password: + raise RuntimeError("IMAP_USERNAME oder IMAP_PASSWORD fehlt in .env") + + unseen_only = env_bool("IMAP_UNSEEN_ONLY", "true") + mark_as_read = env_bool("IMAP_MARK_AS_READ", "false") + + log.info(f"Verbinde mit IMAP {host}:{port} als {username} ...") + conn = imaplib.IMAP4_SSL(host, port) + conn.login(username, password) + + conn.select(folder) + criteria = "UNSEEN" if unseen_only else "ALL" + typ, data = conn.uid("search", None, criteria) + if typ != "OK": + raise RuntimeError("IMAP-Suche fehlgeschlagen") + + message_uids = data[0].split() + log.info(f"{len(message_uids)} Nachricht(en) zur Prüfung gefunden.") + + imported_hashes = load_seen_uids() + total_imported = 0 + processed_mail_count = 0 + + for msg_uid in message_uids: + typ, fetched = conn.uid("fetch", msg_uid, "(BODY.PEEK[])") + if typ != "OK" or not fetched or not fetched[0]: + log.warning(f"Konnte Nachricht UID {msg_uid.decode()} nicht abrufen.") + continue + + raw = fetched[0][1] + msg = email.message_from_bytes(raw) + + mail_had_ics = False + mail_had_successful_import = False + + subject = msg.get("Subject", "(ohne Betreff)") + log.info(f"Prüfe Mail UID {msg_uid.decode()} – Betreff: {subject}") + + for part in msg.walk(): + content_type = part.get_content_type() + filename = part.get_filename() or "" + is_ics = ( + content_type in ("text/calendar", "application/ics") + or filename.lower().endswith(".ics") + ) + if not is_ics: + continue + + ics_bytes = part.get_payload(decode=True) + if not ics_bytes: + continue + + mail_had_ics = True + uid_hash = hashlib.sha256(ics_bytes).hexdigest() + + if uid_hash in imported_hashes: + log.info( + f" Überspringe bereits importierten ICS-Anhang {filename!r} ({uid_hash[:12]}…)" + ) + continue + + log.info(f" ICS-Anhang gefunden: {filename!r} ({uid_hash[:12]}…)") + + try: + imported_count = import_ics_to_caldav(ics_bytes, uid_hash) + if imported_count > 0: + save_uid(uid_hash) + imported_hashes.add(uid_hash) + total_imported += imported_count + mail_had_successful_import = True + else: + log.warning( + f" Kein VEVENT importiert für Anhang {filename!r}; Mail bleibt ungelesen." + ) + except Exception as e: + log.warning( + f" Fehler beim Verarbeiten von {filename!r}: {e}; Mail bleibt ungelesen." + ) + + if mark_as_read and mail_had_successful_import: + store_typ, _ = conn.uid("store", msg_uid, "+FLAGS", "(\\Seen)") + if store_typ == "OK": + processed_mail_count += 1 + log.info( + f"Mail UID {msg_uid.decode()} erfolgreich verarbeitet und als gelesen markiert." + ) + else: + log.warning( + f"Mail UID {msg_uid.decode()} wurde importiert, konnte aber nicht als gelesen markiert werden." + ) + elif mark_as_read and mail_had_ics and not mail_had_successful_import: + log.info( + f"Mail UID {msg_uid.decode()} hatte ICS-Anhang, aber keinen erfolgreichen Import; bleibt ungelesen." + ) + + conn.close() + conn.logout() + + log.info(f"Fertig. {total_imported} Termin(e) importiert.") + log.info(f"{processed_mail_count} Mail(s) als gelesen markiert.") + + +def main(): + log.info("=" * 60) + log.info(f"ICS-Importer gestartet ({datetime.now(timezone.utc).isoformat()})") + process_mailbox() + log.info("=" * 60) + + +if __name__ == "__main__": + main()