# mailx
多通道邮件发送库,统一接口、简单易用,支持 SMTP / 阿里云 / AWS SES / Mailgun 等多种通道,可自由扩展。
## 特性
- **多通道接入**:`smtp`、`aliyun`、`aws`、`mailgun` 开箱即用,实现 `mailx.Sender` 接口即可扩展新通道
- **链式消息构建**:`mailx.NewMessage()` 流式拼接邮件内容
- **通道管理器**:注册多个通道,发送时按名称路由或临时指定配置
- **发送时指定配置**:无需提前初始化,可在调用 `Send` 时传入任意通道配置
- **日志可插拔**:定义轻量 `Logger` 接口,可适配 loggerx / zap / logrus 等任意日志库
## 安装
```bash
go get code.yun.ink/pkg/mailx
```
## 快速开始
> 第一次使用?推荐先跑通最简示例 `examples/quickstart`(逐行注释),
> 再阅读下面的 API 说明,最后看 `examples/` 里的进阶示例。
```bash
# 克隆仓库后直接运行最简示例(需把其中 SMTP 配置换成你的)
cd examples/quickstart && go run main.go
```
### 方式一:单个通道直接发送
```go
package main
import (
"context"
"fmt"
"code.yun.ink/pkg/mailx"
"code.yun.ink/pkg/mailx/smtp"
)
func main() {
ctx := context.Background()
err := smtp.New(smtp.Config{
Host: "smtp.qq.com",
Port: 587,
User: "sender@qq.com",
Password: "authorization-code",
From: "sender@qq.com", // 可选,默认取 User
}).Send(ctx, mailx.NewMessage().
To("receiver@example.com").
Subject("Hello").
HTML("
Hello world
").
Build())
fmt.Println(err)
}
```
### 方式二:多通道管理器
注册多个通道,发送时按名称切换,适合业务侧多通道路由/降级。
```go
mgr := mailx.NewManager()
mgr.Register(smtp.New(smtp.Config{Host: "smtp.qq.com", Port: 587, User: "u", Password: "p"}))
mgr.Register(aliyun.New(aliyun.Config{
AccessKeyID: "ak", AccessKeySecret: "sk", AccountName: "noreply@example.com",
}))
mgr.Register(aws.New(aws.Config{
AccessKeyID: "ak", AccessKeySecret: "sk", Region: "ap-northeast-1", Sender: "noreply@example.com",
}))
mgr.Register(mailgun.New(mailgun.Config{APIKey: "key", Domain: "mg.example.com", Sender: "noreply@example.com"}))
// 可选:指定默认通道,不设置则使用第一个注册的通道
mgr.SetDefault("aliyun")
msg := mailx.NewMessage().
From("noreply@example.com").
To("user@example.com").
Subject("Hello").
Body("hi").
Build()
mgr.Send(ctx, msg) // 使用默认通道
mgr.SendWith(ctx, "aws", msg) // 使用指定通道
```
**同一通道类型注册多份不同配置**:用 `RegisterNamed` 指定实例名,即可注册多个 smtp 实例(如主备切换)。
```go
mgr.RegisterNamed("smtp-main", smtp.New(smtp.Config{Host: "smtp.qq.com", Port: 465, User: "a@qq.com", Password: "main"}))
mgr.RegisterNamed("smtp-backup", smtp.New(smtp.Config{Host: "smtp.163.com", Port: 465, User: "a@163.com", Password: "backup"}))
mgr.SendWith(ctx, "smtp-main", msg) // 用主 SMTP
mgr.SendWith(ctx, "smtp-backup", msg) // 用备用 SMTP
```
> 说明:`Register(s)` 以通道类型名(`s.Name()`)作为实例名,同一类型仅能注册一个;需要多配置时用 `RegisterNamed(name, s)`。
### 方式三:发送时临时指定配置
通道无需提前注册,发送时直接传入带配置的通道实例。
```go
err := mgr.SendBy(ctx, mailgun.New(mailgun.Config{
APIKey: "another-key", Domain: "mg2.example.com", Sender: "noreply@example.com",
}), msg)
```
## 消息构建 API
| 方法 | 说明 |
| --- | --- |
| `From(addr)` | 发件人(可含显示名,如 `"张三" `) |
| `To(addr...)` / `Cc(...)` / `Bcc(...)` | 收件人 / 抄送 / 密送(可含显示名) |
| `Subject(s)` | 主题 |
| `Text(s)` | 纯文本正文(可选,推荐配合 HTML 使用) |
| `Body(s)` / `HTML(s)` | 正文(HTML) |
| `ReplyTo(addr)` | 回复地址 |
| `Attach(path)` / `AttachBytes(name, data)` | 普通附件(路径 / 内存字节) |
| `InlineImage(cid, path)` / `InlineImageBytes(cid, name, data)` | 内嵌图片,HTML 中用 `
` 引用 |
| `Header(key, value)` | 自定义邮件头(如 `List-Unsubscribe`、`X-Mailer`),标准头不可覆盖 |
| `Build()` | 生成 `*Message` |
### 内嵌图片示例
```go
msg := mailx.NewMessage().
To("user@example.com").
Subject("Welcome").
HTML(`Hi
`).
InlineImageBytes("logo1", "logo.png", pngBytes). // 或 InlineImage("logo1", "logo.png")
Build()
```
> 说明:SMTP 与 Mailgun 支持内嵌图片;AWS SES / 阿里云 DirectMail 的 SendEmail 不支持内嵌图片,若使用会返回明确错误。
### 从配置/DTO 快捷构建
```go
// 从 map 构建(便于接入配置/HTTP 请求体)
msg, _ := mailx.FromMap(map[string]any{
"from": "a@e.com", "to": "b@e.com, c@e.com",
"subject": "hi", "text": "plain", "html": "hi",
})
// 从 JSON 构建
msg, _ := mailx.FromBytes(jsonData)
```
### 地址工具
```go
mailx.IsValidAddress(`"张三" `) // true,支持显示名
mailx.ExtractEmail(`"张三" `) // "a@b.com",SMTP 命令需纯地址
mailx.AddressList("a@e.com, b@e.com; c@e.com") // 兼容逗号/分号分隔
```
## 错误处理
所有错误均可通过 `errors.Is` 精确判断类型:
```go
err := mgr.SendWith(ctx, "nope", msg)
switch {
case errors.Is(err, mailx.ErrSenderNotFound):
// 通道未注册,尝试其他通道
case errors.Is(err, mailx.ErrInvalidConfig):
// 配置缺失/非法
case errors.Is(err, mailx.ErrInvalidMessage):
// 消息校验失败
case errors.Is(err, mailx.ErrSendFailed):
// 发送过程中通道返回错误
}
```
## 扩展新通道
实现 `mailx.Sender` 接口即可:
```go
type Sender interface {
Name() string // 通道唯一名称
Send(ctx context.Context, msg *mailx.Message) error
}
```
```go
package mychannel
type MyChannel struct{ cfg Config }
func New(cfg Config) *MyChannel { return &MyChannel{cfg: cfg} }
func (c *MyChannel) Name() string { return "mychannel" }
func (c *MyChannel) Send(ctx context.Context, msg *mailx.Message) error {
// 实现发送逻辑
return nil
}
```
## 日志
通道默认不输出日志;可通过两种方式注入:
```go
// 1. 管理器全局注入
mgr.SetLogger(myLogger) // 实现 mailx.Logger 接口
// 2. 通过 context 注入
ctx = mailx.WithLogger(ctx, myLogger)
```
## 通道配置一览
| 通道 | 配置结构 | 必填 |
| --- | --- | --- |
| smtp | `smtp.Config{Host, Port, User, Password, From, ReplyTo, Encryption, Timeout}` | Host/Port/User/Password |
| aliyun | `aliyun.Config{AccessKeyID, AccessKeySecret, Endpoint, AccountName, ReplyAddress, Timeout}` | AccessKeyID/AccessKeySecret/AccountName |
| aws | `aws.Config{AccessKeyID, AccessKeySecret, Region, Sender, Timeout}` | Sender(AWS 需预先验证发件地址) |
| mailgun | `mailgun.Config{APIKey, Domain, Sender, Timeout}` | APIKey/Domain |
### SMTP 加密
`smtp.Config.Encryption` 支持以下模式,默认 `auto`(按端口自动选择):
| 模式 | 说明 |
| --- | --- |
| `auto` | 端口 465 走 SSL,其余端口走 STARTTLS |
| `ssl` | 隐式 TLS(端口 465) |
| `tls` | STARTTLS 升级加密(端口 587/25) |
| `none` | 明文(仅内网/测试,不推荐) |
## 超时控制
每个通道都支持 `Timeout` 配置,防止发送阻塞,避免 goroutine 泄漏:
- 默认超时 30s
- 若调用方通过 `context.WithTimeout/WithDeadline` 传入更早的 deadline,则以更早者为准
- 各通道具体实现:SMTP 为连接与投递的整体 deadline;阿里云/AWS/Mailgun 通过 API 超时 + ctx 取消兜底
```go
client := smtp.New(smtp.Config{
Host: "smtp.qq.com", Port: 465, User: "u", Password: "p",
Timeout: 10 * time.Second, // 单次发送 10s 超时
})
```
## Manager 实例管理
```go
// 注册(实例名 = 通道类型名)与命名注册(可同类型多份)
mgr.Register(s) // 实例名取 s.Name()
mgr.RegisterNamed("smtp-backup", s) // 自定义实例名
// 注销与默认实例
mgr.Unregister("smtp-backup") // 注销后自动回退默认实例
mgr.SetDefault("smtp-main") // 设置默认实例
// 遍历与查询
mgr.Registered("smtp-backup") // 实例是否已注册
mgr.Names() // 所有实例名
mgr.Senders() // []SenderInfo{Name 实例名, Type 通道类型}
mgr.Default() // 当前默认实例名
```
遍历示例:
```go
for _, info := range mgr.Senders() {
fmt.Println(info.Name, info.Type) // 如 "smtp-main" "smtp"
}
```
## 生产建议
- **复用通道实例**:各通道内部惰性初始化并复用 SDK 客户端/连接池,业务侧应复用 `New` 出来的单例,避免每次新建
- **消息校验**:`Message.Validate` 自动限制收件人总数(To+Cc+Bcc)≤50、附件数 ≤20、自定义头 ≤20、整封邮件大小 ≤25MB(`MaxMessageSize`),防止滥用配额
- **并发安全**:`Manager` 与所有通道实例均线程安全,可安全地在多个 goroutine 中共享
- **日志**:生产环境建议实现并注入 `Logger`。通过 `Manager` 发送时,框架会自动记录通道名、收件人、发送耗时与失败原因
- **自定义邮件头**:营销邮件常用 `List-Unsubscribe` 退订头、`X-Mailer` 标识等,用 `Header(key, value)` 设置(标准头不可覆盖)
## 示例代码
仓库提供由浅入深的可运行示例,位于 `examples/` 目录(详见 [`examples/README.md`](./examples/README.md)):
| 示例 | 难度 | 说明 |
| --- | --- | --- |
| `examples/quickstart` | 入门 | 最简 SMTP 发送,逐行注释,适合第一次接触 |
| `examples/basic` | 入门 | 单通道完整用法:HTML+附件+内嵌图片+显示名 |
| `examples/manager` | 入门 | 多通道管理器:注册路由、主备切换、临时指定配置 |
| `examples/with_env` | 进阶 | 用环境变量管理多通道凭据,避免密钥写死 |
| `examples/advanced` | 进阶 | 主备切换、实例遍历、日志、超时、错误分类 |
| `examples/custom_sender` | 进阶 | 实现自定义通道 + 接入自定义 Logger |
```bash
# 5 分钟上手(最简示例)
cd examples/quickstart && go run main.go
# 其余示例(在项目根目录执行)
go run ./examples/basic
go run ./examples/manager
go run ./examples/with_env # 需先配置 examples/with_env/.env
go run ./examples/advanced
go run ./examples/custom_sender
```
核心 API 的用法示例(`Example*` 测试)也会在 `go test` 中自动验证,可通过 `go doc` 查看。
## 其他能力
- `ParseHTMLResource(html)`:解析 HTML 中引用的 css/js/img 等静态资源地址
- `aliyun.SyncStatus(ctx)`:阿里云通道回传发送状态记录