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
75 lines
2.4 KiB
Go
75 lines
2.4 KiB
Go
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
|
|
}
|