feat: implement Milestone 3 integration points
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>
This commit is contained in:
278
cmd/thread.go
Normal file
278
cmd/thread.go
Normal file
@@ -0,0 +1,278 @@
|
||||
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 <conversation-id>",
|
||||
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 <conversation-id>",
|
||||
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: <original>)")
|
||||
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, ", ")
|
||||
}
|
||||
Reference in New Issue
Block a user