Files
mailx/readme.md
T
2026-08-15 01:38:05 +08:00

316 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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}` | SenderAWS 需预先验证发件地址) |
| 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)`:阿里云通道回传发送状态记录