package main import ( "context" "flag" "fmt" "log/slog" "os" "github.com/jackc/pgx/v5/pgxpool" "github.com/ultisuite/ulti-backend/internal/api/mail" "github.com/ultisuite/ulti-backend/internal/dbmigrate" "github.com/ultisuite/ulti-backend/internal/envexpand" "github.com/ultisuite/ulti-backend/internal/mail/sanitize" ) func main() { accountID := flag.String("account", "", "mail account UUID (optional; all accounts if empty)") dryRun := flag.Bool("dry-run", false, "scan only, do not write updates") flag.Parse() for _, path := range []string{".env", "../.env"} { _ = envexpand.ApplyFile(path) } dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { slog.Error("DATABASE_URL is required") os.Exit(1) } ctx := context.Background() if err := dbmigrate.Up(dbURL); err != nil { slog.Error("migration failed", "error", err) os.Exit(1) } pool, err := pgxpool.New(ctx, dbURL) if err != nil { slog.Error("db connect failed", "error", err) os.Exit(1) } defer pool.Close() if *dryRun { scanned, changed, err := scanBodies(ctx, pool, *accountID) if err != nil { slog.Error("scan failed", "error", err) os.Exit(1) } fmt.Printf("dry-run: scanned=%d would_update=%d\n", scanned, changed) return } if *accountID != "" { svc := mail.NewService(pool, nil, nil, nil, "") result, err := svc.ResanitizeAccountBodiesByID(ctx, *accountID) if err != nil { slog.Error("resanitize failed", "account_id", *accountID, "error", err) os.Exit(1) } fmt.Printf("account=%s scanned=%d updated=%d\n", *accountID, result.Scanned, result.Updated) return } rows, err := pool.Query(ctx, `SELECT id FROM mail_accounts WHERE is_active = true ORDER BY created_at`) if err != nil { slog.Error("list accounts failed", "error", err) os.Exit(1) } defer rows.Close() svc := mail.NewService(pool, nil, nil, nil, "") var totalScanned, totalUpdated int for rows.Next() { var id string if err := rows.Scan(&id); err != nil { slog.Error("scan account id failed", "error", err) os.Exit(1) } result, err := svc.ResanitizeAccountBodiesByID(ctx, id) if err != nil { slog.Error("resanitize failed", "account_id", id, "error", err) os.Exit(1) } fmt.Printf("account=%s scanned=%d updated=%d\n", id, result.Scanned, result.Updated) totalScanned += result.Scanned totalUpdated += result.Updated } if err := rows.Err(); err != nil { slog.Error("list accounts failed", "error", err) os.Exit(1) } fmt.Printf("done: accounts scanned_messages=%d updated=%d\n", totalScanned, totalUpdated) } func scanBodies(ctx context.Context, pool *pgxpool.Pool, accountID string) (scanned, changed int, err error) { query := ` SELECT id, body_html FROM messages WHERE body_html <> ''` args := []any{} if accountID != "" { query += ` AND account_id = $1` args = append(args, accountID) } rows, err := pool.Query(ctx, query, args...) if err != nil { return 0, 0, err } defer rows.Close() for rows.Next() { var id, body string if err := rows.Scan(&id, &body); err != nil { return scanned, changed, err } scanned++ if sanitize.SanitizeHTML(body) != body { changed++ } } return scanned, changed, rows.Err() }