Go go-smtp 怎样给同一封邮件添加多个收件人
来源:17golang原创
时间:2026-10-30 22:13:15 296浏览 收藏
在 Go 里用 github.com/emersion/go-smtp 发同一封邮件给多人,关键不是把地址拼成一个字符串,而是把每个地址作为一个独立的 SMTP 收件人交给库。最简单的入口是 smtp.SendMail 的 []string 参数;需要逐个处理收件人响应时,则使用 Client.Mail 后连续调用 Client.Rcpt。
官方地址:https://pkg.go.dev/github.com/emersion/go-smtp
同一封邮件的多个收件人放在 SMTP envelope 的收件人列表中;邮件内容里的To、Cc、Bcc头部只负责展示和表达副本关系。最小场景传入[]string,需要每个地址的错误边界时改用多次Rcpt。
先把“收件人”拆成两个层次
SMTP 发送至少有两组容易混淆的地址。第一组是 envelope 收件人,也就是服务器收到的多个 RCPT TO 目标,决定这封消息会尝试投递给谁。第二组是消息头中的 To、Cc、Bcc,它们会随邮件内容传给客户端,用来展示收件关系。
因此,To: alice@example.com, bob@example.com 不能代替 SMTP envelope 的多个收件人。反过来,把多个地址放进 envelope,也不会自动替你生成收件人能看到的 To 或 Cc 头。Bcc 通常只加入 envelope,不写入消息头,这样其他收件人看不到密送地址。

用 SendMail 给一封邮件添加多个地址
smtp.SendMail 的第四个参数是 []string。切片中的每个地址都会成为 SMTP envelope 的一个收件人;消息本身仍然通过最后的 io.Reader 传入。
package main
import (
"log"
"strings"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
)
func main() {
// 认证对象只负责登录 SMTP 服务,收件人列表单独传给 SendMail。
auth := sasl.NewPlainClient("", "sender@example.org", "password")
// 每个元素都是一个独立的 SMTP envelope 收件人,不要拼成逗号字符串。
recipients := []string{
"alice@example.net",
"bob@example.net",
"audit@example.net",
}
// To/Cc 头部控制收件人看到的副本关系;Bcc 地址不写入头部。
msg := strings.NewReader("From: sender@example.org\r\n" +
"To: alice@example.net\r\n" +
"Cc: bob@example.net\r\n" +
"Subject: team notice\r\n" +
"\r\n" +
"This is the message body.\r\n")
// 地址列表、邮件内容和认证信息在一次调用中组成一封邮件。
if err := smtp.SendMail("mail.example.com:25", auth,
"sender@example.org", recipients, msg); err != nil {
// 发送失败时保留原始错误,便于区分连接、认证和收件人问题。
log.Fatal(err)
}
}
这个例子里 audit@example.net 也在 envelope 中,所以它会参与投递;但它没有出现在 To 或 Cc 头部,适合表达密送关系。若要真正使用 Bcc,应把它留在 recipients 中,同时不写入任何消息头。
官方文档还说明,传给 SendMail 的消息应是先放头部、再空一行、最后放正文的 RFC 822 风格内容,行结束使用 CRLF。SendMail 适合简单发送;它不会替你提供 DKIM 签名或 MIME 附件能力。
什么时候应该改用 Client.Rcpt
如果只想把多个地址交给一次简单发送,SendMail 已经够用。如果需要知道哪个地址在 SMTP 阶段被拒绝,或者需要在进入正文发送前制定自己的收件人策略,就使用低层 Client:先调用一次 Mail,再对每个地址调用一次 Rcpt,最后调用 Data 写入正文。
package mailer
import (
"fmt"
"io"
"github.com/emersion/go-smtp"
)
func sendWithClient(raw string, from string, recipients []string) error {
// Dial 只建立到 SMTP 服务的连接;生产环境应根据服务要求选择 TLS 入口。
client, err := smtp.Dial("mail.example.com:25")
if err != nil {
return fmt.Errorf("连接 SMTP 服务失败: %w", err)
}
// 未完成 Quit 时仍关闭连接,避免连接错误路径泄漏资源。
defer client.Close()
// 一个 Mail 开始一笔邮件事务,后面可以跟多个 Rcpt。
if err := client.Mail(from, nil); err != nil {
return fmt.Errorf("设置发件人失败: %w", err)
}
// 每个地址单独发送 Rcpt,便于保留被拒绝地址和服务器错误。
for _, recipient := range recipients {
if err := client.Rcpt(recipient, nil); err != nil {
return fmt.Errorf("收件人 %s 被 SMTP 服务拒绝: %w", recipient, err)
}
}
// 只有收件人阶段完成后才进入 Data,并写入完整的 RFC 822 消息。
writer, err := client.Data()
if err != nil {
return fmt.Errorf("打开 DATA 阶段失败: %w", err)
}
if _, err := io.WriteString(writer, raw); err != nil {
// 写入失败时先关闭数据写入器,再由 defer 关闭客户端连接。
_ = writer.Close()
return fmt.Errorf("写入邮件正文失败: %w", err)
}
if err := writer.Close(); err != nil {
return fmt.Errorf("结束 DATA 阶段失败: %w", err)
}
// Quit 正常结束 SMTP 会话;失败时 defer 仍会执行 Close。
if err := client.Quit(); err != nil {
return fmt.Errorf("结束 SMTP 会话失败: %w", err)
}
return nil
}

Rcpt 必须发生在 Mail 之后,可以在下一个 Rcpt 前继续处理地址。示例选择遇到一个拒绝就返回,代表“全部目标都被接受后才发送正文”的应用策略;如果业务允许部分投递,就必须把已接受、被拒绝和最终消息状态分开记录,不能简单忽略错误。
To、Cc、Bcc 应该怎样和切片对应
| 业务关系 | 消息头 | envelope 收件人 | 其他收件人是否可见 |
|---|---|---|---|
| 主送 | 写入 To | 加入 recipients 或调用 Rcpt | 可见 |
| 抄送 | 写入 Cc | 加入 recipients 或调用 Rcpt | 可见 |
| 密送 | 不写入 Bcc | 加入 recipients 或调用 Rcpt | 对其他收件人隐藏 |
这张表的关键是最后一列:To 和 Cc 的展示关系由消息头决定,但是否真的投递给某个地址仍要看 envelope;Bcc 则通常只存在于 envelope。多收件人文章里最常见的错误,就是只修改 To 文本,却没有把所有目标地址加入 []string 或多次 Rcpt。
几个容易踩到的边界
空切片不是“发给所有人”
收件人切片应该在调用前按业务规则生成,并明确处理空列表。空列表不能被理解为广播,也不应该靠 SMTP 服务端猜测目标。若没有合法收件人,直接在应用层返回错误更容易定位问题。
一个地址被拒绝后不要静默忽略
低层方案下,每次 Rcpt 都可能返回 SMTP 错误。继续发送还是立即终止,应由业务策略决定;无论选择哪一种,都要保存地址和错误,否则最终只会看到“邮件发送失败”而不知道是哪个目标出了问题。
消息头和 envelope 要保持业务语义一致
如果把地址加入 envelope,却完全不在消息头中表达主送或抄送关系,收件人仍可能收到邮件,但阅读体验会变得混乱。Bcc 是有意隐藏的例外;其他地址最好让头部和 envelope 的业务关系保持一致。
写完正文后再关闭 Data 写入器
Data 返回的写入器需要关闭,库才会结束正文阶段。之后再调用 Quit 完成会话;若连接或写入路径提前出错,也要保留原始错误并关闭资源。
SendMail 和 Client 的选择
| 场景 | 建议 | 原因 |
|---|---|---|
| 固定收件人列表,统一发送 | smtp.SendMail | API 简短,直接传入 []string |
| 要知道每个地址的 SMTP 响应 | Client.Rcpt | 每个收件人都有独立调用点 |
| 要复用连接发送多封邮件 | Client | 可以自己控制连接、事务和退出时机 |
| 需要 DKIM、附件或复杂 MIME | 组合其他邮件库后再交给 go-smtp | go-smtp 本身是低层 SMTP 机制 |
可以把决策压缩成一句话:只关心“这封信发给哪些地址”,传 []string 给 SendMail;关心“每一个地址在 SMTP 阶段发生了什么”,就用 Mail 加多次 Rcpt。
相关问题
多个收件人能不能只写在 To 头部?
不建议。To 头部只是消息内容的一部分,真正的 SMTP 投递目标仍要通过 SendMail 的切片或 Client.Rcpt 传递。
Bcc 地址是否也要加入 recipients?
要。Bcc 通常不写入消息头,但仍要加入 envelope 的收件人列表,否则 SMTP 服务不会尝试投递给它。
Client.Rcpt 被拒绝后还能继续加地址吗?
协议层允许在一次 Mail 后继续处理其他 Rcpt,但是否继续应由你的业务策略决定。最简单的全量发送策略是记录错误并终止,部分投递策略则要显式维护已接受地址集合。
-
364 收藏
-
263 收藏
-
397 收藏
-
108 收藏
-
349 收藏
-
350 收藏
-
297 收藏
-
219 收藏
-
346 收藏
-
451 收藏
-
248 收藏
-
234 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习