登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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

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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>