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

11 KiB
Raw Blame History

mailx

多通道邮件发送库,统一接口、简单易用,支持 SMTP / 阿里云 / AWS SES / Mailgun 等多种通道,可自由扩展。

特性

  • 多通道接入smtpaliyunawsmailgun 开箱即用,实现 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-UnsubscribeX-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):阿里云通道回传发送状态记录