This commit is contained in:
yun
2026-08-15 01:38:05 +08:00
parent 139330aee2
commit eb8f660ab5
57 changed files with 5401 additions and 1438 deletions
+50
View File
@@ -0,0 +1,50 @@
# mailx 使用示例
本目录包含多个由浅入深的示例,方便快速接入。
## 示例总览
| 示例 | 难度 | 说明 |
| --- | --- | --- |
| [`quickstart`](./quickstart) | 入门 | 最简 SMTP 发送,逐行注释,适合第一次接触 |
| [`basic`](./basic) | 入门 | 单通道直接发送,含附件/抄送/回复地址 |
| [`manager`](./manager) | 入门 | 多通道管理器:注册、路由、默认通道、临时指定配置 |
| [`with_env`](./with_env) | 进阶 | 用环境变量管理多通道凭据,避免密钥写死在代码里 |
| [`advanced`](./advanced) | 进阶 | 主备切换、实例遍历、日志注入、超时、内嵌图片、错误分类 |
| [`custom_sender`](./custom_sender) | 进阶 | 实现自定义通道 + 自定义 Logger |
## 快速开始(5 分钟)
```bash
# 1. 进入最简示例
cd examples/quickstart
# 2. 打开 main.go,把里面的 4 个 SMTP 配置改成你自己的
# (QQ 邮箱需先开启 SMTP 服务并获取授权码)
# 3. 运行
go run main.go
```
看到 `send success` 即表示发送成功。
## 运行方式
所有示例都在 mailx 主模块内,直接在示例目录执行 `go run .` 即可:
```bash
# 示例:运行多通道管理器示例
cd examples/manager
go run .
# 示例:运行环境变量示例(先配置好 .env)
cd examples/with_env
go run .
```
## 注意事项
1. **凭据**:所有示例中的密钥都是占位符,运行前必须替换为真实值
2. **SMTP 授权码**:QQ/163 等邮箱不是用邮箱登录密码,而是需要在邮箱后台开启 SMTP 服务后生成的授权码
3. **AWS/阿里云**:需要先完成服务开通、域名验证(发件地址需为已验证身份)
4. **`.env`**`with_env` 示例依赖 `github.com/joho/godotenv`,首次运行前执行 `go get github.com/joho/godotenv``with_env/.env` 含敏感信息,勿提交 git
+111
View File
@@ -0,0 +1,111 @@
// advanced 展示生产环境常用的一些进阶能力。
//
// 覆盖以下内容:
// 1. 同一通道类型(smtp)注册多份不同配置(主备切换)
// 2. 遍历当前注册的通道实例信息
// 3. 注入自定义 Logger(日志可插拔)
// 4. 设置通道超时,防止发送阻塞
// 5. 内嵌图片 + HTML 邮件
// 6. 用 FromMap 从 map 快捷构建消息
// 7. 按错误类型精确处理(errors.Is)
//
// 怎么运行:设置好下方配置后执行:go run main.go
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"code.yun.ink/pkg/mailx"
"code.yun.ink/pkg/mailx/smtp"
)
// appLogger 一个简单的 Logger 实现(也可接入 zap/logrus)。
// 生产环境建议实现 Debugf/Infof/Warnf/Errorf 四个方法,用于观测发送过程。
type appLogger struct{}
func (appLogger) Debugf(_ context.Context, format string, args ...any) {
log.Printf("[debug] "+format, args...)
}
func (appLogger) Infof(_ context.Context, format string, args ...any) {
log.Printf("[info] "+format, args...)
}
func (appLogger) Warnf(_ context.Context, format string, args ...any) {
log.Printf("[warn] "+format, args...)
}
func (appLogger) Errorf(_ context.Context, format string, args ...any) {
log.Printf("[error] "+format, args...)
}
func main() {
ctx := context.Background()
mgr := mailx.NewManager()
mgr.SetLogger(appLogger{}) // 注入全局 Logger
// ===== 1. 同一通道类型注册多份配置(主备切换) =====
_ = mgr.RegisterNamed("smtp-main", smtp.New(smtp.Config{
Host: "smtp.qq.com", Port: 465, User: "a@qq.com", Password: "main-code",
Timeout: 10 * time.Second, // ===== 4. 通道超时,防止发送阻塞 =====
}))
_ = mgr.RegisterNamed("smtp-backup", smtp.New(smtp.Config{
Host: "smtp.163.com", Port: 465, User: "a@163.com", Password: "backup-code",
Timeout: 10 * time.Second,
}))
_ = mgr.SetDefault("smtp-main") // 默认用主通道
// ===== 2. 遍历当前注册的通道实例信息 =====
for _, info := range mgr.Senders() {
fmt.Printf("sender instance: name=%s type=%s\n", info.Name, info.Type)
}
// ===== 5. 内嵌图片 + HTML 邮件 =====
msg := mailx.NewMessage().
From(`"通知中心" <a@qq.com>`). // 发件人带显示名
To("user@example.com").
Subject("生产环境告警").
Text("这是一封告警邮件,请及时处理。").
HTML(`<h2>磁盘使用率超过 90%</h2><img src="cid:chart1">`).
InlineImageBytes("chart1", "chart.png", []byte{0x89, 0x50, 0x4e, 0x47}). // 内嵌图片,cid:chart1 对应
Header("X-Mailer", "mailx"). // 自定义邮件头
Header("List-Unsubscribe", "<https://example.com/unsub>"). // 退订头(营销邮件合规)
Build()
// ===== 3. 用默认通道发送(自动注入 Logger) =====
if err := mgr.Send(ctx, msg); err != nil {
fmt.Println("main send failed:", err)
}
// ===== 7. 错误类型精确处理:主通道失败自动切换备通道 =====
err := mgr.SendWith(ctx, "smtp-main", msg)
if err != nil {
// 发送失败(如网络/认证)属于 ErrSendFailed,而非配置/消息问题
if errors.Is(err, mailx.ErrSendFailed) {
fmt.Println("main failed, switching to backup:", err)
if err2 := mgr.SendWith(ctx, "smtp-backup", msg); err2 != nil {
fmt.Println("backup send failed:", err2)
return
}
} else if errors.Is(err, mailx.ErrInvalidMessage) {
fmt.Println("message invalid:", err) // 消息本身有问题,重试无意义
return
} else {
fmt.Println("send error:", err)
}
}
// ===== 6. 用 FromMap 从 map 快捷构建消息(适合接入 HTTP 请求体) =====
m, err := mailx.FromMap(map[string]any{
"from": "a@qq.com",
"to": "user@example.com, admin@example.com",
"subject": "welcome",
"text": "hi",
"html": "<b>welcome</b>",
})
if err == nil {
_ = mgr.Send(ctx, m)
}
}
+67
View File
@@ -0,0 +1,67 @@
// basic 展示单个通道直接发送的完整用法。
//
// 功能:用 SMTP 发送一封含 HTML 正文、抄送、附件、内嵌图片的邮件。
//
// 怎么运行:
// 1. 把下方 smtp.Config 的 4 个配置改成你自己的
// 2. 在项目根目录执行:go run ./examples/basic
package main
import (
"context"
"fmt"
"code.yun.ink/pkg/mailx"
"code.yun.ink/pkg/mailx/smtp"
)
func main() {
ctx := context.Background()
// ===== 第 1 步:创建 SMTP 通道 =====
client := smtp.New(smtp.Config{
Host: "smtp.qq.com", // SMTP 服务器地址
Port: 587, // 端口:465=SSL587/25=STARTTLS
User: "sender@qq.com", // 账号
Password: "your-auth-code", // 授权码(非邮箱密码)
From: "sender@qq.com", // 默认发件人(可选,Message.From 未设置时使用)
ReplyTo: "sender@qq.com", // 默认回复地址(可选)
})
// ===== 第 2 步:构建消息 =====
// 地址都可带显示名,如 "张三" <a@qq.com>;收件人/抄送/密送可传多个
msg := mailx.NewMessage().
From(`"客服中心" <sender@qq.com>`). // 发件人(带显示名)
To("receiver@example.com"). // 收件人
Cc("manager@example.com"). // 抄送(可选)
Bcc("leader@example.com"). // 密送(可选)
Subject("hello from mailx").
Text("如果邮件客户端不支持 HTML,会显示这段纯文本。"). // 纯文本正文(推荐)
HTML(`<h1>Hello</h1><p>This email is sent by mailx.</p><img src="cid:logo1">`).
ReplyTo("support@example.com"). // 回复地址(可选,覆盖 Config.ReplyTo
Attach("report.txt"). // 按路径添加附件(路径需真实存在)
AttachBytes("summary.txt", []byte("summary content")). // 内存附件
InlineImageBytes("logo1", "logo.png", mustPNG()). // 内嵌图片(HTML 中 src="cid:logo1"
Build()
// ===== 第 3 步:发送 =====
if err := client.Send(ctx, msg); err != nil {
fmt.Println("send failed:", err)
return
}
fmt.Println("send success")
}
// mustPNG 返回一个极小的 1x1 PNG(1 像素透明图)用于演示内嵌图片。
// 实际使用中请替换为真实的图片文件路径或字节内容。
func mustPNG() []byte {
// 一个合法的 1x1 透明 PNG
return []byte{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00,
0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
}
}
+80
View File
@@ -0,0 +1,80 @@
// custom_sender 展示如何接入一个全新的发送通道(自定义 Sender)。
//
// 适用场景:公司内部自研邮件网关、短信通道、钉钉/飞书通知等,
// 只需实现 mailx.Sender 接口(Name + Send),即可复用 Manager 的
// 注册路由、默认通道、日志注入、消息校验等全部能力。
//
// 怎么运行:在项目根目录执行:go run ./examples/custom_sender
package main
import (
"context"
"fmt"
"log"
"code.yun.ink/pkg/mailx"
)
// printSender 自定义通道:仅把消息打印到控制台,不真正发送。
// 只需实现 Sender 接口的两个方法:Name() 和 Send()。
type printSender struct {
name string // 通道类型名
}
func newPrintSender(name string) *printSender { return &printSender{name: name} }
// Name 返回通道类型名(如 "print"),用于 Manager 注册与路由
func (p *printSender) Name() string { return p.name }
// Send 实现发送逻辑。ctx 里可通过 mailx.LoggerFromContext 拿到注入的日志器。
func (p *printSender) Send(ctx context.Context, msg *mailx.Message) error {
logger := mailx.LoggerFromContext(ctx) // 读取 Manager 注入的 Logger(可选)
logger.Infof(ctx, "printSender Send called")
fmt.Printf("[%s] to=%v cc=%v subject=%q text=%q html=%q\n",
p.name, msg.To, msg.Cc, msg.Subject, msg.TextBody, msg.Body)
return nil
}
// appLogger 实现 mailx.Logger 接口(4 个方法)。
// 生产中可改用 zap / logrus / slog 等,只需实现同样 4 个方法即可接入。
type appLogger struct{}
func (appLogger) Debugf(_ context.Context, format string, args ...any) {
log.Printf("[debug] "+format, args...)
}
func (appLogger) Infof(_ context.Context, format string, args ...any) {
log.Printf("[info] "+format, args...)
}
func (appLogger) Warnf(_ context.Context, format string, args ...any) {
log.Printf("[warn] "+format, args...)
}
func (appLogger) Errorf(_ context.Context, format string, args ...any) {
log.Printf("[error] "+format, args...)
}
func main() {
ctx := context.Background()
mgr := mailx.NewManager()
mgr.SetLogger(appLogger{}) // 注入 Logger,自定义通道内可读取
// 注册自定义通道
_ = mgr.Register(newPrintSender("print"))
// 同一自定义类型也可注册多份实例(不同配置)
_ = mgr.RegisterNamed("print-debug", newPrintSender("print"))
msg := mailx.NewMessage().
To("user@example.com").
Subject("hello").
Text("from custom sender").
HTML("<b>from custom sender</b>").
Build()
// 通过 Manager 统一发送
if err := mgr.Send(ctx, msg); err != nil {
fmt.Println("send failed:", err)
return
}
fmt.Println("send ok")
}
+92
View File
@@ -0,0 +1,92 @@
// manager 展示多通道管理器的完整用法。
//
// 功能:
// 1. 注册多个不同类型的通道(smtp/aliyun/aws),统一路由
// 2. 同一类型注册多份配置(主备 SMTP 切换)
// 3. 发送时按实例名指定通道,或发送时临时指定配置
// 4. 遍历当前注册的通道实例信息
//
// 怎么运行:
// 1. 替换下方各通道的凭据为真实值
// 2. 在项目根目录执行:go run ./examples/manager
package main
import (
"context"
"fmt"
"code.yun.ink/pkg/mailx"
"code.yun.ink/pkg/mailx/aliyun"
"code.yun.ink/pkg/mailx/aws"
"code.yun.ink/pkg/mailx/smtp"
)
func main() {
ctx := context.Background()
mgr := mailx.NewManager()
// ===== 注册多个不同类型的通道 =====
// 用 Register 注册时,实例名 = 通道类型名(smtp / aliyun / aws / mailgun
_ = mgr.Register(smtp.New(smtp.Config{
Host: "smtp.qq.com", Port: 587, User: "a@qq.com", Password: "code",
}))
_ = 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",
}))
// ===== 同一类型注册多份配置(主备切换) =====
// 用 RegisterNamed 指定实例名,可注册多个 smtp 实例
_ = mgr.RegisterNamed("smtp-main", smtp.New(smtp.Config{
Host: "smtp.qq.com", Port: 465, User: "a@qq.com", Password: "main-code",
}))
_ = mgr.RegisterNamed("smtp-backup", smtp.New(smtp.Config{
Host: "smtp.163.com", Port: 465, User: "a@163.com", Password: "backup-code",
}))
// ===== 设置默认通道(不设置则用第一个注册的) =====
_ = mgr.SetDefault("aliyun")
// ===== 遍历当前注册的通道实例信息 =====
fmt.Println("registered instances:")
for _, info := range mgr.Senders() {
fmt.Printf(" - %s (type: %s)\n", info.Name, info.Type)
}
fmt.Println("default:", mgr.Default())
msg := mailx.NewMessage().
From("noreply@example.com").
To("user@example.com").
Subject("Notice").
Text("hello").
HTML("<h1>Hello</h1>").
Build()
// ===== 用默认通道发送 =====
if err := mgr.Send(ctx, msg); err != nil {
fmt.Println("default send:", err)
}
// ===== 按实例名指定通道发送 =====
if err := mgr.SendWith(ctx, "aws", msg); err != nil {
fmt.Println("aws send:", err)
}
if err := mgr.SendWith(ctx, "smtp-backup", msg); err != nil { // 主 SMTP 失败时可切备用
fmt.Println("smtp-backup send:", err)
}
// ===== 发送时临时指定配置,无需提前注册 =====
if err := mgr.SendBy(ctx, smtp.New(smtp.Config{
Host: "smtp.163.com", Port: 465, User: "b@163.com", Password: "code",
}), msg); err != nil {
fmt.Println("temp smtp send:", err)
}
// ===== 注销一个实例(可选) =====
mgr.Unregister("smtp-main")
fmt.Println("after unregister, instances:", mgr.Names())
}
+51
View File
@@ -0,0 +1,51 @@
// quickstart 是最简单的 mailx 入门示例。
//
// 功能:用 QQ 邮箱 SMTP 发送一封纯文本邮件。
//
// 怎么运行:
// 1. 在 QQ 邮箱「设置 -> 账户」中开启 SMTP,获取授权码(不是 QQ 密码)
// 2. 把下方 main.go 里的 4 个配置改成你自己的
// 3. 在本目录执行:go run main.go
//
// 参考:https://mail.qq.com 开启 SMTP 服务的授权码获取方法
package main
import (
"context"
"fmt"
"code.yun.ink/pkg/mailx" // mailx 核心包:消息构建 + Sender 接口
"code.yun.ink/pkg/mailx/smtp" // smtp 通道:基于 SMTP 协议发送
)
func main() {
// context 用于控制发送的超时与取消,先创建空背景 context
ctx := context.Background()
// ===== 第 1 步:创建发送通道 =====
// smtp.New 接收一个 smtp.Config 配置,返回一个发送通道实例
client := smtp.New(smtp.Config{
Host: "smtp.qq.com", // SMTP 服务器地址
Port: 587, // 端口:465 走 SSL587 走 STARTTLS
User: "sender@qq.com", // 你的 QQ 邮箱地址
Password: "your-auth-code", // 授权码(QQ 邮箱后台开启 SMTP 后获取)
// From: "sender@qq.com", // 可选:默认发件人,不填则用上面的 User
})
// ===== 第 2 步:构建邮件消息 =====
// 使用链式调用(builder 模式),一行一个字段,最后 Build() 生成消息
msg := mailx.NewMessage().
From("sender@qq.com"). // 发件人(可带显示名,如 "张三" <a@qq.com>
To("receiver@example.com"). // 收件人,可传多个:To("a@x.com", "b@x.com")
Subject("hello from mailx"). // 邮件主题
Text("This is a plain text email."). // 纯文本正文
Build() // 生成 *Message
// ===== 第 3 步:发送 =====
// 传入 ctx 和消息,返回 error(nil 表示成功)
if err := client.Send(ctx, msg); err != nil {
fmt.Println("send failed:", err)
return
}
fmt.Println("send success")
}
+31
View File
@@ -0,0 +1,31 @@
# 复制本文件为 .env 并填入你自己的真实凭据
# 注意:.env 包含敏感信息,不要提交到 git(已在 .gitignore 中忽略)
# ===== 收件人(必填) =====
TO_ADDR=receiver@example.com
# 发件人兜底地址(选填)
FROM_ADDR=sender@example.com
# ===== SMTP 通道(选填,配置后即启用) =====
SMTP_HOST=smtp.qq.com
SMTP_PORT=587
SMTP_USER=sender@qq.com
SMTP_PASSWORD=your-auth-code
SMTP_FROM=sender@qq.com
# ===== 阿里云邮件推送(选填) =====
ALIYUN_ACCESS_KEY_ID=your-access-key-id
ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
ALIYUN_ACCOUNT_NAME=noreply@example.com
# ALIYUN_ENDPOINT=dm.aliyuncs.com
# ===== AWS SES(选填) =====
AWS_ACCESS_KEY_ID=your-aws-ak
AWS_ACCESS_KEY_SECRET=your-aws-sk
AWS_REGION=ap-northeast-1
AWS_SENDER=noreply@example.com
# ===== Mailgun(选填) =====
MAILGUN_API_KEY=your-api-key
MAILGUN_DOMAIN=mg.example.com
MAILGUN_SENDER=noreply@example.com
+127
View File
@@ -0,0 +1,127 @@
// with_env 展示如何通过环境变量管理通道凭据(避免把密钥写死在代码里)。
//
// 适用场景:已有多个通道的凭据(SMTP / 阿里云 / AWS SES / Mailgun),
// 希望在程序启动时从环境变量读取配置,注册到 Manager 统一管理。
//
// 怎么运行:
// 1. 复制 .env.example 为 .env(或直接设置下方环境变量)
// 2. 安装 dotenv 依赖:go get github.com/joho/godotenv
// 3. 设置好至少一个通道的凭据后执行:go run main.go
package main
import (
"context"
"fmt"
"log"
"os"
"code.yun.ink/pkg/mailx"
"code.yun.ink/pkg/mailx/aliyun"
"code.yun.ink/pkg/mailx/aws"
"code.yun.ink/pkg/mailx/mailgun"
"code.yun.ink/pkg/mailx/smtp"
"github.com/joho/godotenv" // 读取 .env 文件(可选)
)
// buildSenders 从环境变量读取凭据,返回已配置好的通道列表。
// 只返回凭据齐全的通道,未配置的通道直接跳过,方便按需启用。
func buildSenders() []mailx.Sender {
var senders []mailx.Sender
// --- SMTP ---
if os.Getenv("SMTP_HOST") != "" {
senders = append(senders, smtp.New(smtp.Config{
Host: os.Getenv("SMTP_HOST"),
Port: atoi(os.Getenv("SMTP_PORT"), 465),
User: os.Getenv("SMTP_USER"),
Password: os.Getenv("SMTP_PASSWORD"),
From: os.Getenv("SMTP_FROM"),
}))
}
// --- 阿里云邮件推送 ---
if os.Getenv("ALIYUN_ACCESS_KEY_ID") != "" {
senders = append(senders, aliyun.New(aliyun.Config{
AccessKeyID: os.Getenv("ALIYUN_ACCESS_KEY_ID"),
AccessKeySecret: os.Getenv("ALIYUN_ACCESS_KEY_SECRET"),
AccountName: os.Getenv("ALIYUN_ACCOUNT_NAME"),
Endpoint: os.Getenv("ALIYUN_ENDPOINT"), // 可选,默认 dm.aliyuncs.com
}))
}
// --- AWS SES ---
if os.Getenv("AWS_ACCESS_KEY_ID") != "" {
senders = append(senders, aws.New(aws.Config{
AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"),
AccessKeySecret: os.Getenv("AWS_ACCESS_KEY_SECRET"),
Region: os.Getenv("AWS_REGION"),
Sender: os.Getenv("AWS_SENDER"),
}))
}
// --- Mailgun ---
if os.Getenv("MAILGUN_API_KEY") != "" {
senders = append(senders, mailgun.New(mailgun.Config{
APIKey: os.Getenv("MAILGUN_API_KEY"),
Domain: os.Getenv("MAILGUN_DOMAIN"),
Sender: os.Getenv("MAILGUN_SENDER"),
}))
}
return senders
}
func main() {
ctx := context.Background()
// 可选:加载 .env 文件(没有 .env 时静默忽略,改用系统环境变量)
_ = godotenv.Load()
senders := buildSenders()
if len(senders) == 0 {
log.Fatal("no channel configured, please set env vars (see .env.example)")
}
// 注册到 Manager,第一个注册的成为默认通道
mgr := mailx.NewManager()
for _, s := range senders {
if err := mgr.Register(s); err != nil {
log.Fatalf("register %s: %v", s.Name(), err)
}
}
log.Printf("registered channels: %v (default: %s)", mgr.Names(), mgr.Default())
msg := mailx.NewMessage().
From(firstNonEmpty(os.Getenv("FROM_ADDR"), os.Getenv("SMTP_FROM"), os.Getenv("AWS_SENDER"), os.Getenv("MAILGUN_SENDER"))).
To(os.Getenv("TO_ADDR")).
Subject("hello from mailx (env config)").
Text("This email is sent using env-var configured channel.").
Build()
// 用默认通道发送
if err := mgr.Send(ctx, msg); err != nil {
fmt.Println("send failed:", err)
return
}
fmt.Println("send success")
}
// atoi 解析端口,失败时返回默认值
func atoi(s string, def int) int {
n := 0
if _, err := fmt.Sscanf(s, "%d", &n); err != nil || n <= 0 {
return def
}
return n
}
// firstNonEmpty 返回第一个非空字符串
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
return v
}
}
return ""
}