# 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)`:阿里云通道回传发送状态记录