Add comprehensive integration capabilities to Pop CLI: - Multi-account support with named profiles - Webhook management with signature verification - External PGP key management (import/export/encrypt/decrypt/sign/verify) - CLI plugin system for extensibility - Complete documentation in README.md All compilation errors fixed and build verified CLEAN. Security review delegated to FRE-5202. Co-Authored-By: Paperclip <noreply@paperclip.ing>
366 lines
9.9 KiB
Go
366 lines
9.9 KiB
Go
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
|
|
}
|