直接用 net/smtp 发邮件容易出错,因其不处理 MIME 编码、附件、HTML 混排,也无自动重试或超时控制,易导致乱码、被当垃圾邮件、拒收等问题。

为什么直接用 net/smtp 发邮件容易出错
Go 标准库的 net/smtp 本身不处理 MIME 编码、附件、HTML 内容混排,也不自动重试或超时控制。你写一次发信逻辑,可能在中文主题里看到乱码,在 Gmail 收件箱被当垃圾邮件,在企业邮箱里因缺少 Date 或 Message-ID 头被拒收。
常见错误现象包括:554 5.7.1 Message rejected due to spam content、Subject: =?UTF-8?B?...?= 解码失败、附件打开为空、HTML 邮件在 Outlook 里只显示源码。
- 必须手动构造
MIME多部分结构,漏掉boundary或换行符就会导致整个邮件解析失败 -
smtp.Auth类型选错(比如用smtp.PlainAuth连接要求 OAuth2 的服务)会静默失败 - 没有设置
Content-Transfer-Encoding: base64时,二进制附件内容会被 SMTP 中间网关截断
封装一个最小但可用的 Mailer 结构体
核心不是“功能多”,而是“默认安全”:统一编码、强制设置必要头字段、隔离配置与业务逻辑。
推荐结构体字段精简到这 4 个:host、port、auth、from。其余如超时、重试、日志钩子,通过方法参数或选项函数注入,避免初始化时耦合。
立即学习“go语言免费学习笔记(深入)”;
type Mailer struct {
host string
port int
auth smtp.Auth
from string
}
func NewMailer(host string, port int, username, password, from string) *Mailer {
return &Mailer{
host: host,
port: port,
auth: smtp.PlainAuth("", username, password, host),
from: from,
}
}
-
from字段必须是完整邮箱地址(如"admin@example.com"),不能只写昵称,否则某些 MX 服务器拒绝投递 - 不要在
NewMailer里做连接测试——SMTP 连接延迟高,且测试成功不代表后续发信一定成功 - 如果用腾讯企业邮或阿里云邮件推送,
host应为"smtp.exmail.qq.com"或"smtpdm.aliyun.com",端口优先选465(SSL)而非587(STARTTLS),后者需额外调用smtp.SendMail的 TLS 版本
发送 HTML + 纯文本双版本邮件的关键点
只发 HTML 邮件,iOS Mail 和某些终端客户端会显示空白;只发纯文本,又丢失样式。正确做法是用 multipart/alternative 包裹两个版本,并确保纯文本版本是 HTML 去标签后的合理降级(不是简单删 <p>)。
标准库没提供便捷构造器,得手写 MIME 头和分隔符。别自己拼字符串——用 mime/multipart 包:
w := multipart.NewWriter(wr)
w.SetBoundary("boundary_" + randStr(16))
// text/plain part
pw, _ := w.CreatePart(map[string][]string{"Content-Type": {"text/plain; charset=utf-8"}})
pw.Write([]byte(plainBody))
// text/html part
pw, _ = w.CreatePart(map[string][]string{"Content-Type": {"text/html; charset=utf-8"}})
pw.Write([]byte(htmlBody))
w.Close() // 必须调用,否则 boundary --boundary_... 不会写入末尾
-
randStr(16)用于生成唯一 boundary,避免和邮件正文里的字符串冲突 - 两个 part 的
charset=utf-8必须显式声明,否则 Outlook 可能用 GBK 解码中文 - 纯文本内容建议用
html.ToText(来自golang.org/x/net/html)提取,而不是正则粗暴去标签——保留换行和缩进可读性
生产环境绕不开的三个细节
开发机上跑通不等于线上可用。真正卡住上线的,往往是这些不起眼的配置。
- 发信频率限制:腾讯企业邮默认每分钟最多 200 封,超出直接返回
452 4.7.0 Too many messages per minute—— 要加本地限流(如golang.org/x/time/rate),别依赖 SMTP 服务端兜底 -
Message-ID头必须全局唯一且符合 RFC 5322,推荐格式:<uuid@domain>,用github.com/google/uuid生成,否则某些反垃圾系统判定为伪造邮件 - Gmail / Apple Mail 对未验证域名的发信会降权,务必在 DNS 配置
SPF(v=spf1 include:_spf.google.com ~all)、DKIM(需私钥签名)、DMARC(v=DMARC1; p=none;)——这些跟 Go 代码无关,但模块文档里必须写清楚依赖项
最常被忽略的是日志上下文:每次调用 Send 方法时,至少记录 to、subject、error 和耗时。没有这三者,线上出问题时连是不是发信超时都查不出来。


















