FRE-683: Add contacts & attachments management
- Implemented contact CRUD operations (list, add, edit, delete) - Implemented attachment management (list, upload, download) - Created internal/contact/manager.go for contact persistence - Created internal/attachment/manager.go for attachment storage - Added CLI commands in cmd/contacts.go and cmd/attachments.go - Integrated contact and attachment commands into root CLI Files: - internal/contact/types.go - Contact data models - internal/contact/manager.go - Contact CRUD operations - internal/attachment/manager.go - Attachment file operations - cmd/contacts.go - Contact CLI commands - cmd/attachments.go - Attachment CLI commands Contacts stored in ~/.config/pop/contacts.json Attachments stored in ~/.config/pop/attachments/
This commit is contained in:
124
cmd/attachments.go
Normal file
124
cmd/attachments.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/frenocorp/pop/internal/attachment"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func attachmentCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "attachment",
|
||||
Short: "Manage attachments",
|
||||
Long: `List, download, and upload attachments.`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(attachmentListCmd())
|
||||
cmd.AddCommand(attachmentDownloadCmd())
|
||||
cmd.AddCommand(attachmentUploadCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func attachmentListCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all attachments",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
manager := attachment.NewAttachmentManager()
|
||||
|
||||
ids, err := manager.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list attachments: %w", err)
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
fmt.Println("No attachments found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Attachments:")
|
||||
for _, id := range ids {
|
||||
fmt.Printf(" - %s\n", id)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func attachmentDownloadCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "download <attachment-id> [output-dir]",
|
||||
Short: "Download an attachment",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("attachment ID is required")
|
||||
}
|
||||
|
||||
id := args[0]
|
||||
outputDir := "."
|
||||
if len(args) > 1 {
|
||||
outputDir = args[1]
|
||||
}
|
||||
|
||||
manager := attachment.NewAttachmentManager()
|
||||
|
||||
data, err := manager.Get(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get attachment: %w", err)
|
||||
}
|
||||
|
||||
if err := manager.Download(id, id, outputDir); err != nil {
|
||||
return fmt.Errorf("failed to download attachment: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Downloaded attachment %s to %s\n", id, outputDir)
|
||||
fmt.Printf("Size: %d bytes\n", len(data))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func attachmentUploadCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "upload <attachment-id> <file-path>",
|
||||
Short: "Upload an attachment",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("attachment ID and file path are required")
|
||||
}
|
||||
|
||||
id := args[0]
|
||||
filePath := args[1]
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
manager := attachment.NewAttachmentManager()
|
||||
|
||||
if err := manager.Upload(id, file.Name(), file); err != nil {
|
||||
return fmt.Errorf("failed to upload attachment: %w", err)
|
||||
}
|
||||
|
||||
info, _ := file.Stat()
|
||||
fmt.Printf("Uploaded attachment %s (%s, %d bytes)\n", id, file.Name(), info.Size())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func readAll(r io.Reader) ([]byte, error) {
|
||||
return io.ReadAll(r)
|
||||
}
|
||||
174
cmd/contacts.go
Normal file
174
cmd/contacts.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/frenocorp/pop/internal/contact"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func contactCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "contact",
|
||||
Short: "Manage contacts",
|
||||
Long: `List, add, edit, and delete contacts.`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(contactListCmd())
|
||||
cmd.AddCommand(contactAddCmd())
|
||||
cmd.AddCommand(contactEditCmd())
|
||||
cmd.AddCommand(contactDeleteCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func contactListCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all contacts",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
manager := contact.NewContactManager()
|
||||
|
||||
page, _ := cmd.Flags().GetInt("page")
|
||||
pageSize, _ := cmd.Flags().GetInt("page-size")
|
||||
search, _ := cmd.Flags().GetString("search")
|
||||
|
||||
req := contact.ListContactsRequest{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Search: search,
|
||||
}
|
||||
|
||||
resp, err := manager.List(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list contacts: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.Contacts) == 0 {
|
||||
fmt.Println("No contacts found")
|
||||
return nil
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(resp.Contacts, "", " ")
|
||||
fmt.Println(string(data))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntP("page", "p", 0, "Page number")
|
||||
cmd.Flags().IntP("page-size", "n", 10, "Items per page")
|
||||
cmd.Flags().StringP("search", "s", "", "Search by email or name")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func contactAddCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add a new contact",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
email, _ := cmd.Flags().GetString("email")
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
phone, _ := cmd.Flags().GetString("phone")
|
||||
|
||||
if email == "" {
|
||||
return fmt.Errorf("email is required")
|
||||
}
|
||||
|
||||
manager := contact.NewContactManager()
|
||||
req := contact.CreateContactRequest{
|
||||
Email: email,
|
||||
Name: name,
|
||||
Phone: phone,
|
||||
}
|
||||
|
||||
contact, err := manager.Create(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create contact: %w", err)
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(contact, "", " ")
|
||||
fmt.Printf("Created contact:\n%s\n", string(data))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("email", "e", "", "Contact email (required)")
|
||||
cmd.Flags().StringP("name", "n", "", "Contact name")
|
||||
cmd.Flags().StringP("phone", "p", "", "Contact phone")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func contactEditCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "edit <id>",
|
||||
Short: "Edit a contact",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("contact ID is required")
|
||||
}
|
||||
|
||||
id := args[0]
|
||||
manager := contact.NewContactManager()
|
||||
|
||||
_, err := manager.Get(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get contact: %w", err)
|
||||
}
|
||||
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
phone, _ := cmd.Flags().GetString("phone")
|
||||
address, _ := cmd.Flags().GetString("address")
|
||||
notes, _ := cmd.Flags().GetString("notes")
|
||||
|
||||
req := contact.UpdateContactRequest{
|
||||
Name: &name,
|
||||
Phone: &phone,
|
||||
Address: &address,
|
||||
Notes: ¬es,
|
||||
}
|
||||
|
||||
updated, err := manager.Update(id, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update contact: %w", err)
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(updated, "", " ")
|
||||
fmt.Printf("Updated contact:\n%s\n", string(data))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("name", "n", "", "New name")
|
||||
cmd.Flags().StringP("phone", "p", "", "New phone")
|
||||
cmd.Flags().StringP("address", "a", "", "New address")
|
||||
cmd.Flags().StringP("notes", "o", "", "New notes")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func contactDeleteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <id>",
|
||||
Short: "Delete a contact",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("contact ID is required")
|
||||
}
|
||||
|
||||
id := args[0]
|
||||
manager := contact.NewContactManager()
|
||||
|
||||
if err := manager.Delete(id); err != nil {
|
||||
return fmt.Errorf("failed to delete contact: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Deleted contact: %s\n", id)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
261
cmd/draft.go
Normal file
261
cmd/draft.go
Normal file
@@ -0,0 +1,261 @@
|
||||
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 := auth.NewSessionManager()
|
||||
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 := auth.NewSessionManager()
|
||||
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 := auth.NewSessionManager()
|
||||
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 := auth.NewSessionManager()
|
||||
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); err != nil {
|
||||
return fmt.Errorf("failed to send draft: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Draft sent successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
415
cmd/mail.go
Normal file
415
cmd/mail.go
Normal file
@@ -0,0 +1,415 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"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 mailCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "mail",
|
||||
Short: "Manage email messages",
|
||||
Long: `List, read, send, delete, and manage email messages with ProtonMail.`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(mailListCmd())
|
||||
cmd.AddCommand(mailReadCmd())
|
||||
cmd.AddCommand(mailSendCmd())
|
||||
cmd.AddCommand(mailDeleteCmd())
|
||||
cmd.AddCommand(mailTrashCmd())
|
||||
cmd.AddCommand(mailDraftCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func mailListCmd() *cobra.Command {
|
||||
var folder, page, pageSize, starred, readFlag string
|
||||
var since int64
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List messages",
|
||||
Long: `List messages from ProtonMail with pagination and folder filtering.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfgMgr := config.NewConfigManager()
|
||||
cfg, err := cfgMgr.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
sessionMgr := auth.NewSessionManager()
|
||||
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)
|
||||
|
||||
folderVal := mail.FolderInbox
|
||||
switch folder {
|
||||
case "inbox":
|
||||
folderVal = mail.FolderInbox
|
||||
case "sent":
|
||||
folderVal = mail.FolderSent
|
||||
case "drafts":
|
||||
folderVal = mail.FolderDraft
|
||||
case "trash":
|
||||
folderVal = mail.FolderTrash
|
||||
case "spam":
|
||||
folderVal = mail.FolderSpam
|
||||
default:
|
||||
return fmt.Errorf("unknown folder: %s (valid: inbox, sent, drafts, trash, spam)", folder)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var starredPtr *bool
|
||||
if starred != "" {
|
||||
v := starred == "true"
|
||||
starredPtr = &v
|
||||
}
|
||||
|
||||
var readPtr *bool
|
||||
if readFlag != "" {
|
||||
v := readFlag == "true"
|
||||
readPtr = &v
|
||||
}
|
||||
|
||||
req := mail.ListMessagesRequest{
|
||||
Folder: folderVal,
|
||||
Page: pageVal,
|
||||
PageSize: pageSizeVal,
|
||||
Passphrase: session.AccessToken,
|
||||
Starred: starredPtr,
|
||||
Read: readPtr,
|
||||
Since: since,
|
||||
}
|
||||
|
||||
result, err := mailClient.ListMessages(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list messages: %w", err)
|
||||
}
|
||||
|
||||
return printMessages(result.Messages)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&folder, "folder", "inbox", "Folder to list (inbox, sent, drafts, trash, spam)")
|
||||
cmd.Flags().StringVar(&page, "page", "1", "Page number")
|
||||
cmd.Flags().StringVar(&pageSize, "page-size", "20", "Messages per page (max 100)")
|
||||
cmd.Flags().StringVar(&starred, "starred", "", "Filter by starred (true/false)")
|
||||
cmd.Flags().StringVar(&readFlag, "read", "", "Filter by read status (true/false)")
|
||||
cmd.Flags().Int64Var(&since, "since", 0, "Only messages modified since Unix timestamp")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func mailReadCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "read <message-id>",
|
||||
Short: "Read a message",
|
||||
Long: `Read and display a message, decrypting the PGP body.`,
|
||||
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 := auth.NewSessionManager()
|
||||
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)
|
||||
|
||||
msg, err := mailClient.GetMessage(messageID, session.AccessToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get message: %w", err)
|
||||
}
|
||||
|
||||
return printMessageDetail(msg)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func mailSendCmd() *cobra.Command {
|
||||
var to, cc, bcc, subject, bodyFile string
|
||||
var html bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "send",
|
||||
Short: "Send a message",
|
||||
Long: `Compose and send a message with PGP encryption.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if to == "" {
|
||||
return fmt.Errorf("recipient is required (--to)")
|
||||
}
|
||||
if subject == "" {
|
||||
return fmt.Errorf("subject is required (--subject)")
|
||||
}
|
||||
|
||||
body := ""
|
||||
if bodyFile != "" {
|
||||
data, err := os.ReadFile(bodyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read body file: %w", err)
|
||||
}
|
||||
body = string(data)
|
||||
}
|
||||
|
||||
recipients := parseRecipients(to)
|
||||
var ccRecipients, bccRecipients []mail.Recipient
|
||||
if cc != "" {
|
||||
ccRecipients = parseRecipients(cc)
|
||||
}
|
||||
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 := auth.NewSessionManager()
|
||||
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)
|
||||
|
||||
req := mail.SendRequest{
|
||||
To: recipients,
|
||||
CC: ccRecipients,
|
||||
BCC: bccRecipients,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
HTML: html,
|
||||
Passphrase: session.AccessToken,
|
||||
}
|
||||
|
||||
if err := mailClient.Send(req); err != nil {
|
||||
return fmt.Errorf("failed to send message: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Message sent successfully")
|
||||
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", "", "Message subject")
|
||||
cmd.Flags().StringVarP(&bodyFile, "body-file", "f", "", "File containing message body")
|
||||
cmd.Flags().BoolVar(&html, "html", false, "Send body as HTML")
|
||||
cmd.Flags().StringVar(&bodyFile, "body", "", "Inline message body")
|
||||
_ = cmd.MarkFlagRequired("to")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func mailDeleteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <message-id>",
|
||||
Short: "Permanently delete a message",
|
||||
Long: `Permanently delete a message from ProtonMail. This action cannot be undone.`,
|
||||
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 := auth.NewSessionManager()
|
||||
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.PermanentlyDelete(messageID); err != nil {
|
||||
return fmt.Errorf("failed to delete message: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Message deleted permanently")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func mailTrashCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "trash <message-id>",
|
||||
Short: "Move a message to trash",
|
||||
Long: `Move a message to the trash folder.`,
|
||||
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 := auth.NewSessionManager()
|
||||
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.MoveToTrash(messageID); err != nil {
|
||||
return fmt.Errorf("failed to move to trash: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Message moved to trash")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func printMessages(messages []mail.Message) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ID\tFrom\tSubject\tDate\tStarred\tRead")
|
||||
fmt.Fprintln(w, "--\t----\t-------\t----\t-------\t----")
|
||||
|
||||
for _, msg := range messages {
|
||||
id := msg.MessageID
|
||||
if len(id) > 12 {
|
||||
id = id[:12]
|
||||
}
|
||||
|
||||
from := msg.Sender.DisplayName()
|
||||
subject := msg.Subject
|
||||
if len(subject) > 50 {
|
||||
subject = subject[:47] + "..."
|
||||
}
|
||||
|
||||
date := msg.CreatedAt.Format("2006-01-02 15:04")
|
||||
starred := "-"
|
||||
if msg.Starred {
|
||||
starred = "*"
|
||||
}
|
||||
read := "Y"
|
||||
if !msg.Read {
|
||||
read = "N"
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", id, from, subject, date, starred, read)
|
||||
}
|
||||
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
func printMessageDetail(msg *mail.Message) error {
|
||||
fmt.Printf("From: %s\n", msg.Sender.DisplayName())
|
||||
fmt.Printf("To: %s\n", formatRecipients(msg.Recipients))
|
||||
fmt.Printf("Subject: %s\n", msg.Subject)
|
||||
fmt.Printf("Date: %s\n", msg.CreatedAt.Format("2006-01-02 15:04:05 MST"))
|
||||
fmt.Printf("ID: %s\n", msg.MessageID)
|
||||
fmt.Printf("Starred: %t\n", msg.Starred)
|
||||
fmt.Printf("Read: %t\n", msg.Read)
|
||||
fmt.Printf("Folder: %s\n", msg.Folder().Name())
|
||||
|
||||
if msg.Body != "" {
|
||||
fmt.Printf("\n--- Body ---\n%s\n", msg.Body)
|
||||
}
|
||||
|
||||
if len(msg.Attachments) > 0 {
|
||||
fmt.Printf("\n--- Attachments (%d) ---\n", len(msg.Attachments))
|
||||
for _, att := range msg.Attachments {
|
||||
sizeStr := formatSize(att.Size)
|
||||
fmt.Printf(" [%s] %s (%s)\n", att.AttachmentID, att.Name, sizeStr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRecipients(input string) []mail.Recipient {
|
||||
var recipients []mail.Recipient
|
||||
for _, addr := range strings.Split(input, ",") {
|
||||
addr = strings.TrimSpace(addr)
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
r := mail.Recipient{Address: addr}
|
||||
if strings.Contains(addr, "<") {
|
||||
parts := strings.SplitN(addr, "<", 2)
|
||||
r.Name = strings.TrimSpace(parts[0])
|
||||
r.Address = strings.Trim(parts[1], "<>")
|
||||
}
|
||||
recipients = append(recipients, r)
|
||||
}
|
||||
return recipients
|
||||
}
|
||||
|
||||
func formatRecipients(recipients []mail.Recipient) string {
|
||||
parts := make([]string, len(recipients))
|
||||
for i, r := range recipients {
|
||||
parts[i] = r.DisplayName()
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func formatSize(bytes int) string {
|
||||
switch {
|
||||
case bytes >= 1024*1024:
|
||||
return fmt.Sprintf("%.1f MB", float64(bytes)/1024/1024)
|
||||
case bytes >= 1024:
|
||||
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
|
||||
default:
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ func NewRootCmd() *cobra.Command {
|
||||
rootCmd.AddCommand(loginCmd())
|
||||
rootCmd.AddCommand(logoutCmd())
|
||||
rootCmd.AddCommand(sessionCmd())
|
||||
rootCmd.AddCommand(contactCmd())
|
||||
rootCmd.AddCommand(attachmentCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user