feat: 添加邮件服务器客户端库的基础功能

- 新增完整的Go客户端库实现,支持邮件服务器API的各种操作
- 实现账户管理、签名管理、邮件发送、审计、配额、通道等功能模块
- 提供ServiceAuth和AppAuth两种认证模式的客户端
- 添加详细的README文档,包含安装指南和使用示例
- 配置.gitignore文件以忽略构建产物和开发工具配置
- 支持分页查询、错误处理和客户端选项配置
This commit is contained in:
shiran
2026-04-18 10:36:45 +08:00
commit 5b4774e9c1
16 changed files with 1251 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
vendor/
.idea/
.vscode/
+200
View File
@@ -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: "<h1>Hello World</h1>",
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 <token>` | 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),
)
```
+36
View File
@@ -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)
}
+54
View File
@@ -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)
}
+35
View File
@@ -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)
}
+27
View File
@@ -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
}
+23
View File
@@ -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)
}
+153
View File
@@ -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)
}
+3
View File
@@ -0,0 +1,3 @@
module gitea.s1f.ren/shiran/email-serverr-cli
go 1.26.2
+33
View File
@@ -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)
}
+67
View File
@@ -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
}
+29
View File
@@ -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
}
+32
View File
@@ -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
}
+27
View File
@@ -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
}
+38
View File
@@ -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
}
+484
View File
@@ -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"`
}