package cmd import ( "fmt" "os" "strconv" "strings" "text/tabwriter" "github.com/frenocorp/pop/internal/api" "github.com/frenocorp/pop/internal/config" internalmail "github.com/frenocorp/pop/internal/mail" "github.com/spf13/cobra" ) func threadCmd() *cobra.Command { cmd := &cobra.Command{ Use: "thread", Short: "Manage email conversations", Long: `View, reply to, and manage email conversation threads.`, } cmd.AddCommand(threadListCmd()) cmd.AddCommand(threadShowCmd()) cmd.AddCommand(threadReplyCmd()) return cmd } func threadListCmd() *cobra.Command { var page, pageSize string cmd := &cobra.Command{ Use: "list", Short: "List conversation threads", Long: `List email conversation threads sorted by latest activity.`, RunE: func(cmd *cobra.Command, args []string) error { pageVal, err := strconv.Atoi(page) if err != nil || pageVal < 1 { pageVal = 1 } pageSizeVal, err := strconv.Atoi(pageSize) if err != nil || pageSizeVal < 1 { pageSizeVal = 20 } if pageSizeVal > 100 { pageSizeVal = 100 } 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.ListConversations(pageVal, pageSizeVal, session.MailPassphrase) if err != nil { return fmt.Errorf("failed to list conversations: %w", err) } if len(result.Conversations) == 0 { fmt.Println("No conversations found") return nil } return printConversations(result.Conversations) }, } cmd.Flags().StringVar(&page, "page", "1", "Page number") cmd.Flags().StringVar(&pageSize, "page-size", "20", "Conversations per page (max 100)") return cmd } func threadShowCmd() *cobra.Command { cmd := &cobra.Command{ Use: "show ", Short: "Show a conversation thread", Long: `Display all messages in a conversation thread in chronological order.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { convID := args[0] 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) req := internalmail.GetConversationRequest{ ConversationID: convID, Passphrase: session.MailPassphrase, } result, err := mailClient.GetConversation(req) if err != nil { return fmt.Errorf("failed to get conversation: %w", err) } fmt.Printf("Conversation: %s\n", result.Subject) fmt.Printf("Messages: %d\n", result.MessageCount) fmt.Printf("Participants: %s\n", formatRecipients(result.Participants)) fmt.Println() for i, msg := range result.Messages { from := msg.Sender.DisplayName() date := msg.CreatedAt.Format("2006-01-02 15:04") subject := msg.Subject if len(subject) > 60 { subject = subject[:57] + "..." } fmt.Printf("--- Message %d ---\n", i+1) fmt.Printf("From: %s\n", from) fmt.Printf("Date: %s\n", date) fmt.Printf("Subject: %s\n", subject) if msg.Body != "" { body := msg.Body if len(body) > 200 { body = body[:197] + "..." } fmt.Printf("Body: %s\n", body) } fmt.Println() } return nil }, } return cmd } func threadReplyCmd() *cobra.Command { var body, bodyFile, subject string var html bool cmd := &cobra.Command{ Use: "reply ", Short: "Reply to a conversation", Long: `Reply to the latest message in a conversation thread.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { convID := args[0] 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) req := internalmail.GetConversationRequest{ ConversationID: convID, Passphrase: session.MailPassphrase, } conv, err := mailClient.GetConversation(req) if err != nil { return fmt.Errorf("failed to get conversation: %w", err) } if len(conv.Messages) == 0 { return fmt.Errorf("conversation has no messages") } latestMsg := conv.Messages[len(conv.Messages)-1] var bodyContent string if body != "" { bodyContent = body } else if bodyFile != "" { data, err := os.ReadFile(bodyFile) if err != nil { return fmt.Errorf("failed to read body file: %w", err) } bodyContent = string(data) } replySubject := subject if replySubject == "" { replySubject = "Re: " + latestMsg.Subject } replyTo := []internalmail.Recipient{latestMsg.Sender.ToRecipient()} sendReq := internalmail.SendRequest{ To: replyTo, Subject: replySubject, Body: bodyContent, HTML: html, InReplyTo: latestMsg.MimeMessageID, References: latestMsg.ConversationID, Passphrase: session.MailPassphrase, } if err := mailClient.Send(sendReq); err != nil { return fmt.Errorf("failed to send reply: %w", err) } fmt.Println("Reply sent successfully") return nil }, } cmd.Flags().StringVarP(&subject, "subject", "s", "", "Reply subject (default: Re: )") cmd.Flags().StringVarP(&bodyFile, "body-file", "f", "", "File containing reply body") cmd.Flags().BoolVar(&html, "html", false, "Send body as HTML") cmd.Flags().StringVar(&body, "body", "", "Inline reply body") return cmd } func printConversations(conversations []internalmail.Conversation) error { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tSubject\tMessages\tLast From\tLast Date") fmt.Fprintln(w, "--\t-------\t--------\t---------\t---------") for _, conv := range conversations { id := conv.ConversationID if len(id) > 12 { id = id[:12] } subject := conv.Subject if len(subject) > 50 { subject = subject[:47] + "..." } lastFrom := "-" lastDate := "-" if conv.LastMessage != nil { lastFrom = conv.LastMessage.Sender.DisplayName() lastDate = conv.LastMessage.CreatedAt.Format("2006-01-02 15:04") } fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\n", id, subject, conv.MessageCount, lastFrom, lastDate) } return w.Flush() } func formatThreadParticipants(participants []internalmail.Recipient) string { parts := make([]string, len(participants)) for i, p := range participants { parts[i] = p.DisplayName() } return strings.Join(parts, ", ") }