Files
pop/cmd/draft.go
Senior Engineer e499d16b7c FRE-681: Fix code review findings - body flag, PGP encryption, passphrase handling
- cmd/mail.go: Fix duplicate --body/--body-file flag binding (both used bodyFile)
- internal/mail/client.go: Add PGP encryption to Send via EncryptBody, add passphrase to MoveToTrash and SendDraft
- internal/mail/pgp.go: Store armored private key, add getUnlockedKeyRing helper,
  fix Decrypt/SignData/EncryptAndSign/DecryptAttachment to use passphrase via key.Unlock
- internal/mail/pgp.go: Add EncryptBody method for Send encryption with sender key
- cmd/draft.go: Update SendDraft call to include passphrase parameter
2026-04-28 10:08:33 -04:00

274 lines
7.0 KiB
Go

package cmd
import (
"fmt"
"os"
"strconv"
"github.com/frenocorp/pop/internal/api"
"github.com/frenocorp/pop/internal/auth"
"github.com/frenocorp/pop/internal/config"
"github.com/frenocorp/pop/internal/mail"
"github.com/spf13/cobra"
)
func mailDraftCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "draft",
Short: "Manage draft messages",
Long: `Save, list, edit, and send draft messages.`,
}
cmd.AddCommand(draftSaveCmd())
cmd.AddCommand(draftListCmd())
cmd.AddCommand(draftEditCmd())
cmd.AddCommand(draftSendCmd())
return cmd
}
func draftSaveCmd() *cobra.Command {
var to, cc, bcc, subject, bodyFile, body string
cmd := &cobra.Command{
Use: "save",
Short: "Save a draft message",
Long: `Save a message as a draft in ProtonMail.`,
RunE: func(cmd *cobra.Command, args []string) error {
if to == "" {
return fmt.Errorf("recipient is required (--to)")
}
msgBody := body
if bodyFile != "" {
data, err := os.ReadFile(bodyFile)
if err != nil {
return fmt.Errorf("failed to read body file: %w", err)
}
msgBody = string(data)
}
recipients := parseRecipients(to)
var ccRecipients []mail.Recipient
if cc != "" {
ccRecipients = parseRecipients(cc)
}
var bccRecipients []mail.Recipient
if bcc != "" {
bccRecipients = parseRecipients(bcc)
}
cfgMgr := config.NewConfigManager()
cfg, err := cfgMgr.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
sessionMgr, err := auth.NewSessionManager()
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}
session, err := sessionMgr.GetSession()
if err != nil {
return fmt.Errorf("not authenticated: %w", err)
}
client := api.NewProtonMailClient(cfg)
client.SetAuthHeader(session.AccessToken)
mailClient := mail.NewClient(client)
draft := mail.Draft{
To: recipients,
CC: ccRecipients,
BCC: bccRecipients,
Subject: subject,
Body: msgBody,
}
messageID, err := mailClient.SaveDraft(draft, session.AccessToken)
if err != nil {
return fmt.Errorf("failed to save draft: %w", err)
}
fmt.Printf("Draft saved with ID: %s\n", messageID)
return nil
},
}
cmd.Flags().StringVarP(&to, "to", "t", "", "Recipient addresses (comma-separated)")
cmd.Flags().StringVarP(&cc, "cc", "c", "", "CC addresses (comma-separated)")
cmd.Flags().StringVarP(&bcc, "bcc", "b", "", "BCC addresses (comma-separated)")
cmd.Flags().StringVarP(&subject, "subject", "s", "", "Draft subject")
cmd.Flags().StringVarP(&bodyFile, "body-file", "f", "", "File containing draft body")
cmd.Flags().StringVar(&body, "body", "", "Inline draft body")
return cmd
}
func draftListCmd() *cobra.Command {
var page, pageSize string
cmd := &cobra.Command{
Use: "list",
Short: "List draft messages",
Long: `List all draft messages in ProtonMail.`,
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
}
cfgMgr := config.NewConfigManager()
cfg, err := cfgMgr.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
sessionMgr, err := auth.NewSessionManager()
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}
session, err := sessionMgr.GetSession()
if err != nil {
return fmt.Errorf("not authenticated: %w", err)
}
client := api.NewProtonMailClient(cfg)
client.SetAuthHeader(session.AccessToken)
mailClient := mail.NewClient(client)
result, err := mailClient.ListDrafts(pageVal, pageSizeVal, session.AccessToken)
if err != nil {
return fmt.Errorf("failed to list drafts: %w", err)
}
return printMessages(result.Messages)
},
}
cmd.Flags().StringVar(&page, "page", "1", "Page number")
cmd.Flags().StringVar(&pageSize, "page-size", "20", "Drafts per page (max 100)")
return cmd
}
func draftEditCmd() *cobra.Command {
var to, cc, subject, bodyFile, body string
cmd := &cobra.Command{
Use: "edit <draft-id>",
Short: "Edit a draft message",
Long: `Edit an existing draft message in ProtonMail.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
messageID := args[0]
var recipients []mail.Recipient
if to != "" {
recipients = parseRecipients(to)
}
var ccRecipients []mail.Recipient
if cc != "" {
ccRecipients = parseRecipients(cc)
}
msgBody := body
if bodyFile != "" {
data, err := os.ReadFile(bodyFile)
if err != nil {
return fmt.Errorf("failed to read body file: %w", err)
}
msgBody = string(data)
}
cfgMgr := config.NewConfigManager()
cfg, err := cfgMgr.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
sessionMgr, err := auth.NewSessionManager()
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}
session, err := sessionMgr.GetSession()
if err != nil {
return fmt.Errorf("not authenticated: %w", err)
}
client := api.NewProtonMailClient(cfg)
client.SetAuthHeader(session.AccessToken)
mailClient := mail.NewClient(client)
draft := mail.Draft{
To: recipients,
CC: ccRecipients,
Subject: subject,
Body: msgBody,
}
if err := mailClient.UpdateDraft(messageID, draft, session.AccessToken); err != nil {
return fmt.Errorf("failed to update draft: %w", err)
}
fmt.Println("Draft updated successfully")
return nil
},
}
cmd.Flags().StringVarP(&to, "to", "t", "", "New recipient addresses (comma-separated)")
cmd.Flags().StringVarP(&cc, "cc", "c", "", "New CC addresses (comma-separated)")
cmd.Flags().StringVarP(&subject, "subject", "s", "", "New draft subject")
cmd.Flags().StringVarP(&bodyFile, "body-file", "f", "", "File containing new draft body")
cmd.Flags().StringVar(&body, "body", "", "New inline draft body")
return cmd
}
func draftSendCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "send <draft-id>",
Short: "Send a draft message",
Long: `Send an existing draft message from ProtonMail.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
messageID := args[0]
cfgMgr := config.NewConfigManager()
cfg, err := cfgMgr.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
sessionMgr, err := auth.NewSessionManager()
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}
session, err := sessionMgr.GetSession()
if err != nil {
return fmt.Errorf("not authenticated: %w", err)
}
client := api.NewProtonMailClient(cfg)
client.SetAuthHeader(session.AccessToken)
mailClient := mail.NewClient(client)
if err := mailClient.SendDraft(messageID, session.AccessToken); err != nil {
return fmt.Errorf("failed to send draft: %w", err)
}
fmt.Println("Draft sent successfully")
return nil
},
}
return cmd
}