Files
pop/internal/accounts/accounts.go
Michael Freno bf26cd3ed6 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>
2026-05-14 00:40:24 -04:00

236 lines
5.5 KiB
Go

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")
}