From 5b4774e9c12d7f20e169d09c31e92c7b58d2773c Mon Sep 17 00:00:00 2001 From: shiran Date: Sat, 18 Apr 2026 10:36:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=BA=93?= =?UTF-8?q?=E7=9A=84=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增完整的Go客户端库实现,支持邮件服务器API的各种操作 - 实现账户管理、签名管理、邮件发送、审计、配额、通道等功能模块 - 提供ServiceAuth和AppAuth两种认证模式的客户端 - 添加详细的README文档,包含安装指南和使用示例 - 配置.gitignore文件以忽略构建产物和开发工具配置 - 支持分页查询、错误处理和客户端选项配置 --- .gitignore | 10 ++ README.md | 200 +++++++++++++++++++++ account.go | 36 ++++ audit.go | 54 ++++++ audit_rule.go | 35 ++++ channel.go | 27 +++ check.go | 23 +++ client.go | 153 ++++++++++++++++ go.mod | 3 + mail.go | 33 ++++ query.go | 67 +++++++ queue.go | 29 +++ quota.go | 32 ++++ sender.go | 27 +++ signature.go | 38 ++++ types.go | 484 ++++++++++++++++++++++++++++++++++++++++++++++++++ 16 files changed, 1251 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 account.go create mode 100644 audit.go create mode 100644 audit_rule.go create mode 100644 channel.go create mode 100644 check.go create mode 100644 client.go create mode 100644 go.mod create mode 100644 mail.go create mode 100644 query.go create mode 100644 queue.go create mode 100644 quota.go create mode 100644 sender.go create mode 100644 signature.go create mode 100644 types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5a70b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +vendor/ +.idea/ +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3fe4fb --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# email-serverr-cli + +Go client library for the Email Server API. + +## Install + +```bash +go get gitea.s1f.ren/shiran/email-serverr-cli +``` + +## Quick Start + +### Management Client (ServiceAuth) + +```go +package main + +import ( + "context" + "fmt" + emailcli "gitea.s1f.ren/shiran/email-serverr-cli" +) + +func main() { + client := emailcli.NewServiceClient( + "https://your-server.com", + "your-service-token", + ) + + // List accounts + accounts, err := client.ListAccounts(context.Background(), emailcli.AccountListQuery{ + PaginationQuery: emailcli.PaginationQuery{Page: 1, PageSize: 20}, + }) + if err != nil { + panic(err) + } + for _, a := range accounts.List { + fmt.Printf("Account: %s (ID: %d)\n", a.Name, a.ID) + } +} +``` + +### Mail Sending Client (AppAuth) + +```go +client := emailcli.NewAppClient( + "https://your-server.com", + "your-app-key", + "your-app-secret", +) + +resp, err := client.SendMail(context.Background(), emailcli.SendMailReq{ + To: []string{"recipient@example.com"}, + Subject: "Hello", + Body: "

Hello World

", + Channel: "default", +}) +if err != nil { + panic(err) +} +fmt.Printf("Mail sent: log_id=%d status=%s\n", resp.MailLogID, resp.Status) +``` + +## Authentication + +| Mode | Constructor | Header | Use Case | +|------|-------------|--------|----------| +| ServiceAuth | `NewServiceClient` | `Authorization: Bearer ` | Management APIs | +| AppAuth | `NewAppClient` | `X-App-Key` + `X-App-Secret` | Mail sending | + +## API Reference + +### Mail (AppAuth) + +| Method | Description | +|--------|-------------| +| `SendMail` | Send an email | + +### Accounts (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `CreateAccount` | Create mail account | +| `ListAccounts` | List accounts with filters | +| `GetAccount` | Get account details | +| `UpdateAccount` | Update account | +| `DeleteAccount` | Delete account | +| `ResetAccountSecret` | Reset app secret | + +### Signatures (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `CreateSignature` | Create signature | +| `ListSignatures` | List signatures with filters | +| `GetSignature` | Get signature details | +| `UpdateSignature` | Update signature | +| `DeleteSignature` | Delete signature | +| `AuditSignature` | Approve or reject signature | + +### Mail Logs (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `ListMailLogs` | List mail logs with filters | +| `GetMailLog` | Get mail log detail with body | +| `GetMailStats` | Get status statistics | + +### Quotas (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `CreateQuota` | Create quota | +| `ListQuotas` | List quotas with filters | +| `GetQuotaSummary` | Get quota summary for account | +| `UpdateQuota` | Update quota | +| `DeleteQuota` | Delete quota | + +### Channels (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `CreateChannel` | Create channel | +| `ListChannels` | List channels with filters | +| `UpdateChannel` | Update channel | +| `DeleteChannel` | Delete channel | + +### Sender Accounts (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `CreateSender` | Create sender under channel | +| `ListSendersByChannel` | List senders for channel | +| `UpdateSender` | Update sender | +| `DeleteSender` | Delete sender | + +### Audits (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `ListAuditPending` | List pending audit items | +| `GetAuditPendingDetail` | Get pending item detail | +| `ApproveAudit` | Approve single item | +| `RejectAudit` | Reject single item | +| `BatchApproveAudit` | Batch approve | +| `BatchRejectAudit` | Batch reject | +| `ListAuditLogs` | List audit history | +| `GetAuditStats` | Get audit statistics | + +### Audit Rules (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `CreateAuditRule` | Create rule | +| `ListAuditRules` | List all rules | +| `GetAuditRule` | Get rule details | +| `UpdateAuditRule` | Update rule | +| `DeleteAuditRule` | Delete rule | +| `UpdateAuditRuleStatus` | Toggle rule status | +| `TestAuditRule` | Test rules against sample | + +### Queue (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `GetQueueStatus` | Get queue lengths | +| `ListQueuePending` | List pending queue items | +| `CancelQueueItem` | Cancel queued mail | +| `RetryQueueItem` | Retry failed mail | + +### Health Checks (ServiceAuth) + +| Method | Description | +|--------|-------------| +| `ListCheckLogs` | List check logs | +| `GetCheckSummary` | Get sender health summary | +| `TriggerCheck` | Trigger health check | + +## Error Handling + +```go +resp, err := client.SendMail(ctx, req) +if err != nil { + var apiErr *emailcli.APIError + if errors.As(err, &apiErr) { + fmt.Printf("API error: code=%d message=%s\n", apiErr.Code, apiErr.Message) + } +} +``` + +## Options + +```go +client := emailcli.NewServiceClient( + "https://your-server.com", + "token", + emailcli.WithTimeout(60 * time.Second), + emailcli.WithHTTPClient(customClient), +) +``` diff --git a/account.go b/account.go new file mode 100644 index 0000000..30ccaa5 --- /dev/null +++ b/account.go @@ -0,0 +1,36 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) CreateAccount(ctx context.Context, req CreateAccountReq) (*CreateAccountResp, error) { + return post[*CreateAccountResp](c, ctx, "/api/v1/accounts", req) +} + +func (c *Client) ListAccounts(ctx context.Context, q AccountListQuery) (*PaginationResult[Account], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "user_id": q.UserID, + "status": q.Status, + "keyword": q.Keyword, + }) + return get[*PaginationResult[Account]](c, ctx, "/api/v1/accounts", buildQuery(params)) +} + +func (c *Client) GetAccount(ctx context.Context, id uint) (*Account, error) { + return get[*Account](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id), nil) +} + +func (c *Client) UpdateAccount(ctx context.Context, id uint, req UpdateAccountReq) (*Account, error) { + return put[*Account](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id), req) +} + +func (c *Client) DeleteAccount(ctx context.Context, id uint) error { + _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/accounts/%d", id)) + return err +} + +func (c *Client) ResetAccountSecret(ctx context.Context, id uint) (*ResetSecretResp, error) { + return post[*ResetSecretResp](c, ctx, fmt.Sprintf("/api/v1/accounts/%d/reset-secret", id), nil) +} diff --git a/audit.go b/audit.go new file mode 100644 index 0000000..932d57d --- /dev/null +++ b/audit.go @@ -0,0 +1,54 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) ListAuditPending(ctx context.Context, q AuditPendingQuery) (*PaginationResult[MailLog], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "user_id": q.UserID, + "account_id": q.AccountID, + "keyword": q.Keyword, + }) + return get[*PaginationResult[MailLog]](c, ctx, "/api/v1/audits/pending", buildQuery(params)) +} + +func (c *Client) GetAuditPendingDetail(ctx context.Context, id uint) (*MailLogDetail, error) { + return get[*MailLogDetail](c, ctx, fmt.Sprintf("/api/v1/audits/pending/%d", id), nil) +} + +func (c *Client) ApproveAudit(ctx context.Context, id uint) error { + _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/audits/%d/approve", id), nil) + return err +} + +func (c *Client) RejectAudit(ctx context.Context, id uint, req AuditRejectReq) error { + _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/audits/%d/reject", id), req) + return err +} + +func (c *Client) BatchApproveAudit(ctx context.Context, req BatchAuditApproveReq) error { + _, err := post[any](c, ctx, "/api/v1/audits/batch/approve", req) + return err +} + +func (c *Client) BatchRejectAudit(ctx context.Context, req BatchAuditRejectReq) error { + _, err := post[any](c, ctx, "/api/v1/audits/batch/reject", req) + return err +} + +func (c *Client) ListAuditLogs(ctx context.Context, q AuditLogQuery) (*PaginationResult[MailAudit], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "audit_type": q.AuditType, + "action": q.Action, + "user_id": q.UserID, + "start_date": q.StartDate, + "end_date": q.EndDate, + }) + return get[*PaginationResult[MailAudit]](c, ctx, "/api/v1/audits/logs", buildQuery(params)) +} + +func (c *Client) GetAuditStats(ctx context.Context) (*AuditStats, error) { + return get[*AuditStats](c, ctx, "/api/v1/audits/stats", nil) +} diff --git a/audit_rule.go b/audit_rule.go new file mode 100644 index 0000000..2a24369 --- /dev/null +++ b/audit_rule.go @@ -0,0 +1,35 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) CreateAuditRule(ctx context.Context, req CreateAuditRuleReq) (*AuditRule, error) { + return post[*AuditRule](c, ctx, "/api/v1/audit-rules", req) +} + +func (c *Client) ListAuditRules(ctx context.Context) ([]AuditRule, error) { + return get[[]AuditRule](c, ctx, "/api/v1/audit-rules", nil) +} + +func (c *Client) GetAuditRule(ctx context.Context, id uint) (*AuditRule, error) { + return get[*AuditRule](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d", id), nil) +} + +func (c *Client) UpdateAuditRule(ctx context.Context, id uint, req UpdateAuditRuleReq) (*AuditRule, error) { + return put[*AuditRule](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d", id), req) +} + +func (c *Client) DeleteAuditRule(ctx context.Context, id uint) error { + _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d", id)) + return err +} + +func (c *Client) UpdateAuditRuleStatus(ctx context.Context, id uint, status int8) (*AuditRule, error) { + return put[*AuditRule](c, ctx, fmt.Sprintf("/api/v1/audit-rules/%d/status", id), map[string]int8{"status": status}) +} + +func (c *Client) TestAuditRule(ctx context.Context, req TestAuditRuleReq) (*TestAuditRuleResp, error) { + return post[*TestAuditRuleResp](c, ctx, "/api/v1/audit-rules/test", req) +} diff --git a/channel.go b/channel.go new file mode 100644 index 0000000..0b88bef --- /dev/null +++ b/channel.go @@ -0,0 +1,27 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) CreateChannel(ctx context.Context, req CreateChannelReq) (*Channel, error) { + return post[*Channel](c, ctx, "/api/v1/channels", req) +} + +func (c *Client) ListChannels(ctx context.Context, q ChannelListQuery) (*PaginationResult[Channel], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "status": q.Status, + "keyword": q.Keyword, + }) + return get[*PaginationResult[Channel]](c, ctx, "/api/v1/channels", buildQuery(params)) +} + +func (c *Client) UpdateChannel(ctx context.Context, id uint, req UpdateChannelReq) (*Channel, error) { + return put[*Channel](c, ctx, fmt.Sprintf("/api/v1/channels/%d", id), req) +} + +func (c *Client) DeleteChannel(ctx context.Context, id uint) error { + _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/channels/%d", id)) + return err +} diff --git a/check.go b/check.go new file mode 100644 index 0000000..715d7ed --- /dev/null +++ b/check.go @@ -0,0 +1,23 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) ListCheckLogs(ctx context.Context, q CheckLogQuery) (*PaginationResult[CheckLog], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "sender_account_id": q.SenderAccountID, + "start_date": q.StartDate, + "end_date": q.EndDate, + }) + return get[*PaginationResult[CheckLog]](c, ctx, "/api/v1/check-logs", buildQuery(params)) +} + +func (c *Client) GetCheckSummary(ctx context.Context) ([]SenderHealth, error) { + return get[[]SenderHealth](c, ctx, "/api/v1/check-logs/summary", nil) +} + +func (c *Client) TriggerCheck(ctx context.Context, senderAccountID uint) (*TriggerCheckResp, error) { + return post[*TriggerCheckResp](c, ctx, fmt.Sprintf("/api/v1/check-logs/trigger/%d", senderAccountID), nil) +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..8a5c18d --- /dev/null +++ b/client.go @@ -0,0 +1,153 @@ +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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f8c3be5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.s1f.ren/shiran/email-serverr-cli + +go 1.26.2 diff --git a/mail.go b/mail.go new file mode 100644 index 0000000..e05cfdb --- /dev/null +++ b/mail.go @@ -0,0 +1,33 @@ +package emailcli + +import ( + "context" + "fmt" +) + +// SendMail sends an email. Requires an AppClient (X-App-Key/X-App-Secret auth). +func (c *Client) SendMail(ctx context.Context, req SendMailReq) (*SendMailResp, error) { + return post[*SendMailResp](c, ctx, "/api/v1/mail/send", req) +} + +// ListMailLogs lists mail log records with filters (ServiceAuth). +func (c *Client) ListMailLogs(ctx context.Context, q MailLogListQuery) (*PaginationResult[MailLog], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "user_id": q.UserID, + "account_id": q.AccountID, + "status": q.Status, + "start_date": q.StartDate, + "end_date": q.EndDate, + "to": q.To, + "keyword": q.Keyword, + }) + return get[*PaginationResult[MailLog]](c, ctx, "/api/v1/mail-logs", buildQuery(params)) +} + +func (c *Client) GetMailLog(ctx context.Context, id uint) (*MailLogDetail, error) { + return get[*MailLogDetail](c, ctx, fmt.Sprintf("/api/v1/mail-logs/%d", id), nil) +} + +func (c *Client) GetMailStats(ctx context.Context) ([]MailStatItem, error) { + return get[[]MailStatItem](c, ctx, "/api/v1/mail-logs/stats", nil) +} diff --git a/query.go b/query.go new file mode 100644 index 0000000..1bd9cee --- /dev/null +++ b/query.go @@ -0,0 +1,67 @@ +package emailcli + +import ( + "fmt" + "net/url" +) + +func buildQuery(params map[string]interface{}) url.Values { + q := url.Values{} + for k, v := range params { + if v == nil { + continue + } + switch val := v.(type) { + case string: + if val != "" { + q.Set(k, val) + } + case int: + if val != 0 { + q.Set(k, fmt.Sprintf("%d", val)) + } + case int8: + q.Set(k, fmt.Sprintf("%d", val)) + case uint: + if val != 0 { + q.Set(k, fmt.Sprintf("%d", val)) + } + case *int: + if val != nil { + q.Set(k, fmt.Sprintf("%d", *val)) + } + case *int8: + if val != nil { + q.Set(k, fmt.Sprintf("%d", *val)) + } + case *uint: + if val != nil { + q.Set(k, fmt.Sprintf("%d", *val)) + } + default: + q.Set(k, fmt.Sprintf("%v", v)) + } + } + return q +} + +func paginationParams(p PaginationQuery) map[string]interface{} { + m := map[string]interface{}{} + if p.Page > 0 { + m["page"] = p.Page + } + if p.PageSize > 0 { + m["page_size"] = p.PageSize + } + return m +} + +func mergeParams(maps ...map[string]interface{}) map[string]interface{} { + result := map[string]interface{}{} + for _, m := range maps { + for k, v := range m { + result[k] = v + } + } + return result +} diff --git a/queue.go b/queue.go new file mode 100644 index 0000000..638e2d1 --- /dev/null +++ b/queue.go @@ -0,0 +1,29 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) GetQueueStatus(ctx context.Context) (*QueueStatusData, error) { + return get[*QueueStatusData](c, ctx, "/api/v1/queue/status", nil) +} + +func (c *Client) ListQueuePending(ctx context.Context, q QueuePendingQuery) (*PaginationResult[MailLog], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "channel_id": q.ChannelID, + "user_id": q.UserID, + "account_id": q.AccountID, + }) + return get[*PaginationResult[MailLog]](c, ctx, "/api/v1/queue/pending", buildQuery(params)) +} + +func (c *Client) CancelQueueItem(ctx context.Context, mailLogID uint) error { + _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/queue/%d/cancel", mailLogID), nil) + return err +} + +func (c *Client) RetryQueueItem(ctx context.Context, mailLogID uint) error { + _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/queue/%d/retry", mailLogID), nil) + return err +} diff --git a/quota.go b/quota.go new file mode 100644 index 0000000..4767a73 --- /dev/null +++ b/quota.go @@ -0,0 +1,32 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) CreateQuota(ctx context.Context, req CreateQuotaReq) (*MailQuota, error) { + return post[*MailQuota](c, ctx, "/api/v1/quotas", req) +} + +func (c *Client) ListQuotas(ctx context.Context, q QuotaListQuery) (*PaginationResult[MailQuota], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "account_id": q.AccountID, + "user_id": q.UserID, + "status": q.Status, + }) + return get[*PaginationResult[MailQuota]](c, ctx, "/api/v1/quotas", buildQuery(params)) +} + +func (c *Client) GetQuotaSummary(ctx context.Context, accountID uint) (*QuotaSummary, error) { + return get[*QuotaSummary](c, ctx, fmt.Sprintf("/api/v1/quotas/summary/%d", accountID), nil) +} + +func (c *Client) UpdateQuota(ctx context.Context, id uint, req UpdateQuotaReq) (*MailQuota, error) { + return put[*MailQuota](c, ctx, fmt.Sprintf("/api/v1/quotas/%d", id), req) +} + +func (c *Client) DeleteQuota(ctx context.Context, id uint) error { + _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/quotas/%d", id)) + return err +} diff --git a/sender.go b/sender.go new file mode 100644 index 0000000..d0df56b --- /dev/null +++ b/sender.go @@ -0,0 +1,27 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) CreateSender(ctx context.Context, channelID uint, req CreateSenderReq) (*SenderAccount, error) { + return post[*SenderAccount](c, ctx, fmt.Sprintf("/api/v1/channels/%d/senders", channelID), req) +} + +func (c *Client) ListSendersByChannel(ctx context.Context, channelID uint, q SenderListQuery) (*PaginationResult[SenderAccount], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "status": q.Status, + "keyword": q.Keyword, + }) + return get[*PaginationResult[SenderAccount]](c, ctx, fmt.Sprintf("/api/v1/channels/%d/senders", channelID), buildQuery(params)) +} + +func (c *Client) UpdateSender(ctx context.Context, id uint, req UpdateSenderReq) (*SenderAccount, error) { + return put[*SenderAccount](c, ctx, fmt.Sprintf("/api/v1/senders/%d", id), req) +} + +func (c *Client) DeleteSender(ctx context.Context, id uint) error { + _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/senders/%d", id)) + return err +} diff --git a/signature.go b/signature.go new file mode 100644 index 0000000..aeeea8e --- /dev/null +++ b/signature.go @@ -0,0 +1,38 @@ +package emailcli + +import ( + "context" + "fmt" +) + +func (c *Client) CreateSignature(ctx context.Context, req CreateSignatureReq) (*Signature, error) { + return post[*Signature](c, ctx, "/api/v1/signatures", req) +} + +func (c *Client) ListSignatures(ctx context.Context, q SignatureListQuery) (*PaginationResult[Signature], error) { + params := mergeParams(paginationParams(q.PaginationQuery), map[string]interface{}{ + "account_id": q.AccountID, + "status": q.Status, + "user_id": q.UserID, + "keyword": q.Keyword, + }) + return get[*PaginationResult[Signature]](c, ctx, "/api/v1/signatures", buildQuery(params)) +} + +func (c *Client) GetSignature(ctx context.Context, id uint) (*Signature, error) { + return get[*Signature](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id), nil) +} + +func (c *Client) UpdateSignature(ctx context.Context, id uint, req UpdateSignatureReq) (*Signature, error) { + return put[*Signature](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id), req) +} + +func (c *Client) DeleteSignature(ctx context.Context, id uint) error { + _, err := del[any](c, ctx, fmt.Sprintf("/api/v1/signatures/%d", id)) + return err +} + +func (c *Client) AuditSignature(ctx context.Context, id uint, req AuditSignatureReq) error { + _, err := post[any](c, ctx, fmt.Sprintf("/api/v1/signatures/%d/audit", id), req) + return err +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..e9ca42f --- /dev/null +++ b/types.go @@ -0,0 +1,484 @@ +package emailcli + +import "time" + +// PaginationResult is the generic paginated response wrapper. +type PaginationResult[T any] struct { + List []T `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +type PaginationQuery struct { + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` +} + +// --- GormModel fields --- + +type GormModel struct { + ID uint `json:"ID"` + CreatedAt time.Time `json:"CreatedAt"` + UpdatedAt time.Time `json:"UpdatedAt"` + DeletedAt *time.Time `json:"DeletedAt"` +} + +// --- Account --- + +type Account struct { + GormModel + UserID int `json:"user_id"` + Name string `json:"name"` + AppKey string `json:"app_key"` + Status int8 `json:"status"` + AuditMode int8 `json:"audit_mode"` + RateLimit int `json:"rate_limit"` + AllowedChannels string `json:"allowed_channels"` + DefaultSignatureID *uint `json:"default_signature_id"` + Remark string `json:"remark"` +} + +type CreateAccountReq struct { + UserID int `json:"user_id"` + Name string `json:"name"` + AuditMode *int8 `json:"audit_mode,omitempty"` + RateLimit *int `json:"rate_limit,omitempty"` + Remark string `json:"remark,omitempty"` +} + +type CreateAccountResp struct { + ID uint `json:"id"` + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` + Name string `json:"name"` +} + +type UpdateAccountReq struct { + Name *string `json:"name,omitempty"` + Status *int8 `json:"status,omitempty"` + AuditMode *int8 `json:"audit_mode,omitempty"` + RateLimit *int `json:"rate_limit,omitempty"` + AllowedChannels *string `json:"allowed_channels,omitempty"` + DefaultSignatureID *uint `json:"default_signature_id,omitempty"` + Remark *string `json:"remark,omitempty"` +} + +type AccountListQuery struct { + PaginationQuery + UserID *int `json:"user_id,omitempty"` + Status *int8 `json:"status,omitempty"` + Keyword string `json:"keyword,omitempty"` +} + +type ResetSecretResp struct { + AppSecret string `json:"app_secret"` +} + +// --- Signature --- + +type Signature struct { + GormModel + UserID int `json:"user_id"` + AccountID *uint `json:"account_id"` + Title string `json:"title"` + EnglishName string `json:"english_name"` + Content string `json:"content"` + Applicant string `json:"applicant"` + ApplicantInfo string `json:"applicant_info"` + Status int8 `json:"status"` + RejectReason string `json:"reject_reason"` + Auditor string `json:"auditor"` + AuditedAt *time.Time `json:"audited_at"` +} + +type CreateSignatureReq struct { + UserID int `json:"user_id"` + AccountID *uint `json:"account_id,omitempty"` + Title string `json:"title"` + EnglishName string `json:"english_name"` + Content string `json:"content,omitempty"` + Applicant string `json:"applicant,omitempty"` + ApplicantInfo string `json:"applicant_info,omitempty"` +} + +type UpdateSignatureReq struct { + Title *string `json:"title,omitempty"` + EnglishName *string `json:"english_name,omitempty"` + Content *string `json:"content,omitempty"` + Applicant *string `json:"applicant,omitempty"` + ApplicantInfo *string `json:"applicant_info,omitempty"` +} + +type AuditSignatureReq struct { + Status int8 `json:"status"` + RejectReason string `json:"reject_reason,omitempty"` +} + +type SignatureListQuery struct { + PaginationQuery + AccountID *uint `json:"account_id,omitempty"` + Status *int8 `json:"status,omitempty"` + UserID *int `json:"user_id,omitempty"` + Keyword string `json:"keyword,omitempty"` +} + +// --- Mail --- + +type SendMailReq struct { + To []string `json:"to"` + Cc []string `json:"cc,omitempty"` + Bcc []string `json:"bcc,omitempty"` + Subject string `json:"subject"` + Body string `json:"body"` + ContentType string `json:"content_type,omitempty"` + Channel string `json:"channel"` + SignatureID *uint `json:"signature_id,omitempty"` + SignatureTitle string `json:"signature_title,omitempty"` + Attachments []AttachmentItem `json:"attachments,omitempty"` +} + +type AttachmentItem struct { + Filename string `json:"filename"` + Content string `json:"content"` +} + +type SendMailResp struct { + MailLogID uint `json:"mail_log_id"` + Status string `json:"status"` +} + +// --- Mail Log --- + +type MailLog struct { + GormModel + UserID int `json:"user_id"` + AccountID uint `json:"account_id"` + ChannelID *uint `json:"channel_id"` + SenderAccountID *uint `json:"sender_account_id"` + SignatureID *uint `json:"signature_id"` + MessageID string `json:"message_id"` + FromAddress string `json:"from_address"` + ToAddresses string `json:"to_addresses"` + CcAddresses string `json:"cc_addresses"` + BccAddresses string `json:"bcc_addresses"` + Subject string `json:"subject"` + ContentType string `json:"content_type"` + HasAttachment bool `json:"has_attachment"` + SourceIP string `json:"source_ip"` + SourceType string `json:"source_type"` + Status int8 `json:"status"` + RetryCount int `json:"retry_count"` + MaxRetry int `json:"max_retry"` + ErrorMessage string `json:"error_message"` + SentAt *time.Time `json:"sent_at"` +} + +type MailLogListQuery struct { + PaginationQuery + UserID *int `json:"user_id,omitempty"` + AccountID *uint `json:"account_id,omitempty"` + Status *int8 `json:"status,omitempty"` + StartDate string `json:"start_date,omitempty"` + EndDate string `json:"end_date,omitempty"` + To string `json:"to,omitempty"` + Keyword string `json:"keyword,omitempty"` +} + +type MailLogDetail struct { + Log MailLog `json:"log"` + Body string `json:"body"` +} + +type MailStatItem struct { + Status int8 `json:"status"` + Count int64 `json:"count"` +} + +// --- Quota --- + +type MailQuota struct { + GormModel + UserID int `json:"user_id"` + AccountID uint `json:"account_id"` + QuotaType int8 `json:"quota_type"` + Total int `json:"total"` + Used int `json:"used"` + ExpireAt *time.Time `json:"expire_at"` + CycleUnit string `json:"cycle_unit"` + CycleResetAt *time.Time `json:"cycle_reset_at"` + Status int8 `json:"status"` +} + +type CreateQuotaReq struct { + AccountID uint `json:"account_id"` + QuotaType int8 `json:"quota_type"` + Total int `json:"total"` + ExpireAt string `json:"expire_at,omitempty"` + CycleUnit string `json:"cycle_unit,omitempty"` + CycleResetAt string `json:"cycle_reset_at,omitempty"` +} + +type UpdateQuotaReq struct { + Total *int `json:"total,omitempty"` + Status *int8 `json:"status,omitempty"` +} + +type QuotaListQuery struct { + PaginationQuery + AccountID *uint `json:"account_id,omitempty"` + UserID *int `json:"user_id,omitempty"` + Status *int8 `json:"status,omitempty"` +} + +type QuotaSummary struct { + AccountID uint `json:"account_id"` + TotalQuota int `json:"total_quota"` + TotalUsed int `json:"total_used"` + TotalRemaining int `json:"total_remaining"` + Details []MailQuota `json:"details"` +} + +// --- Channel --- + +type Channel struct { + GormModel + Name string `json:"name"` + Code string `json:"code"` + Description string `json:"description"` + Strategy string `json:"strategy"` + Status int8 `json:"status"` +} + +type CreateChannelReq struct { + Name string `json:"name"` + Code string `json:"code"` + Description string `json:"description,omitempty"` + Strategy string `json:"strategy,omitempty"` +} + +type UpdateChannelReq struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Strategy *string `json:"strategy,omitempty"` + Status *int8 `json:"status,omitempty"` +} + +type ChannelListQuery struct { + PaginationQuery + Status *int8 `json:"status,omitempty"` + Keyword string `json:"keyword,omitempty"` +} + +// --- Sender Account --- + +type SenderAccount struct { + GormModel + ChannelID uint `json:"channel_id"` + Name string `json:"name"` + SmtpHost string `json:"smtp_host"` + SmtpPort int `json:"smtp_port"` + SmtpUser string `json:"smtp_user"` + SmtpSSL bool `json:"smtp_ssl"` + FromName string `json:"from_name"` + FromAddress string `json:"from_address"` + DailyLimit int `json:"daily_limit"` + DailySent int `json:"daily_sent"` + Weight int `json:"weight"` + Status int8 `json:"status"` + LastCheckAt *time.Time `json:"last_check_at"` + LastCheckResult string `json:"last_check_result"` +} + +type CreateSenderReq struct { + Name string `json:"name"` + SmtpHost string `json:"smtp_host"` + SmtpPort int `json:"smtp_port"` + SmtpUser string `json:"smtp_user"` + SmtpPassword string `json:"smtp_password"` + SmtpSSL *bool `json:"smtp_ssl,omitempty"` + FromName string `json:"from_name,omitempty"` + FromAddress string `json:"from_address"` + DailyLimit *int `json:"daily_limit,omitempty"` + Weight *int `json:"weight,omitempty"` +} + +type UpdateSenderReq struct { + Name *string `json:"name,omitempty"` + SmtpHost *string `json:"smtp_host,omitempty"` + SmtpPort *int `json:"smtp_port,omitempty"` + SmtpUser *string `json:"smtp_user,omitempty"` + SmtpPassword *string `json:"smtp_password,omitempty"` + SmtpSSL *bool `json:"smtp_ssl,omitempty"` + FromName *string `json:"from_name,omitempty"` + FromAddress *string `json:"from_address,omitempty"` + DailyLimit *int `json:"daily_limit,omitempty"` + Weight *int `json:"weight,omitempty"` + Status *int8 `json:"status,omitempty"` +} + +type SenderListQuery struct { + PaginationQuery + Status *int8 `json:"status,omitempty"` + Keyword string `json:"keyword,omitempty"` +} + +// --- Audit --- + +type MailAudit struct { + GormModel + MailLogID uint `json:"mail_log_id"` + UserID int `json:"user_id"` + AccountID uint `json:"account_id"` + AuditType int8 `json:"audit_type"` + Action int8 `json:"action"` + RejectReason string `json:"reject_reason"` + HitRules string `json:"hit_rules"` + Auditor string `json:"auditor"` + AuditedAt time.Time `json:"audited_at"` +} + +type AuditPendingQuery struct { + PaginationQuery + UserID *int `json:"user_id,omitempty"` + AccountID *uint `json:"account_id,omitempty"` + Keyword string `json:"keyword,omitempty"` +} + +type AuditLogQuery struct { + PaginationQuery + AuditType *int8 `json:"audit_type,omitempty"` + Action *int8 `json:"action,omitempty"` + UserID *int `json:"user_id,omitempty"` + StartDate string `json:"start_date,omitempty"` + EndDate string `json:"end_date,omitempty"` +} + +type AuditRejectReq struct { + RejectReason string `json:"reject_reason"` +} + +type BatchAuditApproveReq struct { + MailLogIDs []uint `json:"mail_log_ids"` +} + +type BatchAuditRejectReq struct { + MailLogIDs []uint `json:"mail_log_ids"` + RejectReason string `json:"reject_reason"` +} + +type AuditStats struct { + PendingCount int64 `json:"pending_count"` + TodayDetails []AuditDetailCount `json:"today_details"` +} + +type AuditDetailCount struct { + AuditType int8 `json:"audit_type"` + Action int8 `json:"action"` + Count int64 `json:"count"` +} + +// --- Audit Rule --- + +type AuditRule struct { + GormModel + Name string `json:"name"` + RuleType string `json:"rule_type"` + Target string `json:"target"` + Condition string `json:"condition"` + Action int8 `json:"action"` + Priority int `json:"priority"` + Status int8 `json:"status"` + Remark string `json:"remark"` +} + +type CreateAuditRuleReq struct { + Name string `json:"name"` + RuleType string `json:"rule_type"` + Target string `json:"target"` + Condition string `json:"condition"` + Action int8 `json:"action"` + Priority *int `json:"priority,omitempty"` + Remark string `json:"remark,omitempty"` +} + +type UpdateAuditRuleReq struct { + Name *string `json:"name,omitempty"` + RuleType *string `json:"rule_type,omitempty"` + Target *string `json:"target,omitempty"` + Condition *string `json:"condition,omitempty"` + Action *int8 `json:"action,omitempty"` + Priority *int `json:"priority,omitempty"` + Status *int8 `json:"status,omitempty"` + Remark *string `json:"remark,omitempty"` +} + +type TestAuditRuleReq struct { + Subject string `json:"subject,omitempty"` + Body string `json:"body,omitempty"` + To []string `json:"to,omitempty"` + From string `json:"from,omitempty"` + AccountID uint `json:"account_id,omitempty"` +} + +type TestAuditRuleResp struct { + Action string `json:"action"` + HitRules []HitRuleEntry `json:"hit_rules"` +} + +type HitRuleEntry struct { + RuleID uint `json:"rule_id"` + RuleName string `json:"rule_name"` + RuleType string `json:"rule_type"` +} + +// --- Queue --- + +type QueueStatusData struct { + Queues map[string]int `json:"queues"` + DelayQueue int `json:"delay_queue"` +} + +type QueuePendingQuery struct { + PaginationQuery + ChannelID *uint `json:"channel_id,omitempty"` + UserID *int `json:"user_id,omitempty"` + AccountID *uint `json:"account_id,omitempty"` +} + +// --- Check --- + +type CheckLog struct { + ID uint `json:"id"` + SenderAccountID uint `json:"sender_account_id"` + VerificationCode string `json:"verification_code"` + SentAt string `json:"sent_at"` + Received bool `json:"received"` + ReceivedAt *string `json:"received_at"` + LatencyMs int `json:"latency_ms"` + ErrorMessage string `json:"error_message"` + CreatedAt string `json:"created_at"` +} + +type CheckLogQuery struct { + PaginationQuery + SenderAccountID *uint `json:"sender_account_id,omitempty"` + StartDate string `json:"start_date,omitempty"` + EndDate string `json:"end_date,omitempty"` +} + +type SenderHealth struct { + SenderAccountID uint `json:"sender_account_id"` + Name string `json:"name"` + FromAddress string `json:"from_address"` + Status int8 `json:"status"` + LastCheckResult string `json:"last_check_result"` + TotalChecks int64 `json:"total_checks"` + SuccessChecks int64 `json:"success_checks"` +} + +type TriggerCheckResp struct { + Result string `json:"result"` + Error string `json:"error,omitempty"` + CheckLog *CheckLog `json:"check_log,omitempty"` +}