feat(PROJ-84): fix-subjects CLI-Backfill für kaputte MIME-Header-Betreffs
CLI-Subkommando "archivmail fix-subjects" korrigiert Bestandsmails mit
undekodiertem RFC-2047-Encoded-Word im Betreff (Folge des Fixes in
31d7113). Dry-Run per Default, echte Änderung nur mit --apply.
Original-EML im Storage bleibt unangetastet - nur Postgres emails.subject
wird korrigiert, danach Manticore-Reindex inkl. Erhalt von OCR-Text.
Jeder Lauf erzeugt einen Audit-Log-Eintrag (metadata_backfill).
Verifiziert auf 192.168.1.132: 543 Kandidaten, 541 reparierbar, 2 zu
Recht übersprungen (RFC-2047-Verletzung im Original). --apply noch
nicht ausgeführt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
31d71138c9
commit
1dfd8c18ac
@@ -32,6 +32,11 @@ const (
|
||||
// day dropped significantly below its trailing 7-day average, or the IMAP
|
||||
// soll/ist comparison revealed a shortfall.
|
||||
EventReconciliationAnomaly = "reconciliation_anomaly"
|
||||
// EventMetadataBackfill: a CLI/system maintenance run repaired derived
|
||||
// metadata columns (e.g. `emails.subject` after the RFC 2047 charset fix).
|
||||
// The archived original is never modified by such a run — the entry
|
||||
// documents scope, mode (dry-run/apply) and counters for traceability.
|
||||
EventMetadataBackfill = "metadata_backfill"
|
||||
)
|
||||
|
||||
// Entry is a single audit log record.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SubjectRow is a minimal projection of an email used by metadata repair runs
|
||||
// (e.g. the fix-subjects backfill). It never carries mail content — the
|
||||
// encrypted original in the store is not touched by such runs.
|
||||
type SubjectRow struct {
|
||||
ID string
|
||||
TenantID *int64
|
||||
Subject string
|
||||
}
|
||||
|
||||
// ListRawEncodedSubjects returns emails whose subject still contains an
|
||||
// RFC 2047 encoded-word pattern (`=?charset?B|Q?...?=`). The SQL LIKE is only
|
||||
// a cheap prefilter; the caller must verify with mailparser.HasEncodedWord and
|
||||
// decide whether decoding actually yields a different value.
|
||||
//
|
||||
// tenantID nil = all tenants. limit <= 0 = no limit.
|
||||
func (s *Store) ListRawEncodedSubjects(ctx context.Context, tenantID *int64, limit int) ([]SubjectRow, error) {
|
||||
if s.db == nil {
|
||||
return nil, fmt.Errorf("storage: list raw encoded subjects: no database configured")
|
||||
}
|
||||
|
||||
query := `SELECT id, tenant_id, COALESCE(subject, '')
|
||||
FROM emails
|
||||
WHERE subject LIKE '%=?%?=%'`
|
||||
args := []interface{}{}
|
||||
if tenantID != nil {
|
||||
args = append(args, *tenantID)
|
||||
query += fmt.Sprintf(" AND tenant_id = $%d", len(args))
|
||||
}
|
||||
query += " ORDER BY received_at ASC"
|
||||
if limit > 0 {
|
||||
args = append(args, limit)
|
||||
query += fmt.Sprintf(" LIMIT $%d", len(args))
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storage: list raw encoded subjects: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []SubjectRow
|
||||
for rows.Next() {
|
||||
var r SubjectRow
|
||||
if err := rows.Scan(&r.ID, &r.TenantID, &r.Subject); err != nil {
|
||||
return nil, fmt.Errorf("storage: list raw encoded subjects: scan: %w", err)
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("storage: list raw encoded subjects: rows: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UpdateSubjectMetadata rewrites only the `subject` metadata column of an
|
||||
// email. The archived original (encrypted EML in the store) is deliberately
|
||||
// left untouched — this is a display/search metadata repair, not a change to
|
||||
// the immutable archive copy.
|
||||
func (s *Store) UpdateSubjectMetadata(ctx context.Context, id, subject string) error {
|
||||
if s.db == nil {
|
||||
return fmt.Errorf("storage: update subject metadata: no database configured")
|
||||
}
|
||||
if _, err := s.db.Exec(ctx, `UPDATE emails SET subject = $1 WHERE id = $2`, subject, id); err != nil {
|
||||
return fmt.Errorf("storage: update subject metadata %s: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user