package emailcli import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) type Client struct { baseURL string serviceToken string appKey string appSecret string httpClient *http.Client } type Option func(*Client) func WithHTTPClient(hc *http.Client) Option { return func(c *Client) { c.httpClient = hc } } func WithTimeout(d time.Duration) Option { return func(c *Client) { c.httpClient.Timeout = d } } // NewServiceClient creates a client authenticated with a service token (management APIs). func NewServiceClient(baseURL, serviceToken string, opts ...Option) *Client { c := &Client{ baseURL: strings.TrimRight(baseURL, "/"), serviceToken: serviceToken, httpClient: &http.Client{Timeout: 30 * time.Second}, } for _, o := range opts { o(c) } return c } // NewAppClient creates a client authenticated with AppKey/AppSecret (mail sending API). func NewAppClient(baseURL, appKey, appSecret string, opts ...Option) *Client { c := &Client{ baseURL: strings.TrimRight(baseURL, "/"), appKey: appKey, appSecret: appSecret, httpClient: &http.Client{Timeout: 30 * time.Second}, } for _, o := range opts { o(c) } return c } type APIResponse[T any] struct { Code int `json:"code"` Message string `json:"message"` Data T `json:"data,omitempty"` } type APIError struct { Code int Message string } func (e *APIError) Error() string { return fmt.Sprintf("api error %d: %s", e.Code, e.Message) } func (c *Client) url(path string) string { return c.baseURL + path } func (c *Client) setAuth(req *http.Request) { if c.serviceToken != "" { req.Header.Set("Authorization", "Bearer "+c.serviceToken) } if c.appKey != "" { req.Header.Set("X-App-Key", c.appKey) req.Header.Set("X-App-Secret", c.appSecret) } } func doRequest[T any](c *Client, ctx context.Context, method, path string, body interface{}, query url.Values) (T, error) { var zero T var bodyReader io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return zero, fmt.Errorf("marshal body: %w", err) } bodyReader = bytes.NewReader(b) } reqURL := c.url(path) if len(query) > 0 { reqURL += "?" + query.Encode() } req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader) if err != nil { return zero, fmt.Errorf("new request: %w", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } c.setAuth(req) resp, err := c.httpClient.Do(req) if err != nil { return zero, fmt.Errorf("do request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return zero, fmt.Errorf("read response: %w", err) } var apiResp APIResponse[T] if err := json.Unmarshal(respBody, &apiResp); err != nil { return zero, fmt.Errorf("unmarshal response (status %d): %w\nbody: %s", resp.StatusCode, err, string(respBody)) } if apiResp.Code != 200 { return zero, &APIError{Code: apiResp.Code, Message: apiResp.Message} } return apiResp.Data, nil } func get[T any](c *Client, ctx context.Context, path string, query url.Values) (T, error) { return doRequest[T](c, ctx, http.MethodGet, path, nil, query) } func post[T any](c *Client, ctx context.Context, path string, body interface{}) (T, error) { return doRequest[T](c, ctx, http.MethodPost, path, body, nil) } func put[T any](c *Client, ctx context.Context, path string, body interface{}) (T, error) { return doRequest[T](c, ctx, http.MethodPut, path, body, nil) } func del[T any](c *Client, ctx context.Context, path string) (T, error) { return doRequest[T](c, ctx, http.MethodDelete, path, nil, nil) }