用 net/smtp + gin 实现多人邮件发送完全可行,但需手动拼 mime 头、将收件人切片转为逗号分隔的 to: 头字段并确保格式合规(去空格、小写、长度≤1000 字节、utf-8 编码),否则易被 qq/163 等邮箱拒收或进垃圾箱。

直接说结论:用 net/smtp + gin 实现多人邮件发送,完全可行,但必须自己拼 MIME 头、处理多收件人、规避 smtp.SendMail 的单地址限制——第三方库(如 gomail)能省事,但引入依赖会增加部署复杂度和 TLS 兼容风险。
为什么 smtp.SendMail 默认不支持多人?
smtp.SendMail 的第四个参数是 []string(收件人列表),但它只用于 SMTP 协议的 RCPT TO 阶段;而邮件正文里的 To: 头字段仍是单值。很多邮箱服务(如 QQ、163)会校验头字段与实际投递地址是否一致,不一致就拒收或进垃圾箱。
- 现象:
contacts传了["a@q.com", "b@q.com"],但只有第一个收到,或全部被拦截 - 根本原因:你拼的
msg字节数组里To:后只写了一个地址,而smtp.SendMail内部把所有[]string当作 RCPT 列表发给 SMTP 服务器——二者语义错位 - 解决方向:要么把
To:头改成逗号分隔的多个邮箱(需注意长度和编码),要么为每个收件人单独构造并发送一封邮件(更稳妥)
多人发送时怎么拼 msg 字节数组?
关键不是“能不能”,而是“怎么拼才不被当垃圾邮件”。直接用 strings.Join(json.Contacts, ", ") 塞进 To: 头,对多数国内邮箱(QQ/163/Outlook)有效,但有三个硬约束:
-
json.Contacts里的每个邮箱必须已去空格、小写标准化(strings.TrimSpace+strings.ToLower),否则 QQ 邮箱会报553 Mail from address must be same as authorization user - 总长度不能超 1000 字节(含换行符),否则部分 SMTP 服务器截断或拒绝,建议单次群发控制在 20 人以内
- 如果内容含中文或 emoji,
Content-Type必须显式声明charset=UTF-8,且body要用utf8.EncodeRune确保无乱码(Go 1.19+ 默认支持,但旧版本需手动处理)
示例片段:
toHeader := strings.Join(json.Contacts, ", ")
msg := []byte("To: " + toHeader + "\r\n" +
"From: " + sendUserName + "\r\n" +
"Subject: =?UTF-8?B?" + base64.StdEncoding.EncodeToString([]byte(json.Subject)) + "?=\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"MIME-Version: 1.0\r\n\r\n" +
json.Content)
要不要加中间件做频率限制和鉴权?
要,而且必须前置。裸接口暴露在公网等于给黑产送 SMTP 中继。
- 鉴权别只靠 JSON 里的
source字段——它可被任意伪造;应从c.Request.Header.Get("X-API-Key")提取 token,查白名单或 JWT 解析 - 限流用
golang.org/x/time/rate最轻量,每 IP 每分钟 5 封足够告警,别用内存型限流器(重启失效),更别用 Redis(小项目杀鸡用牛刀) - 记录日志时至少打上
c.ClientIP()和len(json.Contacts),方便事后追溯群发行为
真正麻烦的不是代码,是 SMTP 服务商的策略漂移——今天能发 50 封,明天可能因某封被投诉就临时限频。所以生产环境务必留一条退路:当 smtp.SendMail 返回 timeout 或 5xx 错误时,不要重试,直接记 error log 并返回 HTTP 503,避免雪崩。











