package cmd import ( "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/frenocorp/pop/internal/api" "github.com/frenocorp/pop/internal/config" internalmail "github.com/frenocorp/pop/internal/mail" "github.com/spf13/cobra" ) func draftAutoSaveCmd() *cobra.Command { cmd := &cobra.Command{ Use: "autosave", Short: "Configure draft auto-save", Long: `Enable or configure automatic draft saving. Set interval, view status, and manage auto-save settings.`, } cmd.AddCommand(autosaveEnableCmd()) cmd.AddCommand(autosaveDisableCmd()) cmd.AddCommand(autosaveStatusCmd()) cmd.AddCommand(autosaveConfigCmd()) cmd.AddCommand(autosaveRunCmd()) return cmd } func autosaveEnableCmd() *cobra.Command { var interval int cmd := &cobra.Command{ Use: "enable", Short: "Enable draft auto-save", Long: `Enable automatic saving of draft messages at a configured interval.`, RunE: func(cmd *cobra.Command, args []string) error { if interval < 10 { interval = 30 } if interval > 3600 { interval = 3600 } cfgMgr := config.NewConfigManager() autoSaveConfig, err := loadAutoSaveConfig(cfgMgr) if err != nil { return fmt.Errorf("failed to load auto-save config: %w", err) } autoSaveConfig.Enabled = true autoSaveConfig.Interval = interval if err := saveAutoSaveConfig(cfgMgr, autoSaveConfig); err != nil { return fmt.Errorf("failed to save auto-save config: %w", err) } fmt.Printf("Draft auto-save enabled (interval: %d seconds)\n", interval) return nil }, } cmd.Flags().IntVarP(&interval, "interval", "i", 30, "Auto-save interval in seconds (10-3600, default: 30)") return cmd } func autosaveDisableCmd() *cobra.Command { cmd := &cobra.Command{ Use: "disable", Short: "Disable draft auto-save", Long: `Disable automatic saving of draft messages.`, RunE: func(cmd *cobra.Command, args []string) error { cfgMgr := config.NewConfigManager() autoSaveConfig, err := loadAutoSaveConfig(cfgMgr) if err != nil { return fmt.Errorf("failed to load auto-save config: %w", err) } autoSaveConfig.Enabled = false if err := saveAutoSaveConfig(cfgMgr, autoSaveConfig); err != nil { return fmt.Errorf("failed to save auto-save config: %w", err) } fmt.Println("Draft auto-save disabled") return nil }, } return cmd } func autosaveStatusCmd() *cobra.Command { cmd := &cobra.Command{ Use: "status", Short: "Show auto-save status", Long: `Display the current auto-save configuration and last save time.`, RunE: func(cmd *cobra.Command, args []string) error { cfgMgr := config.NewConfigManager() autoSaveConfig, err := loadAutoSaveConfig(cfgMgr) if err != nil { return fmt.Errorf("failed to load auto-save config: %w", err) } fmt.Printf("Enabled: %t\n", autoSaveConfig.Enabled) fmt.Printf("Interval: %d seconds\n", autoSaveConfig.Interval) if autoSaveConfig.LastSaved > 0 { lastSaved := time.Unix(autoSaveConfig.LastSaved, 0) elapsed := time.Since(lastSaved) fmt.Printf("Last Save: %s (%s ago)\n", lastSaved.Format("2006-01-02 15:04:05"), elapsed.Round(time.Second).String()) } else { fmt.Println("Last Save: Never") } return nil }, } return cmd } func autosaveConfigCmd() *cobra.Command { var interval int cmd := &cobra.Command{ Use: "config", Short: "Configure auto-save settings", Long: `Update auto-save interval and settings. Does not enable/disable auto-save.`, RunE: func(cmd *cobra.Command, args []string) error { if interval < 10 { interval = 30 } if interval > 3600 { interval = 3600 } cfgMgr := config.NewConfigManager() autoSaveConfig, err := loadAutoSaveConfig(cfgMgr) if err != nil { return fmt.Errorf("failed to load auto-save config: %w", err) } autoSaveConfig.Interval = interval if err := saveAutoSaveConfig(cfgMgr, autoSaveConfig); err != nil { return fmt.Errorf("failed to save auto-save config: %w", err) } fmt.Printf("Auto-save interval updated to %d seconds\n", interval) return nil }, } cmd.Flags().IntVarP(&interval, "interval", "i", 0, "New auto-save interval in seconds (10-3600)") _ = cmd.MarkFlagRequired("interval") return cmd } func autosaveRunCmd() *cobra.Command { var draftID string cmd := &cobra.Command{ Use: "run", Short: "Manually trigger auto-save", Long: `Manually trigger the auto-save process for the current draft or a specific draft.`, RunE: func(cmd *cobra.Command, args []string) error { cfgMgr := config.NewConfigManager() autoSaveConfig, err := loadAutoSaveConfig(cfgMgr) if err != nil { return fmt.Errorf("failed to load auto-save config: %w", err) } if !autoSaveConfig.Enabled { fmt.Println("Auto-save is disabled. Enable with 'pop draft autosave enable'") return nil } session, sessionMgr, err := checkAuthenticatedWithManager() if err != nil { return err } cfg, err := cfgMgr.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) } client := api.NewProtonMailClient(cfg, sessionMgr) client.SetAuthHeader(session.AccessToken) mailClient := internalmail.NewClient(client) localDrafts, err := loadLocalDrafts(cfgMgr) if err != nil { return fmt.Errorf("failed to load local drafts: %w", err) } if draftID == "" { draftID = getLastEditedDraft(localDrafts) if draftID == "" { fmt.Println("No active draft to save") return nil } } localDraft, found := findLocalDraft(localDrafts, draftID) if !found { return fmt.Errorf("draft not found: %s", draftID) } draft := internalmail.Draft{ MessageID: localDraft.MessageID, To: localDraft.To, CC: localDraft.CC, BCC: localDraft.BCC, Subject: localDraft.Subject, Body: localDraft.Body, } var errResult error if localDraft.MessageID == "" { id, err := mailClient.SaveDraft(draft, session.MailPassphrase) if err != nil { errResult = fmt.Errorf("failed to save draft: %w", err) } else { localDraft.MessageID = id fmt.Printf("Draft saved with ID: %s\n", id) } } else { errResult = mailClient.UpdateDraft(localDraft.MessageID, draft, session.MailPassphrase) if errResult != nil { errResult = fmt.Errorf("failed to update draft: %w", errResult) } else { fmt.Printf("Draft updated: %s\n", localDraft.MessageID) } } if errResult == nil { localDraft.LastEdited = time.Now().Unix() autoSaveConfig.LastSaved = time.Now().Unix() saveLocalDrafts(cfgMgr, localDrafts) saveAutoSaveConfig(cfgMgr, autoSaveConfig) } return errResult }, } cmd.Flags().StringVarP(&draftID, "draft", "d", "", "Specific draft ID to save (default: most recently edited)") return cmd } func draftTemplateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "template", Short: "Manage email templates", Long: `Save and reuse email templates for quick draft creation.`, } cmd.AddCommand(templateSaveCmd()) cmd.AddCommand(templateListCmd()) cmd.AddCommand(templateUseCmd()) cmd.AddCommand(templateDeleteCmd()) return cmd } func templateSaveCmd() *cobra.Command { var name, subject, body, bodyFile string cmd := &cobra.Command{ Use: "save ", Short: "Save an email template", Long: `Save a subject and body as a reusable email template.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { templateName := name if len(args) > 0 && args[0] != "" { templateName = args[0] } if templateName == "" { return fmt.Errorf("template name is required") } 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() templates, err := loadTemplates(cfgMgr) if err != nil { return fmt.Errorf("failed to load templates: %w", err) } templates[templateName] = DraftTemplate{ Name: templateName, Subject: subject, Body: msgBody, SavedAt: time.Now().Format(time.RFC3339), } if err := saveTemplates(cfgMgr, templates); err != nil { return fmt.Errorf("failed to save templates: %w", err) } fmt.Printf("Template '%s' saved\n", templateName) return nil }, } cmd.Flags().StringVar(&name, "name", "", "Template name (or pass as positional argument)") cmd.Flags().StringVarP(&subject, "subject", "s", "", "Template subject") cmd.Flags().StringVarP(&bodyFile, "body-file", "f", "", "File containing template body") cmd.Flags().StringVar(&body, "body", "", "Inline template body") return cmd } func templateListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List email templates", Long: `List all saved email templates.`, RunE: func(cmd *cobra.Command, args []string) error { cfgMgr := config.NewConfigManager() templates, err := loadTemplates(cfgMgr) if err != nil { return fmt.Errorf("failed to load templates: %w", err) } if len(templates) == 0 { fmt.Println("No templates saved") return nil } for _, t := range templates { subject := t.Subject if len(subject) > 50 { subject = subject[:47] + "..." } fmt.Printf(" %-20s %-50s %s\n", t.Name, subject, t.SavedAt) } return nil }, } return cmd } func templateUseCmd() *cobra.Command { var to string cmd := &cobra.Command{ Use: "use ", Short: "Use an email template", Long: `Load an email template and create a draft from it.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { templateName := args[0] cfgMgr := config.NewConfigManager() templates, err := loadTemplates(cfgMgr) if err != nil { return fmt.Errorf("failed to load templates: %w", err) } template, found := templates[templateName] if !found { return fmt.Errorf("template not found: %s", templateName) } var recipients []internalmail.Recipient if to != "" { recipients = parseRecipients(to) } session, sessionMgr, err := checkAuthenticatedWithManager() if err != nil { return err } cfg, err := cfgMgr.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) } client := api.NewProtonMailClient(cfg, sessionMgr) client.SetAuthHeader(session.AccessToken) mailClient := internalmail.NewClient(client) draft := internalmail.Draft{ To: recipients, Subject: template.Subject, Body: template.Body, } id, err := mailClient.SaveDraft(draft, session.MailPassphrase) if err != nil { return fmt.Errorf("failed to create draft from template: %w", err) } fmt.Printf("Draft created from template '%s' with ID: %s\n", templateName, id) return nil }, } cmd.Flags().StringVarP(&to, "to", "t", "", "Recipient addresses (comma-separated)") return cmd } func templateDeleteCmd() *cobra.Command { cmd := &cobra.Command{ Use: "delete ", Short: "Delete an email template", Long: `Delete a saved email template.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { templateName := args[0] cfgMgr := config.NewConfigManager() templates, err := loadTemplates(cfgMgr) if err != nil { return fmt.Errorf("failed to load templates: %w", err) } if _, found := templates[templateName]; !found { return fmt.Errorf("template not found: %s", templateName) } delete(templates, templateName) if err := saveTemplates(cfgMgr, templates); err != nil { return fmt.Errorf("failed to save templates: %w", err) } fmt.Printf("Template '%s' deleted\n", templateName) return nil }, } return cmd } type DraftTemplate struct { Name string `json:"name"` Subject string `json:"subject"` Body string `json:"body"` SavedAt string `json:"saved_at"` } type LocalDraft struct { MessageID string `json:"message_id"` To []internalmail.Recipient `json:"to"` CC []internalmail.Recipient `json:"cc,omitempty"` BCC []internalmail.Recipient `json:"bcc,omitempty"` Subject string `json:"subject"` Body string `json:"body"` LastEdited int64 `json:"last_edited"` } func getAutoSaveConfigPath(cfgMgr *config.ConfigManager) string { return filepath.Join(cfgMgr.ConfigDir(), "autosave.json") } func loadAutoSaveConfig(cfgMgr *config.ConfigManager) (*internalmail.DraftAutoSaveConfig, error) { path := getAutoSaveConfigPath(cfgMgr) data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return &internalmail.DraftAutoSaveConfig{ Enabled: false, Interval: 30, }, nil } return nil, err } var config internalmail.DraftAutoSaveConfig if err := json.Unmarshal(data, &config); err != nil { return nil, err } return &config, nil } func saveAutoSaveConfig(cfgMgr *config.ConfigManager, autoSaveConfig *internalmail.DraftAutoSaveConfig) error { path := getAutoSaveConfigPath(cfgMgr) data, err := json.MarshalIndent(autoSaveConfig, "", " ") if err != nil { return err } if err := os.MkdirAll(cfgMgr.ConfigDir(), 0700); err != nil { return err } return os.WriteFile(path, data, 0600) } func getLocalDraftsPath(cfgMgr *config.ConfigManager) string { return filepath.Join(cfgMgr.ConfigDir(), "local-drafts.json") } func loadLocalDrafts(cfgMgr *config.ConfigManager) ([]LocalDraft, error) { path := getLocalDraftsPath(cfgMgr) data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return []LocalDraft{}, nil } return nil, err } var drafts []LocalDraft if err := json.Unmarshal(data, &drafts); err != nil { return nil, err } return drafts, nil } func saveLocalDrafts(cfgMgr *config.ConfigManager, drafts []LocalDraft) error { path := getLocalDraftsPath(cfgMgr) data, err := json.MarshalIndent(drafts, "", " ") if err != nil { return err } if err := os.MkdirAll(cfgMgr.ConfigDir(), 0700); err != nil { return err } return os.WriteFile(path, data, 0600) } func getLastEditedDraft(drafts []LocalDraft) string { var lastID string var lastTime int64 for _, d := range drafts { if d.LastEdited > lastTime { lastTime = d.LastEdited lastID = d.MessageID if lastID == "" { lastID = fmt.Sprintf("local-%d", d.LastEdited) } } } return lastID } func findLocalDraft(drafts []LocalDraft, id string) (LocalDraft, bool) { for _, d := range drafts { if d.MessageID == id { return d, true } } return LocalDraft{}, false } func getTemplatesPath(cfgMgr *config.ConfigManager) string { return filepath.Join(cfgMgr.ConfigDir(), "templates.json") } func loadTemplates(cfgMgr *config.ConfigManager) (map[string]DraftTemplate, error) { path := getTemplatesPath(cfgMgr) data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return map[string]DraftTemplate{}, nil } return nil, err } var templates map[string]DraftTemplate if err := json.Unmarshal(data, &templates); err != nil { return nil, err } return templates, nil } func saveTemplates(cfgMgr *config.ConfigManager, templates map[string]DraftTemplate) error { path := getTemplatesPath(cfgMgr) data, err := json.MarshalIndent(templates, "", " ") if err != nil { return err } if err := os.MkdirAll(cfgMgr.ConfigDir(), 0700); err != nil { return err } return os.WriteFile(path, data, 0600) }