当前位置:首页 > 文章列表 > Golang > Go教程 > Go go-smtp 怎样给同一封邮件添加多个收件人

Go go-smtp 怎样给同一封邮件添加多个收件人

来源:17golang原创 2026-10-30 22:13:15 0浏览 收藏

在 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,不写入消息头,这样其他收件人看不到密送地址。

Go go-smtp SendMail、多个 SMTP 收件人和 To Cc Bcc 头部的静态关系说明图
图1:静态结构说明图,展示 SendMail 组织多个收件人时 API、信封和头部的关系,不是运行截图。

用 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
}
Go go-smtp Client Mail Rcpt Data 和 SMTPError 的静态关系说明图
图2:静态边界说明图,展示 Client 方案逐个添加收件人时的对象与错误边界,不是运行证据。

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.SendMailAPI 简短,直接传入 []string
要知道每个地址的 SMTP 响应Client.Rcpt每个收件人都有独立调用点
要复用连接发送多封邮件Client可以自己控制连接、事务和退出时机
需要 DKIM、附件或复杂 MIME组合其他邮件库后再交给 go-smtpgo-smtp 本身是低层 SMTP 机制

可以把决策压缩成一句话:只关心“这封信发给哪些地址”,传 []string 给 SendMail;关心“每一个地址在 SMTP 阶段发生了什么”,就用 Mail 加多次 Rcpt。

相关问题

多个收件人能不能只写在 To 头部?

不建议。To 头部只是消息内容的一部分,真正的 SMTP 投递目标仍要通过 SendMail 的切片或 Client.Rcpt 传递。

Bcc 地址是否也要加入 recipients?

要。Bcc 通常不写入消息头,但仍要加入 envelope 的收件人列表,否则 SMTP 服务不会尝试投递给它。

Client.Rcpt 被拒绝后还能继续加地址吗?

协议层允许在一次 Mail 后继续处理其他 Rcpt,但是否继续应由你的业务策略决定。最简单的全量发送策略是记录错误并终止,部分投递策略则要显式维护已接受地址集合。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go go-message 怎样解码带字符集声明的邮件主题Go go-message 怎样解码带字符集声明的邮件主题
上一篇
Go go-message 怎样解码带字符集声明的邮件主题
Go go-message 遇到未知字符集错误时能否继续读取
下一篇
Go go-message 遇到未知字符集错误时能否继续读取
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    798次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    791次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    877次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    848次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    679次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码