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>
This commit is contained in:
235
internal/accounts/accounts.go
Normal file
235
internal/accounts/accounts.go
Normal file
@@ -0,0 +1,235 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user