Files
pop/internal/api/client.go
Michael Freno 7bbba9f15c FRE-683: Add contacts & attachments management
- Implemented contact CRUD operations (list, add, edit, delete)
- Implemented attachment management (list, upload, download)
- Created internal/contact/manager.go for contact persistence
- Created internal/attachment/manager.go for attachment storage
- Added CLI commands in cmd/contacts.go and cmd/attachments.go
- Integrated contact and attachment commands into root CLI

Files:
- internal/contact/types.go - Contact data models
- internal/contact/manager.go - Contact CRUD operations
- internal/attachment/manager.go - Attachment file operations
- cmd/contacts.go - Contact CLI commands
- cmd/attachments.go - Attachment CLI commands

Contacts stored in ~/.config/pop/contacts.json
Attachments stored in ~/.config/pop/attachments/
2026-04-26 10:26:29 -04:00

123 lines
2.6 KiB
Go

package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/frenocorp/pop/internal/config"
)
type ProtonMailClient struct {
baseURL string
httpClient *http.Client
config *config.Config
rateLimiter *RateLimiter
authHeader string
authMu sync.RWMutex
}
type RateLimiter struct {
mu sync.Mutex
requests []time.Time
limit int
window time.Duration
}
func NewProtonMailClient(cfg *config.Config) *ProtonMailClient {
return &ProtonMailClient{
baseURL: cfg.APIBaseURL,
httpClient: &http.Client{Timeout: time.Duration(cfg.TimeoutSec) * time.Second},
config: cfg,
rateLimiter: &RateLimiter{
requests: make([]time.Time, 0, cfg.RateLimitReq),
limit: cfg.RateLimitReq,
window: time.Duration(cfg.RateLimitWin) * time.Second,
},
}
}
func (c *ProtonMailClient) SetAuthHeader(token string) {
c.authMu.Lock()
defer c.authMu.Unlock()
c.authHeader = token
}
func (c *ProtonMailClient) getAuthHeader() string {
c.authMu.RLock()
defer c.authMu.RUnlock()
return c.authHeader
}
func (c *ProtonMailClient) GetBaseURL() string {
return c.baseURL
}
func (rl *RateLimiter) Wait() {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
windowStart := now.Add(-rl.window)
// Remove old requests outside the window
validRequests := make([]time.Time, 0, rl.limit)
for _, t := range rl.requests {
if t.After(windowStart) {
validRequests = append(validRequests, t)
}
}
rl.requests = validRequests
// Wait if at limit
if len(rl.requests) >= rl.limit {
sleep := rl.requests[0].Add(rl.window).Sub(now)
if sleep > 0 {
time.Sleep(sleep)
}
}
}
func (c *ProtonMailClient) Do(req *http.Request) (*http.Response, error) {
c.rateLimiter.Wait()
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.getAuthHeader()))
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
// Record the request
c.rateLimiter.mu.Lock()
c.rateLimiter.requests = append(c.rateLimiter.requests, time.Now())
c.rateLimiter.mu.Unlock()
// Check for API errors
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
var apiErr APIError
if err := json.Unmarshal(body, &apiErr); err == nil {
resp.Body = io.NopCloser(io.MultiReader(io.NopCloser(bytes.NewReader(body)), bytes.NewReader(body)))
return resp, &apiErr
}
}
return resp, nil
}
type APIError struct {
HTTPStatus int `json:"-"`
Code int `json:"Code,omitempty"`
Message string `json:"Message,omitempty"`
}
func (e *APIError) Error() string {
return fmt.Sprintf("API error %d: %s", e.HTTPStatus, e.Message)
}