commit 5b4774e9c12d7f20e169d09c31e92c7b58d2773c Author: shiran Date: Sat Apr 18 10:36:45 2026 +0800 feat: 添加邮件服务器客户端库的基础功能 - 新增完整的Go客户端库实现,支持邮件服务器API的各种操作 - 实现账户管理、签名管理、邮件发送、审计、配额、通道等功能模块 - 提供ServiceAuth和AppAuth两种认证模式的客户端 - 添加详细的README文档,包含安装指南和使用示例 - 配置.gitignore文件以忽略构建产物和开发工具配置 - 支持分页查询、错误处理和客户端选项配置 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"` +}