diff --git a/cmd/archivmail/cmd_reindex.go b/cmd/archivmail/cmd_reindex.go index 7ea0cce..7db17e2 100644 --- a/cmd/archivmail/cmd_reindex.go +++ b/cmd/archivmail/cmd_reindex.go @@ -129,6 +129,19 @@ func runReindex(args []string) { } idx := idxMgr.ForTenant(tenantID) + + // Preserve existing OCR text — IndexSync uses REPLACE which would + // otherwise wipe attachment_text written by the OCR worker. + // Only read from the index when the DB confirms OCR has run (ocr_chars>0). + _, ocrChars, _ := mailStore.GetOCRMeta(ctx, id) + if ocrChars > 0 { + if reader, ok := idx.(index.AttachmentTextReader); ok { + if existing, err := reader.GetAttachmentText(id); err == nil && existing != "" { + doc.AttachmentText = existing + } + } + } + if err := idx.IndexSync(doc); err != nil { logger.Warn("reindex: index failed", "id", id, "err", err) errors++ diff --git a/docs/PRD.md b/docs/PRD.md index 21ef6a7..b20b87f 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -34,6 +34,7 @@ Ein selbst gehostetes, unternehmenstaugliches Mail-Archiv-System, das E-Mails au | P2 | Audit-Log & Compliance-Berichte | Planned | | P2 | E-Mail-Export (EML/PDF) | Planned | | P2 | REST API für externe CRM-Anbindung | Planned | +| P1 | IMAP Per-Folder UID-Tracking + UIDVALIDITY-Check (Reliability-Fix) | Planned | ## Success Metrics - Import und Indexierung von 100.000+ E-Mails ohne Performanceprobleme diff --git a/features/INDEX.md b/features/INDEX.md index 8a55a02..f4ff4e0 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -60,7 +60,9 @@ | PROJ-41 | Dashboard Zeitreihe + Speicherprognose | Deployed | [PROJ-41](PROJ-41-dashboard-zeitreihe.md) | 2026-04-05 | | PROJ-42 | Gespeicherte Suchanfragen | Deployed | [PROJ-42](PROJ-42-gespeicherte-suchanfragen.md) | 2026-04-05 | | PROJ-43 | Automatische Archivierungsregeln | Planned | [PROJ-43](PROJ-43-archivierungsregeln.md) | 2026-04-05 | +| PROJ-44 | OCR-GUI-Integration (Status, Download, Such-Highlight) | In Review | [PROJ-44](PROJ-44-ocr-gui-integration.md) | 2026-05-08 | +| PROJ-45 | IMAP Per-Folder UID-Tracking + UIDVALIDITY-Check | Deployed | [PROJ-45](PROJ-45-imap-folder-uid-tracking.md) | 2026-05-11 | -## Next Available ID: PROJ-44 +## Next Available ID: PROJ-46 diff --git a/internal/imap/scheduler.go b/internal/imap/scheduler.go index 4360f48..773fd34 100644 --- a/internal/imap/scheduler.go +++ b/internal/imap/scheduler.go @@ -302,13 +302,16 @@ func (s *Scheduler) syncFolder( serverUIDValidity = selectData.UIDValidity } - effectiveLastUID := state.LastUID + // BUG-2 fix: use DecideResync instead of inline switch to keep the + // branching logic in one place and avoid drift with the unit-tested path. + decision := DecideResync(state.LastUID, state.UIDValidity, serverUIDValidity) + effectiveLastUID := decision.EffectiveStart - switch { - case serverUIDValidity == 0: + if serverUIDValidity == 0 { log.Warn("uidvalidity zero from server, keeping stored last_uid", "folder", folder, "stored_uid_validity", state.UIDValidity) - case state.UIDValidity != 0 && state.UIDValidity != serverUIDValidity: + } + if decision.UIDValidReset { // Mailbox was rebuilt server-side — UIDs restart at 1. We must // resync the entire folder from scratch. PROJ-32 message-id // deduplication prevents duplicate writes. @@ -327,7 +330,6 @@ func (s *Scheduler) syncFolder( acc.ID, folder, state.UIDValidity, serverUIDValidity), }) } - effectiveLastUID = 0 } var uids []imapv2.UID @@ -359,14 +361,22 @@ func (s *Scheduler) syncFolder( maxUID uint32 = effectiveLastUID ) - // PROJ-45: even when there are 0 new UIDs, persist the (possibly new) - // uid_validity so a later UIDVALIDITY change can still be detected. + // BUG-1 fix: when the server returns UIDVALIDITY=0 (misbehaving server), + // keep the stored valid value so future mismatch detection still works. + // Writing 0 would silently disable the UIDVALIDITY guard for this folder. + persistedValidity := serverUIDValidity + if serverUIDValidity == 0 { + persistedValidity = state.UIDValidity + } + + // PROJ-45: even when there are 0 new UIDs, persist the uid_validity + // so a later UIDVALIDITY change can still be detected. defer func() { newState := FolderState{ AccountID: acc.ID, Folder: folder, LastUID: maxUID, - UIDValidity: serverUIDValidity, + UIDValidity: persistedValidity, } if err := s.store.UpsertFolderState(ctx, newState); err != nil { log.Warn("imap scheduler: persist folder state failed",