package accounts import ( "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/frenocorp/pop/internal/config" ) // Account represents a named ProtonMail account profile. type Account struct { Name string `json:"name"` Email string `json:"email"` UID string `json:"uid,omitempty"` APIBaseURL string `json:"api_base_url"` Default bool `json:"default"` CreatedAt string `json:"created_at"` LastUsedAt string `json:"last_used_at,omitempty"` } // AccountsStore manages multiple named account profiles. type AccountsStore struct { configDir string accountsFile string } // NewAccountsStore creates a new store for managing multiple accounts. func NewAccountsStore() (*AccountsStore, error) { cfg := config.NewConfigManager() configDir := cfg.ConfigDir() return &AccountsStore{ configDir: configDir, accountsFile: filepath.Join(configDir, "accounts.json"), }, nil } // LoadAccounts reads all stored accounts from disk. func (s *AccountsStore) LoadAccounts() ([]Account, error) { if err := os.MkdirAll(s.configDir, 0700); err != nil { return nil, fmt.Errorf("failed to create config dir: %w", err) } data, err := os.ReadFile(s.accountsFile) if err != nil { if os.IsNotExist(err) { return []Account{}, nil } return nil, fmt.Errorf("failed to read accounts file: %w", err) } var accounts []Account if err := json.Unmarshal(data, &accounts); err != nil { return nil, fmt.Errorf("failed to parse accounts: %w", err) } return accounts, nil } // SaveAccounts writes all accounts to disk. func (s *AccountsStore) SaveAccounts(accounts []Account) error { if err := os.MkdirAll(s.configDir, 0700); err != nil { return fmt.Errorf("failed to create config dir: %w", err) } data, err := json.MarshalIndent(accounts, "", " ") if err != nil { return fmt.Errorf("failed to marshal accounts: %w", err) } if err := os.WriteFile(s.accountsFile, data, 0600); err != nil { return fmt.Errorf("failed to write accounts file: %w", err) } return nil } // AddAccount adds a new account profile. If name conflicts, returns an error. func (s *AccountsStore) AddAccount(name, email, apiBaseURL string, isDefault bool) error { accounts, err := s.LoadAccounts() if err != nil { return err } for _, acc := range accounts { if acc.Name == name { return fmt.Errorf("account with name %q already exists", name) } } if apiBaseURL == "" { apiBaseURL = "https://api.protonmail.ch" } account := Account{ Name: name, Email: email, APIBaseURL: apiBaseURL, Default: isDefault, CreatedAt: time.Now().UTC().Format(time.RFC3339), } if isDefault { for i := range accounts { accounts[i].Default = false } } accounts = append(accounts, account) return s.SaveAccounts(accounts) } // GetAccount retrieves an account by name. Falls back to default if name is empty. func (s *AccountsStore) GetAccount(name string) (*Account, error) { accounts, err := s.LoadAccounts() if err != nil { return nil, err } if name != "" { for _, acc := range accounts { if acc.Name == name { return &acc, nil } } return nil, fmt.Errorf("account %q not found", name) } for _, acc := range accounts { if acc.Default { return &acc, nil } } return nil, fmt.Errorf("no default account set (use 'pop accounts add' to create one)") } // UpdateAccount updates an existing account's fields. func (s *AccountsStore) UpdateAccount(name string, email, apiBaseURL *string, isDefault *bool) (*Account, error) { accounts, err := s.LoadAccounts() if err != nil { return nil, err } found := false for i, acc := range accounts { if acc.Name == name { if email != nil { accounts[i].Email = *email } if apiBaseURL != nil { accounts[i].APIBaseURL = *apiBaseURL } if isDefault != nil && *isDefault { for j := range accounts { accounts[j].Default = false } accounts[i].Default = true } found = true accounts[i].LastUsedAt = time.Now().UTC().Format(time.RFC3339) if err := s.SaveAccounts(accounts); err != nil { return nil, err } return &accounts[i], nil } } if !found { return nil, fmt.Errorf("account %q not found", name) } return nil, nil } // SetDefaultAccount sets the default account by name. func (s *AccountsStore) SetDefaultAccount(name string) error { accounts, err := s.LoadAccounts() if err != nil { return err } found := false for i, acc := range accounts { if acc.Name == name { accounts[i].Default = true found = true } else { accounts[i].Default = false } } if !found { return fmt.Errorf("account %q not found", name) } return s.SaveAccounts(accounts) } // RemoveAccount removes an account by name. func (s *AccountsStore) RemoveAccount(name string) error { accounts, err := s.LoadAccounts() if err != nil { return err } newAccounts := make([]Account, 0, len(accounts)) found := false for _, acc := range accounts { if acc.Name == name { found = true continue } newAccounts = append(newAccounts, acc) } if !found { return fmt.Errorf("account %q not found", name) } return s.SaveAccounts(newAccounts) } // AccountSessionDir returns the session directory for a given account name. func AccountSessionDir(configDir, accountName string) string { return filepath.Join(configDir, "accounts", accountName) } // AccountSessionFile returns the session file path for a given account. func AccountSessionFile(configDir, accountName string) string { return filepath.Join(AccountSessionDir(configDir, accountName), "session.json") }