package emailcli import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // Client 是访问 Email Server 后端的 HTTP 客户端。 // 同一个 Client 可以同时持有 ServiceToken 与 AppKey/Secret, // 两种头会同时带上;一般建议按用途分别创建实例。 // // 构造方式: // - NewServiceClient: 管理端(Authorization: Bearer ) // - NewAppClient: 发件端 (X-App-Key / X-App-Secret) type Client struct { baseURL string // 基础地址,不带尾斜杠,例如 https://api.example.com serviceToken string // ServiceAuth 令牌(管理接口) appKey string // AppAuth AppKey(发件接口) appSecret string // AppAuth AppSecret httpClient *http.Client // 底层 HTTP 客户端,默认超时 30s } // Option 用于在构造 Client 时传入可选配置。 type Option func(*Client) // WithHTTPClient 使用自定义 *http.Client,例如配置代理、传输层、证书等。 func WithHTTPClient(hc *http.Client) Option { return func(c *Client) { c.httpClient = hc } } // WithTimeout 修改底层 HTTP 客户端的超时(默认 30 秒)。 // // 注意:如果同时使用 WithHTTPClient,请确保先设置自定义客户端再应用超时。 func WithTimeout(d time.Duration) Option { return func(c *Client) { c.httpClient.Timeout = d } } // NewServiceClient 创建一个管理端客户端。 // // 参数: // - baseURL: 后端根地址,例如 "https://mail.example.com" // - serviceToken: 后端环境变量 SERVICE_TOKEN 的值 // - opts: 可选项(WithHTTPClient / WithTimeout) // // 所有请求会自动带上 Authorization: Bearer 头, // 覆盖账号、签名、配额、通道、发信、审核、队列、健康检查等全部管理接口。 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 创建一个发件客户端。 // // 参数: // - baseURL: 后端根地址 // - appKey / appSecret: 管理端创建账号时返回的凭据 // - opts: 可选项(WithHTTPClient / WithTimeout) // // 只能用于调用 SendMail 接口。若要调用管理接口,请改用 NewServiceClient。 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 } // APIResponse 是后端统一响应体。 // // { "code": 200, "message": "ok", "data": ... } // // SDK 内部会解包 Data,正常情况下调用方无需直接使用该类型。 type APIResponse[T any] struct { Code int `json:"code"` Message string `json:"message"` Data T `json:"data,omitempty"` } // APIError 表示后端业务错误(HTTP 状态码可能仍是 200,但 code != 200)。 // // 使用 errors.As 判断: // // var apiErr *emailcli.APIError // if errors.As(err, &apiErr) { ... } type APIError struct { Code int // 业务错误码(非 HTTP 状态码) Message string // 错误描述 } // Error 实现 error 接口。 func (e *APIError) Error() string { return fmt.Sprintf("api error %d: %s", e.Code, e.Message) } // url 拼接出完整请求 URL。 func (c *Client) url(path string) string { return c.baseURL + path } // setAuth 根据客户端类型自动写入认证头。 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) } } // doRequest 是所有 API 的底层实现:组装请求 → 发送 → 解包 APIResponse[T]。 // // 参数说明: // - method: HTTP 方法(GET/POST/PUT/DELETE) // - path: API 路径(以 / 开头),例如 /api/v1/accounts // - body: 请求体(会被 json.Marshal),无则传 nil // - query: URL 查询参数,通常由 buildQuery 生成 // // 返回:解包后的 data 字段。code != 200 时返回 *APIError。 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 } // get 是 doRequest 的 GET 便捷函数。 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) } // post 是 doRequest 的 POST 便捷函数。 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) } // put 是 doRequest 的 PUT 便捷函数。 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) } // del 是 doRequest 的 DELETE 便捷函数。 func del[T any](c *Client, ctx context.Context, path string) (T, error) { return doRequest[T](c, ctx, http.MethodDelete, path, nil, nil) }