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>
236 lines
5.5 KiB
Go
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")
|
|
}
|