11 KiB
11 KiB
mailx
多通道邮件发送库,统一接口、简单易用,支持 SMTP / 阿里云 / AWS SES / Mailgun 等多种通道,可自由扩展。
特性
- 多通道接入:
smtp、aliyun、aws、mailgun开箱即用,实现mailx.Sender接口即可扩展新通道 - 链式消息构建:
mailx.NewMessage()流式拼接邮件内容 - 通道管理器:注册多个通道,发送时按名称路由或临时指定配置
- 发送时指定配置:无需提前初始化,可在调用
Send时传入任意通道配置 - 日志可插拔:定义轻量
Logger接口,可适配 loggerx / zap / logrus 等任意日志库
安装
go get code.yun.ink/pkg/mailx
快速开始
第一次使用?推荐先跑通最简示例
examples/quickstart(逐行注释), 再阅读下面的 API 说明,最后看examples/里的进阶示例。
# 克隆仓库后直接运行最简示例(需把其中 SMTP 配置换成你的)
cd examples/quickstart && go run main.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)
}
方式二:多通道管理器
注册多个通道,发送时按名称切换,适合业务侧多通道路由/降级。
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 实例(如主备切换)。
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)。
方式三:发送时临时指定配置
通道无需提前注册,发送时直接传入带配置的通道实例。
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 |
内嵌图片示例
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 快捷构建
// 从 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)
地址工具
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 精确判断类型:
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 接口即可:
type Sender interface {
Name() string // 通道唯一名称
Send(ctx context.Context, msg *mailx.Message) error
}
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
}
日志
通道默认不输出日志;可通过两种方式注入:
// 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 取消兜底
client := smtp.New(smtp.Config{
Host: "smtp.qq.com", Port: 465, User: "u", Password: "p",
Timeout: 10 * time.Second, // 单次发送 10s 超时
})
Manager 实例管理
// 注册(实例名 = 通道类型名)与命名注册(可同类型多份)
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() // 当前默认实例名
遍历示例:
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/quickstart |
入门 | 最简 SMTP 发送,逐行注释,适合第一次接触 |
examples/basic |
入门 | 单通道完整用法:HTML+附件+内嵌图片+显示名 |
examples/manager |
入门 | 多通道管理器:注册路由、主备切换、临时指定配置 |
examples/with_env |
进阶 | 用环境变量管理多通道凭据,避免密钥写死 |
examples/advanced |
进阶 | 主备切换、实例遍历、日志、超时、错误分类 |
examples/custom_sender |
进阶 | 实现自定义通道 + 接入自定义 Logger |
# 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):阿里云通道回传发送状态记录