fix: Mails vor der Einrichtung waren fuer immer ausserhalb des Suchfensters

Der Zeitstempel des letzten Abrufs wird auch dann gesetzt, wenn ein Abruf
nichts fand – und danach sucht Certain nur noch ab gestern. Wer das Postfach
einrichtet, nachdem die Mails schon da waren, findet sie nie: Der erste,
ergebnislose Abruf schiebt das Fenster an ihnen vorbei.

Jetzt gibt es "Aelteres nachholen": Es sucht die letzten 90 Tage und uebergeht
den Zeitstempel. Dass dabei nichts doppelt ankommt, sichert wie bisher die
Message-ID.

Ausserdem sagt die Meldung jetzt, AB WANN gesucht wurde. Bisher stand dort
"Stimmt der Ordner?" – die Frage war fast immer die falsche.

Und ein latenter Fehler, der im August nicht auffaellt: Das Suchdatum wurde mit
strftime("%b") gebaut, also mit dem Monatskuerzel in der Sprache des Servers.
IMAP verlangt die englischen; auf einem deutschen System haette die Suche im
Maerz, Mai, Oktober und Dezember nichts gefunden. Die Kuerzel stehen jetzt fest
im Code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-27 12:59:08 +02:00
co-authored by Claude Opus 5
parent ca63fef5c4
commit 60b8178c57
7 changed files with 115 additions and 21 deletions
+33 -10
View File
@@ -166,7 +166,19 @@ def _passender_absender(
return None
def _suchkriterium(quelle: MailSource) -> str:
# IMAP verlangt die ENGLISCHEN Monatskürzel. ``strftime("%b")`` richtet sich
# nach der Sprache des Servers – auf einem deutschen System käme „Mär", „Mai",
# „Okt", „Dez" heraus, und die Suche fände nichts. Im August fällt das nicht
# auf, weil das Kürzel dort in beiden Sprachen gleich lautet.
MONATE = ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
def _imap_datum(tag: date) -> str:
return f"{tag.day:02d}-{MONATE[tag.month - 1]}-{tag.year}"
def _suchkriterium(quelle: MailSource, tage: int | None = None) -> tuple[str, date]:
"""Nach Datum suchen, nie nach „ungelesen".
Auf ``UNSEEN`` zu filtern klingt sparsam, ist aber eine Falle: Wer eine
@@ -174,16 +186,22 @@ def _suchkriterium(quelle: MailSource) -> str:
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.
``tage`` übergeht den letzten Abruf und sucht ab heute minus so vielen
Tagen. Das ist nötig, weil der Zeitstempel auch dann gesetzt wird, wenn ein
Abruf nichts fand: Ältere Mails wären danach für immer ausserhalb des
Fensters, obwohl sie im Postfach liegen.
"""
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")}")'
if tage is not None:
seit = date.today() - timedelta(days=tage)
elif quelle.last_poll_at:
seit = quelle.last_poll_at.date() - timedelta(days=1)
else:
seit = date.today() - timedelta(days=ERSTER_ABRUF_TAGE)
return f'(SINCE "{_imap_datum(seit)}")', seit
def hole_neue(db: Session, quelle: MailSource) -> dict:
def hole_neue(db: Session, quelle: MailSource, tage: int | None = None) -> dict:
"""Neue Mails abholen und als Einträge im Eingang ablegen."""
user = db.get(User, quelle.owner_id)
absender = (
@@ -200,7 +218,8 @@ def hole_neue(db: Session, quelle: MailSource) -> dict:
code, _ = verbindung.select(f'"{quelle.folder}"')
if code != "OK":
raise MailFehler(f"Ordner „{quelle.folder}“ nicht gefunden")
code, daten = verbindung.search(None, _suchkriterium(quelle))
kriterium, seit = _suchkriterium(quelle, tage)
code, daten = verbindung.search(None, kriterium)
if code != "OK":
raise MailFehler("Suche im Postfach fehlgeschlagen")
@@ -287,8 +306,11 @@ def hole_neue(db: Session, quelle: MailSource) -> dict:
verbindung.logout()
except Exception:
pass
# Das Suchdatum gehoert in die Antwort: Findet sich nichts, ist die Frage
# nicht "stimmt der Ordner", sondern "wie weit wurde ueberhaupt gesucht".
return {"gefunden": gefunden, "neu": neu, "uebersprungen": uebersprungen,
"unklar": unklar, "ohne_zuordnung": ohne_zuordnung}
"unklar": unklar, "seit": seit.isoformat(),
"ohne_zuordnung": ohne_zuordnung}
def _nachbereiten(verbindung: imaplib.IMAP4, quelle: MailSource, uid: bytes) -> None:
@@ -314,4 +336,5 @@ def abruf_mit_fehlerbehandlung(db: Session, quelle: MailSource) -> dict:
quelle.last_poll_at = datetime.now(timezone.utc)
db.commit()
return {"gefunden": 0, "neu": 0, "uebersprungen": 0, "unklar": 0,
"seit": None,
"ohne_zuordnung": [], "fehler": str(e)}