fix: Abruf sucht nach Datum statt nach "ungelesen"

Auf UNSEEN zu filtern klingt sparsam, ist aber eine Falle: Wer eine Mail in
seinem Mailprogramm ansieht, bevor Certain sie geholt hat, verliert sie
dauerhaft – sie ist dann fuer immer gelesen. Genau das ist passiert, als die
Mails vor dem ersten Abruf schon in Gmail geoeffnet waren: Ergebnis "0 neu",
ohne jeden Hinweis auf die Ursache.

Gesucht wird jetzt ab dem letzten Abruf mit einem Tag Ueberlappung; dass
nichts doppelt ankommt, sichert die Message-ID ab.

Die Meldung nennt ausserdem, wie viele Mails ueberhaupt geprueft wurden.
"0 neu" allein liess offen, ob nichts da war oder nichts passte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-22 20:25:47 +02:00
co-authored by Claude Opus 5
parent f3a08f2937
commit bfb6fa6b6d
5 changed files with 56 additions and 15 deletions
+19 -10
View File
@@ -146,11 +146,18 @@ def _passender_absender(absender: list[MailSender], von: str, betreff: str) -> M
def _suchkriterium(quelle: MailSource) -> str:
"""Ungelesene, sonst ab dem letzten Abruf – nie das ganze Postfach."""
if quelle.mark_seen and not quelle.done_folder:
return "(UNSEEN)"
seit = quelle.last_poll_at.date() if quelle.last_poll_at else (
date.today() - timedelta(days=ERSTER_ABRUF_TAGE)
"""Nach Datum suchen, nie nach „ungelesen".
Auf ``UNSEEN`` zu filtern klingt sparsam, ist aber eine Falle: Wer eine
Mail in seinem Mailprogramm ansieht, bevor Certain sie geholt hat, verliert
sie dauerhaft – sie ist dann für immer gelesen. Gesucht wird deshalb ab dem
letzten Abruf, mit einem Tag Überlappung gegen Zeitzonen-Verschiebungen.
Dass nichts doppelt ankommt, sichert die Message-ID ab.
"""
seit = (
quelle.last_poll_at.date() - timedelta(days=1)
if quelle.last_poll_at
else date.today() - timedelta(days=ERSTER_ABRUF_TAGE)
)
return f'(SINCE "{seit.strftime("%d-%b-%Y")}")'
@@ -164,7 +171,7 @@ def hole_neue(db: Session, quelle: MailSource) -> dict:
konten = {a.id: a for a in db.query(Account).filter(Account.owner_id == quelle.owner_id)}
verbindung = _verbinden(quelle)
neu = uebersprungen = unklar = 0
neu = uebersprungen = unklar = gefunden = 0
# Was durchgefallen ist, mit Absender und Betreff – sonst raet man beim
# Einrichten, was in den Filter gehoert.
ohne_zuordnung: list[dict] = []
@@ -176,7 +183,9 @@ def hole_neue(db: Session, quelle: MailSource) -> dict:
if code != "OK":
raise MailFehler("Suche im Postfach fehlgeschlagen")
for uid in (daten[0] or b"").split():
uids = (daten[0] or b"").split()
gefunden = len(uids)
for uid in uids:
code, roh = verbindung.fetch(uid, "(RFC822)")
if code != "OK" or not roh or not isinstance(roh[0], tuple):
continue
@@ -250,8 +259,8 @@ def hole_neue(db: Session, quelle: MailSource) -> dict:
verbindung.logout()
except Exception:
pass
return {"neu": neu, "uebersprungen": uebersprungen, "unklar": unklar,
"ohne_zuordnung": ohne_zuordnung}
return {"gefunden": gefunden, "neu": neu, "uebersprungen": uebersprungen,
"unklar": unklar, "ohne_zuordnung": ohne_zuordnung}
def _nachbereiten(verbindung: imaplib.IMAP4, quelle: MailSource, uid: bytes) -> None:
@@ -276,5 +285,5 @@ def abruf_mit_fehlerbehandlung(db: Session, quelle: MailSource) -> dict:
quelle.last_error = str(e)[:500]
quelle.last_poll_at = datetime.now(timezone.utc)
db.commit()
return {"neu": 0, "uebersprungen": 0, "unklar": 0,
return {"gefunden": 0, "neu": 0, "uebersprungen": 0, "unklar": 0,
"ohne_zuordnung": [], "fehler": str(e)}