fix(PROJ-86): Manticore-Index-Drift (emails_global) + Datumssortierung bei fehlendem Date-Header
emails_global bekam nie Tenant-Mails gespiegelt, Superadmin-Suche sah nur
~30% aller Mails. Zusätzlich sanken Mails ohne gültigen Date-Header
(date_ts=0) beim datumssortierten Listing ans Ende aller Ergebnisse und
waren dadurch praktisch unauffindbar ("GUI zeigt neue Mails nicht") -
Import/Indexierung liefen technisch korrekt, nur Sortierung/Spiegelung
waren kaputt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPFC6Jk2ke1Pq9XcuVGP1R
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7727ad5cdf
commit
26c0e04c75
@@ -297,6 +297,9 @@ func (s *Server) handleDeleteDSGVOMails(w http.ResponseWriter, r *http.Request)
|
||||
continue
|
||||
}
|
||||
_ = searchIdx.Delete(m.MailID)
|
||||
if s.idxMgr != nil && tenantID != nil {
|
||||
_ = s.idxMgr.Global().Delete(m.MailID)
|
||||
}
|
||||
m.Deleted = true
|
||||
m.Deletable = false
|
||||
m.Reason = "Geloescht auf DSGVO-Antrag"
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"archivmail/internal/index"
|
||||
"archivmail/internal/safego"
|
||||
@@ -221,7 +222,7 @@ func (s *Server) importRawMessage(ctx context.Context, raw []byte, tenantID *int
|
||||
Body: pm.TextBody + " " + pm.HTMLBody,
|
||||
AttachNames: strings.Join(attachNames, " "),
|
||||
HasAttachment: len(pm.Attachments) > 0,
|
||||
Date: pm.Date,
|
||||
Date: index.EffectiveDate(pm.Date, time.Now()),
|
||||
Size: int64(len(raw)),
|
||||
}
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ func (imp *Importer) storeAndIndex(raw []byte, tenantID *int64, accountID int64,
|
||||
Body: pm.TextBody,
|
||||
AttachNames: strings.Join(attachNames, " "),
|
||||
HasAttachment: len(pm.Attachments) > 0,
|
||||
Date: pm.Date,
|
||||
Date: index.EffectiveDate(pm.Date, time.Now()),
|
||||
Size: int64(len(raw)),
|
||||
TenantID: tenantID,
|
||||
}
|
||||
@@ -303,6 +303,12 @@ func (imp *Importer) storeAndIndex(raw []byte, tenantID *int64, accountID int64,
|
||||
if err := imp.idxMgr.ForTenant(tenantID).IndexSync(doc); err != nil {
|
||||
log.Warn("failed to index mail", "id", id, "err", err)
|
||||
// Non-fatal: mail is stored, just not searchable yet
|
||||
} else if tenantID != nil {
|
||||
// Superadmin/global search reads emails_global — mirror every
|
||||
// tenant-scoped mail there too, not just untenanted ones.
|
||||
if err := imp.idxMgr.Global().IndexSync(doc); err != nil {
|
||||
log.Warn("failed to index mail into global index", "id", id, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// PROJ-44: enqueue OCR job for any mail with attachments. Submit is
|
||||
|
||||
@@ -5,6 +5,18 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// EffectiveDate returns headerDate for sorting/filtering unless it is the
|
||||
// zero value (missing or unparseable Date: header — common in spam), in
|
||||
// which case fallback (typically received_at) is used instead. Without this,
|
||||
// mails with a bad Date header get date_ts=0 and sink to the bottom of every
|
||||
// date-sorted search result, effectively hiding them from the mail list.
|
||||
func EffectiveDate(headerDate, fallback time.Time) time.Time {
|
||||
if headerDate.IsZero() {
|
||||
return fallback
|
||||
}
|
||||
return headerDate
|
||||
}
|
||||
|
||||
// MailDocument is the indexed representation of a stored email.
|
||||
type MailDocument struct {
|
||||
ID string
|
||||
|
||||
@@ -100,5 +100,13 @@ func (w *TenantIndexWorker) indexDoc(doc MailDocument) {
|
||||
}
|
||||
if err := idx.IndexSync(doc); err != nil {
|
||||
w.logger.Error("tenant index worker: index failed", "id", doc.ID, "tenant_id", doc.TenantID, "err", err)
|
||||
return
|
||||
}
|
||||
// Superadmin/global search reads emails_global — mirror every
|
||||
// tenant-scoped mail there too, not just untenanted ones.
|
||||
if doc.TenantID != nil {
|
||||
if err := w.mgr.Global().IndexSync(doc); err != nil {
|
||||
w.logger.Error("tenant index worker: global index failed", "id", doc.ID, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (imp *Importer) storeAndIndex(raw []byte, accountID int64, log *slog.Logger
|
||||
Body: pm.TextBody,
|
||||
AttachNames: strings.Join(attachNames, " "),
|
||||
HasAttachment: len(pm.Attachments) > 0,
|
||||
Date: pm.Date,
|
||||
Date: index.EffectiveDate(pm.Date, time.Now()),
|
||||
Size: int64(len(raw)),
|
||||
TenantID: imp.TenantID,
|
||||
}
|
||||
|
||||
@@ -1079,8 +1079,9 @@ func (s *Store) IsIndexed(ctx context.Context, id string) (bool, error) {
|
||||
// UnindexedMail describes one mail awaiting full-text indexing. TenantID is
|
||||
// nil when the mail has no tenant assignment (system-level / global).
|
||||
type UnindexedMail struct {
|
||||
ID string
|
||||
TenantID *int64
|
||||
ID string
|
||||
TenantID *int64
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
// GetUnindexedMails returns up to limit mails with indexed_at IS NULL, newest
|
||||
@@ -1090,7 +1091,7 @@ func (s *Store) GetUnindexedMails(ctx context.Context, limit int) ([]UnindexedMa
|
||||
if s.db == nil {
|
||||
return nil, nil
|
||||
}
|
||||
q := `SELECT id, tenant_id FROM emails WHERE indexed_at IS NULL ORDER BY received_at DESC`
|
||||
q := `SELECT id, tenant_id, received_at FROM emails WHERE indexed_at IS NULL ORDER BY received_at DESC`
|
||||
args := []interface{}{}
|
||||
if limit > 0 {
|
||||
q += " LIMIT $1"
|
||||
@@ -1104,7 +1105,7 @@ func (s *Store) GetUnindexedMails(ctx context.Context, limit int) ([]UnindexedMa
|
||||
var out []UnindexedMail
|
||||
for rows.Next() {
|
||||
var m UnindexedMail
|
||||
if err := rows.Scan(&m.ID, &m.TenantID); err != nil {
|
||||
if err := rows.Scan(&m.ID, &m.TenantID, &m.ReceivedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, m)
|
||||
@@ -1112,6 +1113,14 @@ func (s *Store) GetUnindexedMails(ctx context.Context, limit int) ([]UnindexedMa
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetReceivedAt returns the received_at timestamp for a single mail. Used as
|
||||
// the date-sort fallback when a mail's own Date: header is missing/unparseable.
|
||||
func (s *Store) GetReceivedAt(ctx context.Context, id string) (time.Time, error) {
|
||||
var t time.Time
|
||||
err := s.db.QueryRow(ctx, `SELECT received_at FROM emails WHERE id = $1`, id).Scan(&t)
|
||||
return t, err
|
||||
}
|
||||
|
||||
// ── Backfill ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Backfill walks the store directory, parses each email, inserts missing DB
|
||||
|
||||
Reference in New Issue
Block a user