From 96204c04dde74ce1c6f23cd4f5cdcf4731aa003d Mon Sep 17 00:00:00 2001 From: SysAdmin Agent Date: Wed, 11 Mar 2026 21:00:50 +0000 Subject: [PATCH] Fix email poll: search all recent emails (not just UNSEEN) on manual trigger The manual "Jetzt abrufen" button now runs synchronously and searches all emails from the last 30 days instead of only unread ones. This fixes the issue where already-read emails in IMAP were invisible to the poll task. Duplicate detection (by sender+date+subject) prevents re-imports. Co-Authored-By: Claude Opus 4.6 --- app/stiftung/tasks.py | 37 +++++++++++++++++++++----------- app/stiftung/views/geschichte.py | 20 ++++++++++------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/app/stiftung/tasks.py b/app/stiftung/tasks.py index 0ef9cba..bcf7a1d 100644 --- a/app/stiftung/tasks.py +++ b/app/stiftung/tasks.py @@ -156,21 +156,27 @@ def _upload_to_paperless(content: bytes, filename: str, destinataer=None, betref @shared_task(bind=True, max_retries=3, default_retry_delay=300, name="stiftung.tasks.poll_destinataer_emails") -def poll_destinataer_emails(self): +def poll_destinataer_emails(self, search_all_recent_days=0): """ - Liest ungelesene E-Mails aus dem IMAP-Postfach und verarbeitet sie. + Liest E-Mails aus dem IMAP-Postfach und verarbeitet sie. Wird durch Celery Beat alle 15 Minuten ausgeführt. + + Args: + search_all_recent_days: Wenn > 0, werden alle E-Mails der letzten N Tage + durchsucht (nicht nur ungelesene). Nützlich für manuellen Abruf. """ from stiftung.models import Destinataer, DestinataerEmailEingang, DokumentLink - # IMAP-Konfiguration aus Settings - imap_host = getattr(settings, "IMAP_HOST", None) - imap_port = int(getattr(settings, "IMAP_PORT", 993)) - imap_user = getattr(settings, "IMAP_USER", None) - imap_password = getattr(settings, "IMAP_PASSWORD", None) - imap_folder = getattr(settings, "IMAP_FOLDER", "INBOX") - imap_use_ssl = getattr(settings, "IMAP_USE_SSL", True) + # IMAP-Konfiguration: DB (AppConfiguration) mit Fallback auf env/settings + from stiftung.utils.config import get_config + + imap_host = get_config("imap_host") + imap_port = int(get_config("imap_port", 993)) + imap_user = get_config("imap_user") + imap_password = get_config("imap_password") + imap_folder = get_config("imap_folder", "INBOX") + imap_use_ssl = get_config("imap_use_ssl", True) if not all([imap_host, imap_user, imap_password]): logger.warning( @@ -199,11 +205,18 @@ def poll_destinataer_emails(self): mail.login(imap_user, imap_password) mail.select(imap_folder) - # Ungelesene Nachrichten suchen - _, message_ids_raw = mail.search(None, "UNSEEN") + # Nachrichten suchen + if search_all_recent_days and search_all_recent_days > 0: + from datetime import timedelta + since_date = (datetime.now(dt_timezone.utc) - timedelta(days=search_all_recent_days)).strftime("%d-%b-%Y") + _, message_ids_raw = mail.search(None, "SINCE", since_date) + search_mode = f"ALL seit {since_date}" + else: + _, message_ids_raw = mail.search(None, "UNSEEN") + search_mode = "UNSEEN" message_ids = message_ids_raw[0].split() - logger.info("Postfach '%s': %d ungelesene Nachricht(en) gefunden.", imap_folder, len(message_ids)) + logger.info("Postfach '%s' (%s): %d Nachricht(en) gefunden.", imap_folder, search_mode, len(message_ids)) for msg_id in message_ids: try: diff --git a/app/stiftung/views/geschichte.py b/app/stiftung/views/geschichte.py index cf6f23d..1f50483 100644 --- a/app/stiftung/views/geschichte.py +++ b/app/stiftung/views/geschichte.py @@ -689,18 +689,22 @@ def email_eingang_detail(request, pk): @login_required def email_eingang_poll_trigger(request): - """Löst den IMAP-Poll-Task manuell aus (für Tests und manuelle Verarbeitung).""" + """Löst den IMAP-Poll manuell aus – sucht alle E-Mails der letzten 30 Tage.""" if request.method == "POST": from stiftung.tasks import poll_destinataer_emails try: - task = poll_destinataer_emails.delay() - messages.success( - request, - f"E-Mail-Abruf wurde gestartet (Task-ID: {task.id}). " - "Bitte Seite in ca. 30 Sekunden neu laden.", - ) + # Synchron ausführen für sofortiges Feedback; sucht auch bereits + # gelesene E-Mails der letzten 30 Tage (Duplikate werden übersprungen). + result = poll_destinataer_emails.apply(kwargs={"search_all_recent_days": 30}).get(timeout=60) + processed = result.get("processed", 0) if isinstance(result, dict) else 0 + if result and result.get("status") == "skipped": + messages.warning(request, "IMAP ist nicht konfiguriert. Bitte Einstellungen unter Administration → E-Mail / IMAP prüfen.") + elif processed > 0: + messages.success(request, f"{processed} neue E-Mail(s) importiert.") + else: + messages.info(request, "Keine neuen E-Mails gefunden.") except Exception as exc: - messages.error(request, f"Fehler beim Starten des Tasks: {exc}") + messages.error(request, f"Fehler beim E-Mail-Abruf: {exc}") return redirect("stiftung:email_eingang_list")