package cmd import ( "fmt" "os" "strings" "github.com/frenocorp/pop/internal/api" "github.com/frenocorp/pop/internal/config" internalmail "github.com/frenocorp/pop/internal/mail" "github.com/spf13/cobra" ) func bulkCmd() *cobra.Command { cmd := &cobra.Command{ Use: "bulk", Short: "Bulk operations on messages", Long: `Perform operations on multiple messages at once: delete, trash, star, mark read.`, } cmd.AddCommand(bulkDeleteCmd()) cmd.AddCommand(bulkTrashCmd()) cmd.AddCommand(bulkStarCmd()) cmd.AddCommand(bulkUnstarCmd()) cmd.AddCommand(bulkMarkReadCmd()) cmd.AddCommand(bulkMarkUnreadCmd()) return cmd } func bulkDeleteCmd() *cobra.Command { var ids, idsFile string cmd := &cobra.Command{ Use: "delete", Short: "Permanently delete multiple messages", Long: `Permanently delete multiple messages from ProtonMail. Use --ids for comma-separated IDs or --ids-file for a file with one ID per line.`, RunE: func(cmd *cobra.Command, args []string) error { messageIDs := collectMessageIDs(ids, idsFile) if len(messageIDs) == 0 { return fmt.Errorf("no message IDs provided") } cfgMgr := config.NewConfigManager() cfg, err := cfgMgr.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) } session, sessionMgr, err := checkAuthenticatedWithManager() if err != nil { return err } client := api.NewProtonMailClient(cfg, sessionMgr) client.SetAuthHeader(session.AccessToken) mailClient := internalmail.NewClient(client) result, err := mailClient.BulkDelete(messageIDs) if err != nil { return fmt.Errorf("failed to bulk delete: %w", err) } fmt.Printf("Deleted %d/%d messages\n", result.SuccessCount, result.Total) if len(result.Errors) > 0 { fmt.Fprintln(os.Stderr, "Errors:") for _, e := range result.Errors { fmt.Fprintf(os.Stderr, " - %s: %s\n", e.MessageID, e.Error) } } return nil }, } cmd.Flags().StringVarP(&ids, "ids", "i", "", "Comma-separated message IDs") cmd.Flags().StringVarP(&idsFile, "ids-file", "f", "", "File with one message ID per line") return cmd } func bulkTrashCmd() *cobra.Command { var ids, idsFile string cmd := &cobra.Command{ Use: "trash", Short: "Move multiple messages to trash", Long: `Move multiple messages to the trash folder. Use --ids for comma-separated IDs or --ids-file for a file with one ID per line.`, RunE: func(cmd *cobra.Command, args []string) error { messageIDs := collectMessageIDs(ids, idsFile) if len(messageIDs) == 0 { return fmt.Errorf("no message IDs provided") } cfgMgr := config.NewConfigManager() cfg, err := cfgMgr.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) } session, sessionMgr, err := checkAuthenticatedWithManager() if err != nil { return err } client := api.NewProtonMailClient(cfg, sessionMgr) client.SetAuthHeader(session.AccessToken) mailClient := internalmail.NewClient(client) result, err := mailClient.BulkTrash(messageIDs, session.MailPassphrase) if err != nil { return fmt.Errorf("failed to bulk trash: %w", err) } fmt.Printf("Trashed %d/%d messages\n", result.SuccessCount, result.Total) if len(result.Errors) > 0 { fmt.Fprintln(os.Stderr, "Errors:") for _, e := range result.Errors { fmt.Fprintf(os.Stderr, " - %s: %s\n", e.MessageID, e.Error) } } return nil }, } cmd.Flags().StringVarP(&ids, "ids", "i", "", "Comma-separated message IDs") cmd.Flags().StringVarP(&idsFile, "ids-file", "f", "", "File with one message ID per line") return cmd } func bulkStarCmd() *cobra.Command { var ids, idsFile string cmd := &cobra.Command{ Use: "star", Short: "Star multiple messages", Long: `Star multiple messages at once. Use --ids for comma-separated IDs or --ids-file for a file with one ID per line.`, RunE: func(cmd *cobra.Command, args []string) error { messageIDs := collectMessageIDs(ids, idsFile) if len(messageIDs) == 0 { return fmt.Errorf("no message IDs provided") } cfgMgr := config.NewConfigManager() cfg, err := cfgMgr.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) } session, sessionMgr, err := checkAuthenticatedWithManager() if err != nil { return err } client := api.NewProtonMailClient(cfg, sessionMgr) client.SetAuthHeader(session.AccessToken) mailClient := internalmail.NewClient(client) result, err := mailClient.BulkStar(messageIDs, true) if err != nil { return fmt.Errorf("failed to bulk star: %w", err) } fmt.Printf("Starred %d/%d messages\n", result.SuccessCount, result.Total) if len(result.Errors) > 0 { fmt.Fprintln(os.Stderr, "Errors:") for _, e := range result.Errors { fmt.Fprintf(os.Stderr, " - %s: %s\n", e.MessageID, e.Error) } } return nil }, } cmd.Flags().StringVarP(&ids, "ids", "i", "", "Comma-separated message IDs") cmd.Flags().StringVarP(&idsFile, "ids-file", "f", "", "File with one message ID per line") return cmd } func bulkUnstarCmd() *cobra.Command { var ids, idsFile string cmd := &cobra.Command{ Use: "unstar", Short: "Unstar multiple messages", Long: `Unstar multiple messages at once. Use --ids for comma-separated IDs or --ids-file for a file with one ID per line.`, RunE: func(cmd *cobra.Command, args []string) error { messageIDs := collectMessageIDs(ids, idsFile) if len(messageIDs) == 0 { return fmt.Errorf("no message IDs provided") } cfgMgr := config.NewConfigManager() cfg, err := cfgMgr.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) } session, sessionMgr, err := checkAuthenticatedWithManager() if err != nil { return err } client := api.NewProtonMailClient(cfg, sessionMgr) client.SetAuthHeader(session.AccessToken) mailClient := internalmail.NewClient(client) result, err := mailClient.BulkStar(messageIDs, false) if err != nil { return fmt.Errorf("failed to bulk unstar: %w", err) } fmt.Printf("Unstarred %d/%d messages\n", result.SuccessCount, result.Total) if len(result.Errors) > 0 { fmt.Fprintln(os.Stderr, "Errors:") for _, e := range result.Errors { fmt.Fprintf(os.Stderr, " - %s: %s\n", e.MessageID, e.Error) } } return nil }, } cmd.Flags().StringVarP(&ids, "ids", "i", "", "Comma-separated message IDs") cmd.Flags().StringVarP(&idsFile, "ids-file", "f", "", "File with one message ID per line") return cmd } func bulkMarkReadCmd() *cobra.Command { var ids, idsFile string cmd := &cobra.Command{ Use: "mark-read", Short: "Mark multiple messages as read", Long: `Mark multiple messages as read at once. Use --ids for comma-separated IDs or --ids-file for a file with one ID per line.`, RunE: func(cmd *cobra.Command, args []string) error { messageIDs := collectMessageIDs(ids, idsFile) if len(messageIDs) == 0 { return fmt.Errorf("no message IDs provided") } cfgMgr := config.NewConfigManager() cfg, err := cfgMgr.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) } session, sessionMgr, err := checkAuthenticatedWithManager() if err != nil { return err } client := api.NewProtonMailClient(cfg, sessionMgr) client.SetAuthHeader(session.AccessToken) mailClient := internalmail.NewClient(client) result, err := mailClient.BulkMarkRead(messageIDs, true) if err != nil { return fmt.Errorf("failed to bulk mark read: %w", err) } fmt.Printf("Marked %d/%d messages as read\n", result.SuccessCount, result.Total) if len(result.Errors) > 0 { fmt.Fprintln(os.Stderr, "Errors:") for _, e := range result.Errors { fmt.Fprintf(os.Stderr, " - %s: %s\n", e.MessageID, e.Error) } } return nil }, } cmd.Flags().StringVarP(&ids, "ids", "i", "", "Comma-separated message IDs") cmd.Flags().StringVarP(&idsFile, "ids-file", "f", "", "File with one message ID per line") return cmd } func bulkMarkUnreadCmd() *cobra.Command { var ids, idsFile string cmd := &cobra.Command{ Use: "mark-unread", Short: "Mark multiple messages as unread", Long: `Mark multiple messages as unread at once. Use --ids for comma-separated IDs or --ids-file for a file with one ID per line.`, RunE: func(cmd *cobra.Command, args []string) error { messageIDs := collectMessageIDs(ids, idsFile) if len(messageIDs) == 0 { return fmt.Errorf("no message IDs provided") } cfgMgr := config.NewConfigManager() cfg, err := cfgMgr.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) } session, sessionMgr, err := checkAuthenticatedWithManager() if err != nil { return err } client := api.NewProtonMailClient(cfg, sessionMgr) client.SetAuthHeader(session.AccessToken) mailClient := internalmail.NewClient(client) result, err := mailClient.BulkMarkRead(messageIDs, false) if err != nil { return fmt.Errorf("failed to bulk mark unread: %w", err) } fmt.Printf("Marked %d/%d messages as unread\n", result.SuccessCount, result.Total) if len(result.Errors) > 0 { fmt.Fprintln(os.Stderr, "Errors:") for _, e := range result.Errors { fmt.Fprintf(os.Stderr, " - %s: %s\n", e.MessageID, e.Error) } } return nil }, } cmd.Flags().StringVarP(&ids, "ids", "i", "", "Comma-separated message IDs") cmd.Flags().StringVarP(&idsFile, "ids-file", "f", "", "File with one message ID per line") return cmd } func collectMessageIDs(ids, idsFile string) []string { var messageIDs []string if idsFile != "" { data, err := os.ReadFile(idsFile) if err != nil { return messageIDs } lines := strings.Split(string(data), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } messageIDs = append(messageIDs, line) } } if ids != "" { for _, id := range strings.Split(ids, ",") { id = strings.TrimSpace(id) if id != "" { messageIDs = append(messageIDs, id) } } } return messageIDs }