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 }