# email-serverr-cli
Email Server API 的 Go 客户端库。提供管理端(ServiceAuth)与发件端(AppAuth)两种客户端,
覆盖邮件发送、账号、签名、配额、通道/发信、审核、队列、健康检查等全部后端能力。
- 模块路径: `gitea.s1f.ren/shiran/email-serverr-cli`
- 依赖: 仅使用标准库 `net/http`、`encoding/json`,无第三方依赖
- 风格: 对每一类资源一个文件,请求/响应类型在 `types.go`,底层请求在 `client.go`
## 安装
```bash
go get gitea.s1f.ren/shiran/email-serverr-cli
```
```go
import emailcli "gitea.s1f.ren/shiran/email-serverr-cli"
```
## 快速开始
### 管理客户端(ServiceAuth)
```go
client := emailcli.NewServiceClient(
"https://your-server.com",
"your-service-token",
)
accounts, err := client.ListAccounts(context.Background(), emailcli.AccountListQuery{
PaginationQuery: emailcli.PaginationQuery{Page: 1, PageSize: 20},
})
```
### 发件客户端(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 可选:不传时优先使用账号默认通道,其次使用允许通道列表首个可用项
})
```
### 客户端选项
```go
client := emailcli.NewServiceClient(baseURL, token,
emailcli.WithTimeout(60*time.Second),
emailcli.WithHTTPClient(customClient),
)
```
## 认证方式
| 模式 | 构造函数 | 请求头 | 适用范围 |
|------|----------|--------|----------|
| ServiceAuth | `NewServiceClient` | `Authorization: Bearer ` | 所有 `/api/v1` 管理接口 |
| AppAuth | `NewAppClient` | `X-App-Key` + `X-App-Secret` | 仅 `POST /api/v1/mail/send` |
---
## 枚举值说明
### Account.Status(账号状态)
| 值 | 含义 |
|----|------|
| 0 | 禁用 |
| 1 | 启用 |
### Account.AuditMode(审核模式)
| 值 | 含义 |
|----|------|
| 0 | 免审核(直接入队) |
| 1 | 自动(按规则判定) |
| 2 | 人工(待审核) |
### Signature.Status(签名状态)
| 值 | 含义 |
|----|------|
| 0 | 待审核 |
| 1 | 已通过 |
| 2 | 已驳回 |
### MailLog.Status(邮件状态)
| 值 | 含义 |
|----|------|
| 0 | 待审核 |
| 1 | 排队中 |
| 2 | 发送中 |
| 3 | 成功 |
| 4 | 失败 |
| 5 | 放弃 |
| 6 | 驳回 |
### MailQuota.QuotaType(配额类型)
| 值 | 含义 |
|----|------|
| 1 | 总量配额(`total` 为总发送上限) |
| 2 | 周期配额(到期自动重置) |
### MailQuota.CycleUnit(配额周期单位)
当 `quota_type=2` 时生效,取值:`day`、`week`、`month`、`year`
### MailQuota.Status
| 值 | 含义 |
|----|------|
| 0 | 禁用 |
| 1 | 启用 |
### Channel.Status / SenderAccount.Status
| 值 | 含义 |
|----|------|
| 0 | 禁用 |
| 1 | 启用 |
### Channel.Strategy(发信挑选策略)
| 值 | 含义 |
|------|------|
| `round_robin` | 轮询(默认) |
| `weight` | 按 `weight` 加权随机 |
| `least_used` | 今日发送数最少优先 |
### AuditRule.Action(规则动作)
| 值 | 含义 |
|----|------|
| 1 | 自动通过 |
| 2 | 自动驳回 |
| 3 | 转人工 |
### AuditRule.RuleType / Target(规则类型与目标)
- `RuleType` 取值: `keyword`、`regex`、`domain`
- `Target` 取值: `subject`、`body`、`to`、`from`
### MailAudit.AuditType(审核来源)
| 值 | 含义 |
|----|------|
| 1 | 自动 |
| 2 | 人工 |
### MailAudit.Action(审核动作)
| 值 | 含义 |
|----|------|
| 1 | 通过 |
| 2 | 驳回 |
### SendMailReq.ContentType
取值:`text/plain`(默认)、`text/html`
---
## API 参考
所有管理接口挂载在 `/api/v1` 下。以下按功能模块分组,并给出方法签名、HTTP 路径与关键参数说明。
### 一、发送邮件(AppAuth)
#### `SendMail(ctx, req SendMailReq) -> *SendMailResp`
`POST /api/v1/mail/send`
`SendMailReq` 字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `To` | `[]string` | 是 | 收件人列表,至少 1 个 |
| `Cc` | `[]string` | 否 | 抄送 |
| `Bcc` | `[]string` | 否 | 密送 |
| `Subject` | `string` | 是 | 主题 |
| `Body` | `string` | 是 | 正文 |
| `ContentType` | `string` | 否 | 默认 `text/html` |
| `Channel` | `string` | 否 | 通道 `code`。为空时按 账号默认通道 → 账号允许通道首个可用 顺序自动解析 |
| `SignatureID` | `*uint` | 否 | 指定签名 ID(用户必须拥有且已审核) |
| `SignatureTitle` | `string` | 否 | 按 title + user_id 选择签名;未传则使用默认签名 |
| `Attachments` | `[]AttachmentItem` | 否 | 附件(`filename` + base64 `content`) |
`SendMailResp` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `MailLogID` | `uint` | 创建的邮件日志 ID |
| `Status` | `string` | `queued` / `pending_audit` / `rejected` |
---
### 二、账号管理(ServiceAuth)
#### `CreateAccount(ctx, req CreateAccountReq) -> *CreateAccountResp`
`POST /api/v1/accounts`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `UserID` | `int` | 是 | 关联用户 ID |
| `Name` | `string` | 是 | 账号名称(≤100) |
| `AuditMode` | `*int8` | 否 | 审核模式枚举,默认 `0` |
| `RateLimit` | `*int` | 否 | 频率限制(封/分钟),`0` 表示不限 |
| `DefaultChannelID` | `*uint` | 否 | 默认发件通道 ID |
| `AllowedChannels` | `string` | 否 | 允许发件通道 ID 列表的 JSON 字符串,例如 `"[1,2]"`。为空时视作不限制 |
| `Remark` | `string` | 否 | 备注 |
返回 `CreateAccountResp`(首次创建返回明文 `AppSecret`):
```json
{ "id": 1, "app_key": "...", "app_secret": "只在此次展示", "name": "..." }
```
#### `ListAccounts(ctx, q AccountListQuery) -> *PaginationResult[Account]`
`GET /api/v1/accounts?page=&page_size=&user_id=&status=&keyword=`
| 参数 | 类型 | 说明 |
|------|------|------|
| `Page`/`PageSize` | `int` | 分页,`PageSize` 默认 20 |
| `UserID` | `*int` | 按用户筛选 |
| `Status` | `*int8` | 账号状态(0/1) |
| `Keyword` | `string` | 模糊匹配 `name` 或 `remark` |
#### `GetAccount(ctx, id uint) -> *Account`
`GET /api/v1/accounts/{id}`
#### `UpdateAccount(ctx, id uint, req UpdateAccountReq) -> *Account`
`PUT /api/v1/accounts/{id}`
所有字段均为可选指针,只更新已传字段。
| 字段 | 类型 | 说明 |
|------|------|------|
| `Name` | `*string` | |
| `Status` | `*int8` | 启用/禁用 |
| `AuditMode` | `*int8` | |
| `RateLimit` | `*int` | |
| `DefaultChannelID` | `*uint` | 默认发件通道 |
| `AllowedChannels` | `*string` | 允许发件通道 ID 列表 JSON |
| `DefaultSignatureID` | `*uint` | 默认签名 ID |
| `Remark` | `*string` | |
#### `DeleteAccount(ctx, id uint) error`
`DELETE /api/v1/accounts/{id}`
#### `ResetAccountSecret(ctx, id uint) -> *ResetSecretResp`
`POST /api/v1/accounts/{id}/reset-secret`
返回新生成的明文 `AppSecret`。
---
### 三、签名管理(ServiceAuth)
#### `CreateSignature(ctx, req CreateSignatureReq) -> *Signature`
`POST /api/v1/signatures`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `UserID` | `int` | 是 | |
| `AccountID` | `*uint` | 否 | 绑定账号,为空表示用户全局签名 |
| `Title` | `string` | 是 | 中文抬头 |
| `EnglishName` | `string` | 是 | 英文标识(用于组装 From 地址) |
| `Content` | `string` | 否 | HTML 签名内容 |
| `Applicant` | `string` | 否 | 申请人 |
| `ApplicantInfo` | `string` | 否 | 申请说明 |
新建默认为 `Status=0`(待审核)。
#### `ListSignatures(ctx, q SignatureListQuery) -> *PaginationResult[Signature]`
`GET /api/v1/signatures?page=&page_size=&user_id=&account_id=&status=&keyword=`
#### `GetSignature(ctx, id uint)` / `UpdateSignature` / `DeleteSignature`
`GET|PUT|DELETE /api/v1/signatures/{id}`
#### `AuditSignature(ctx, id uint, req AuditSignatureReq)`
`POST /api/v1/signatures/{id}/audit`
| 字段 | 类型 | 说明 |
|------|------|------|
| `Action` | `int8` | `1`=通过,`2`=驳回 |
| `RejectReason` | `string` | 驳回时建议填写 |
| `Auditor` | `string` | 审核人标识 |
---
### 四、邮件日志(ServiceAuth)
#### `ListMailLogs(ctx, q MailLogListQuery) -> *PaginationResult[MailLog]`
`GET /api/v1/mail-logs`
| 参数 | 类型 | 说明 |
|------|------|------|
| `UserID` | `*int` | |
| `AccountID` | `*uint` | |
| `Status` | `*int8` | 见 `MailLog.Status` 枚举 |
| `StartDate`/`EndDate` | `string` | `YYYY-MM-DD` |
| `To` | `string` | 收件人精确匹配 |
| `Keyword` | `string` | 模糊匹配主题或收件人 |
#### `GetMailLog(ctx, id uint) -> *MailLogDetail`
`GET /api/v1/mail-logs/{id}` 返回 `MailLog` + 正文 `Body`。
#### `GetMailStats(ctx) -> []MailStatItem`
`GET /api/v1/mail-logs/stats` 按 `Status` 分组的邮件数统计。
---
### 五、配额管理(ServiceAuth)
#### `CreateQuota(ctx, req CreateQuotaReq) -> *MailQuota`
`POST /api/v1/quotas`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `UserID` | `int` | 是 | |
| `AccountID` | `uint` | 是 | |
| `QuotaType` | `int8` | 是 | `1`=总量,`2`=周期 |
| `Total` | `int` | 是 | 额度上限 |
| `ExpireAt` | `string` | 否 | `YYYY-MM-DD HH:mm:ss` |
| `CycleUnit` | `string` | 否 | `day` / `week` / `month` / `year` |
| `CycleResetAt` | `string` | 否 | 周期起始时间 |
#### `ListQuotas(ctx, q QuotaListQuery)` · `GetQuotaSummary(ctx, accountID)` · `UpdateQuota(ctx, id, req)` · `DeleteQuota(ctx, id)`
`UpdateQuotaReq` 支持局部更新 `Total`、`Status`、`ExpireAt`、`CycleUnit`、`CycleResetAt`。
`QuotaListQuery` 过滤参数:`UserID`、`AccountID`、`QuotaType`、`Status`。
---
### 六、通道与发信账号(ServiceAuth)
#### 通道 `Channel`
- `CreateChannel(ctx, req CreateChannelReq) -> *Channel` — `POST /api/v1/channels`
- `ListChannels(ctx, q ChannelListQuery) -> *PaginationResult[Channel]` — `GET /api/v1/channels`
- `UpdateChannel(ctx, id, req UpdateChannelReq) -> *Channel` — `PUT /api/v1/channels/{id}`
- `DeleteChannel(ctx, id uint) error` — `DELETE /api/v1/channels/{id}`
`CreateChannelReq` 字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `Code` | `string` | 是 | 唯一标识(发送邮件用) |
| `Name` | `string` | 是 | |
| `Description` | `string` | 否 | |
| `Strategy` | `string` | 否 | 见 `Channel.Strategy` 枚举 |
#### 发信账号 `SenderAccount`
- `CreateSender(ctx, channelID uint, req CreateSenderReq) -> *SenderAccount` — `POST /api/v1/channels/{channelID}/senders`
- `ListSendersByChannel(ctx, channelID, q SenderListQuery) -> *PaginationResult[SenderAccount]` — `GET /api/v1/channels/{channelID}/senders`
- `UpdateSender(ctx, id uint, req UpdateSenderReq) -> *SenderAccount` — `PUT /api/v1/senders/{id}`
- `DeleteSender(ctx, id uint) error` — `DELETE /api/v1/senders/{id}`
`CreateSenderReq` 关键字段:`Name`、`SmtpHost`、`SmtpPort`、`SmtpUser`、`SmtpPassword`、`SmtpSSL`、`FromName`、`FromAddress`、`DailyLimit`、`Weight`。
---
### 七、审核(ServiceAuth)
- `ListAuditPending(ctx, q AuditPendingQuery) -> *PaginationResult[MailLog]` — `GET /api/v1/audits/pending`
- `GetAuditPendingDetail(ctx, id uint) -> *MailLogDetail` — `GET /api/v1/audits/pending/{id}`
- `ApproveAudit(ctx, id uint) error` — `POST /api/v1/audits/{id}/approve`
- `RejectAudit(ctx, id uint, req AuditRejectReq) error` — `POST /api/v1/audits/{id}/reject`
- `BatchApproveAudit(ctx, req BatchAuditApproveReq) error` — `POST /api/v1/audits/batch/approve`
- `BatchRejectAudit(ctx, req BatchAuditRejectReq) error` — `POST /api/v1/audits/batch/reject`
- `ListAuditLogs(ctx, q AuditLogQuery) -> *PaginationResult[MailAudit]` — `GET /api/v1/audits/logs`
- `GetAuditStats(ctx) -> *AuditStats` — `GET /api/v1/audits/stats`
`AuditPendingQuery` 过滤:`AccountID`、`UserID`、`Keyword`。
`AuditLogQuery` 过滤:`AccountID`、`UserID`、`Action`、`AuditType`、`StartDate`、`EndDate`。
---
### 八、审核规则(ServiceAuth)
- `CreateAuditRule(ctx, req CreateAuditRuleReq) -> *AuditRule` — `POST /api/v1/audit-rules`
- `ListAuditRules(ctx) -> []AuditRule` — `GET /api/v1/audit-rules`
- `GetAuditRule(ctx, id uint) -> *AuditRule` — `GET /api/v1/audit-rules/{id}`
- `UpdateAuditRule(ctx, id uint, req UpdateAuditRuleReq) -> *AuditRule` — `PUT /api/v1/audit-rules/{id}`
- `DeleteAuditRule(ctx, id uint) error` — `DELETE /api/v1/audit-rules/{id}`
- `UpdateAuditRuleStatus(ctx, id uint, status int8) -> *AuditRule` — `PUT /api/v1/audit-rules/{id}/status`
- `TestAuditRule(ctx, req TestAuditRuleReq) -> *TestAuditRuleResp` — `POST /api/v1/audit-rules/test`
`CreateAuditRuleReq` 字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `Name` | `string` | 是 | |
| `RuleType` | `string` | 是 | 见 `AuditRule.RuleType` 枚举 |
| `Target` | `string` | 是 | 见 `AuditRule.Target` 枚举 |
| `Condition` | `string` | 是 | 关键词或正则表达式 |
| `Action` | `int8` | 是 | 见 `AuditRule.Action` 枚举 |
| `Priority` | `int` | 否 | 数字越大优先级越高 |
| `Remark` | `string` | 否 | |
---
### 九、队列(ServiceAuth)
- `GetQueueStatus(ctx) -> *QueueStatusData` — `GET /api/v1/queue/status`
- `ListQueuePending(ctx, q QueuePendingQuery) -> *PaginationResult[MailLog]` — `GET /api/v1/queue/pending`
- `CancelQueueItem(ctx, mailLogID uint) error` — `POST /api/v1/queue/{mailLogID}/cancel`
- `RetryQueueItem(ctx, mailLogID uint) error` — `POST /api/v1/queue/{mailLogID}/retry`
`QueueStatusData` 返回各通道队列长度与延迟队列长度:
```go
type QueueStatusData struct {
Queues map[string]int64 `json:"queues"` // key = channel code
DelayQueue int64 `json:"delay_queue"`
}
```
---
### 十、健康检查(ServiceAuth)
- `ListCheckLogs(ctx, q CheckLogQuery) -> *PaginationResult[CheckLog]` — `GET /api/v1/check-logs`
- `GetCheckSummary(ctx) -> []SenderHealth` — `GET /api/v1/check-logs/summary`
- `TriggerCheck(ctx, senderAccountID uint) -> *TriggerCheckResp` — `POST /api/v1/check-logs/trigger/{senderAccountID}`
`CheckLogQuery` 过滤:`SenderAccountID`、`Received`、`StartDate`、`EndDate`。
---
## 统一返回结构
后端所有接口统一使用:
```json
{ "code": 200, "message": "ok", "data": { /* 业务数据 */ } }
```
SDK 内部会解包 `data` 并在非 200 时返回 `*APIError`:
```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)
}
}
```
分页接口统一包装:
```go
type PaginationResult[T any] struct {
List []T `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
```
## License
MIT