316 lines
11 KiB
Markdown
316 lines
11 KiB
Markdown
# 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("<h1>Hello world</h1>").
|
||
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)` | 发件人(可含显示名,如 `"张三" <a@b.com>`) |
|
||
| `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 中用 `<img src="cid:<cid>">` 引用 |
|
||
| `Header(key, value)` | 自定义邮件头(如 `List-Unsubscribe`、`X-Mailer`),标准头不可覆盖 |
|
||
| `Build()` | 生成 `*Message` |
|
||
|
||
### 内嵌图片示例
|
||
|
||
```go
|
||
msg := mailx.NewMessage().
|
||
To("user@example.com").
|
||
Subject("Welcome").
|
||
HTML(`<h1>Hi</h1><img src="cid:logo1">`).
|
||
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": "<b>hi</b>",
|
||
})
|
||
|
||
// 从 JSON 构建
|
||
msg, _ := mailx.FromBytes(jsonData)
|
||
```
|
||
|
||
### 地址工具
|
||
|
||
```go
|
||
mailx.IsValidAddress(`"张三" <a@b.com>`) // true,支持显示名
|
||
mailx.ExtractEmail(`"张三" <a@b.com>`) // "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)`:阿里云通道回传发送状态记录
|