feat(PROJ-18): E-Mail Integritätsprüfung (SHA-256 Verifikation)

- Storage: VerifyIntegrity, GetAllIDs, GetVerifyStatus + DB-Spalten
- main: Hintergrund-Worker alle 5 Minuten (beim Start sofort: 40/40 OK)
- API: verify_ok + verified_at in GET /api/mails/{id} Antwort
- Frontend: Grüner Haken / graues X / rotes X in Mail-Ansicht

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-14 21:28:40 +01:00
parent 7e68c7ab02
commit 3c722d0987
7 changed files with 179 additions and 1 deletions
+44
View File
@@ -165,6 +165,9 @@ func main() {
// Backfill in background: migrate existing files into DB metadata + re-index
go runBackfill(context.Background(), mailStore, idx, worker, logger)
// Background integrity verification — runs every 5 minutes
go runIntegrityCheck(context.Background(), mailStore, logger)
// Start HTTP API
go func() {
logger.Info("starting API server", "addr", bind)
@@ -276,6 +279,47 @@ func runBackfill(ctx context.Context, store *storage.Store, idx index.Indexer, w
logger.Info("backfill: complete", "total", count, "submitted_for_index", needIndex, "errors", errCount)
}
// runIntegrityCheck verifies all stored emails every 5 minutes by re-computing
// their SHA-256 and comparing it to the stored file ID.
func runIntegrityCheck(ctx context.Context, store *storage.Store, logger *slog.Logger) {
// run once at startup, then every 5 minutes
doVerify := func() {
ids, err := store.GetAllIDs(ctx)
if err != nil {
logger.Error("integrity check: get IDs failed", "err", err)
return
}
ok := 0
fail := 0
for _, id := range ids {
verified, err := store.VerifyIntegrity(ctx, id)
if err != nil {
fail++
continue
}
if verified {
ok++
} else {
fail++
logger.Warn("integrity check: FAILED", "id", id)
}
}
logger.Info("integrity check: complete", "ok", ok, "failed", fail, "total", len(ids))
}
doVerify()
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
doVerify()
case <-ctx.Done():
return
}
}
}
// seedDefaultUsers creates default admin and auditor accounts if no users exist yet.
func seedDefaultUsers(users *userstore.Store, logger *slog.Logger) error {
all, err := users.List("")