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