Go邮件服务:SMTP与模板渲染

发布时间:2026/8/19 2:59:35
Go邮件服务:SMTP与模板渲染 Go邮件服务:SMTP与模板渲染摘要: 本篇讲解Go语言发送邮件的两种方式用net/smtp标准库实现基础邮件发送用gomail库实现HTML模板渲染、附件和内嵌图片分享SMTP连接被防火墙阻断导致邮件堆积的踩坑经验对比net/smtp、gomail和第三方邮件API三种方案。开篇故事我们的系统每天发三类邮件注册验证码、订单通知、每周报告。原先是PHP时代留下的邮件脚本用mail函数直接调系统sendmail。迁到Go后第一版用net/smtp标准库发送功能跑通了但邮件内容全是纯文本产品要求加HTML模板和公司logo图片。用html/template渲染HTML邮件logo图片用内嵌资源(CID引用)发过去。写完上线效果不错。但第一周踩了个大坑机房防火墙把SMTP的25端口拦了邮件发不出去又没报错堆积了2000多封验证码邮件在队列里。后来改成走465端口SSL连接加超时和重试才稳定。这篇把Go发邮件的实现写清楚。一、net/smtp基础发送标准库net/smtp够发简单文本邮件。核心是smtp.SendMail函数传入SMTP服务器地址、认证信息、收件人和邮件内容。packagemailimport(crypto/tlsencoding/base64fmtnet/smtpstringstime)// SMTPConfig SMTP服务器配置typeSMTPConfigstruct{Hoststring// SMTP服务器地址如smtp.example.comPortint// 端口SSL常用465TLS常用587Usernamestring// 登录用户名通常是邮箱地址Passwordstring// 登录密码或授权码Fromstring// 发件人地址UseSSLbool// 是否启用SSL/TLS}// EmailMessage 邮件内容typeEmailMessagestruct{To[]string// 收件人列表Cc[]string// 抄送列表Subjectstring// 邮件主题Bodystring// 邮件正文纯文本HTMLstring// HTML正文可选}// SMTPSender 基于net/smtp的邮件发送器typeSMTPSenderstruct{config SMTPConfig}// NewSMTPSender 创建SMTP发送器funcNewSMTPSender(config SMTPConfig)*SMTPSender{returnSMTPSender{config:config}}// Send 发送邮件func(s*SMTPSender)Send(msg EmailMessage)error{// 构造邮件头headers:make(map[string]string)headers[From]s.config.From headers[To]strings.Join(msg.To,, )iflen(msg.Cc)0{headers[Cc]strings.Join(msg.Cc,, )}headers[Subject]s.encodeSubject(msg.Subject)// 日期头headers[Date]time.Now().Format(time.RFC1123Z)// MIME版本支持多格式内容headers[MIME-Version]1.0// 组装邮件内容varbodystringifmsg.HTML!{// 有HTML内容用multipart/alternativeheaders[Content-Type]text/html; charsetUTF-8bodymsg.HTML}else{// 纯文本headers[Content-Type]text/plain; charsetUTF-8bodymsg.Body}// 拼接邮件头和正文varraw strings.Builderfork,v:rangeheaders{raw.WriteString(fmt.Sprintf(%s: %s\r\n,k,v))}raw.WriteString(\r\n)raw.WriteString(body)// 组装收件人列表(包括抄送)recipients:append(msg.To,msg.Cc...)// 认证信息auth:smtp.PlainAuth(,s.config.Username,s.config.Password,s.config.Host)// 根据是否SSL选择发送方式ifs.config.UseSSL{returns.sendSSL(raw.String(),recipients,auth)}// 非SSL直接用SendMailaddr:fmt.Sprintf(%s:%d,s.config.Host,s.config.Port)returnsmtp.SendMail(addr,auth,s.config.From,recipients,[]byte(raw.String()))}// sendSSL 通过SSL/TLS连接发送func(s*SMTPSender)sendSSL(rawstring,recipients[]string,auth smtp.Auth)error{// 构造服务器地址addr:fmt.Sprintf(%s:%d,s.config.Host,s.config.Port)// 建立TLS连接tlsConfig:tls.Config{ServerName:s.config.Host}conn,err:tls.Dial(tcp,addr,tlsConfig)iferr!nil{returnfmt.Errorf(TLS连接失败: %w,err)}deferconn.Close()// 创建SMTP客户端client,err:smtp.NewClient(conn,s.config.Host)iferr!nil{returnfmt.Errorf(创建SMTP客户端失败: %w,err)}deferclient.Close()// 认证iferr:client.Auth(auth);err!nil{returnfmt.Errorf(认证失败: %w,err)}// 设置发件人iferr:client.Mail(s.config.From);err!nil{returnerr}// 添加收件人for_,to:rangerecipients{iferr:client.Rcpt(to);err!nil{returnerr}}// 获取写入器写入邮件内容w,err:client.Data()iferr!nil{returnerr}deferw.Close()_,errw.Write([]byte(raw))returnerr}// encodeSubject 编码邮件主题处理中文func(s*SMTPSender)encodeSubject(subjectstring)string{// 用BASE64编码非ASCII字符RFC 2047格式return?UTF-8?B?base64.StdEncoding.EncodeToString([]byte(subject))?}net/smtp的好处是标准库自带没有外部依赖。缺点是处理HTML、附件、内嵌图片要手动拼MIME格式代码冗长易错。生产环境建议用gomail。二、gomail库与模板渲染gomail封装了邮件发送的细节API简洁。支持HTML正文、附件、内嵌图片配合html/template渲染邮件模板。packagemailimport(bytesfmthtml/templatetimegithub.com/wneessen/go-mail)// MailConfig gomail配置typeMailConfigstruct{Hoststring// SMTP服务器Portint// 端口Usernamestring// 用户名Passwordstring// 密码Fromstring// 发件人FromNamestring// 发件人昵称}// MailSender gomail发送器typeMailSenderstruct{config MailConfig tmpls*template.Template// 预编译的邮件模板}// NewMailSender 创建邮件发送器// templateDir: 邮件模板目录路径funcNewMailSender(config MailConfig,templateDirstring)(*MailSender,error){// 预编译所有邮件模板// 模板文件放在templateDir下如verify_code.htmltmpls,err:template.ParseGlob(templateDir/*.html)iferr!nil{returnnil,fmt.Errorf(加载模板失败: %w,err)}returnMailSender{config:config,tmpls:tmpls,},nil}// TemplateEmail 模板邮件请求typeTemplateEmailstruct{Tostring// 收件人Subjectstring// 主题Templatestring// 模板名如verify_codeDatamap[string]interface{}// 模板数据Attachmentstring// 附件路径可选InlineImgstring// 内嵌图片路径可选}// SendTemplate 发送模板邮件func(s*MailSender)SendTemplate(req TemplateEmail)error{// 创建gomail消息对象msg:mail.NewMsg()// 设置发件人msg.FromFormat(s.config.FromName,s.config.From)// 设置收件人msg.To(req.To)// 设置主题msg.Subject(req.Subject)// 渲染HTML模板varbuf bytes.Bufferiferr:s.tmpls.ExecuteTemplate(buf,req.Template,req.Data);err!nil{returnfmt.Errorf(渲染模板失败: %w,err)}// 设置HTML正文msg.SetBodyString(mail.TypeTextHTML,buf.String())// 添加附件ifreq.Attachment!{iferr:msg.AttachFile(req.Attachment);err!nil{returnerr}}// 添加内嵌图片用CID引用ifreq.InlineImg!{// 嵌入图片HTML中用img srccid:logoiferr:msg.EmbedFile(req.InlineImg);err!nil{returnerr}}// 创建SMTP客户端client,err:mail.NewClient(s.config.Host,mail.WithPort(s.config.Port),mail.WithSMTPPortSecure(mail.SSLTLS),mail.WithUsername(s.config.Username),mail.WithPassword(s.config.Password),// 连接超时10秒mail.WithTimeout(10*time.Second),)iferr!nil{returnerr}// 发送邮件returnclient.DialAndSend(msg)}邮件模板用标准html/template语法。下面是验证码邮件模板的示例。!-- verify_code.html: 验证码邮件模板 --!DOCTYPEhtmlhtmlheadmetacharsetUTF-8stylebody{font-family:sans-serif;color:#333;}.code{font-size:32px;font-weight:bold;color:#2563eb;}.footer{color:#999;font-size:12px;margin-top:20px;}/style/headbodyimgsrccid:logoalt公司Logowidth120h2{{.Title}}/h2p您的验证码是:/ppclasscode{{.Code}}/pp验证码有效期{{.Expire}}分钟请勿泄露给他人。/pdivclassfooter此邮件由系统自动发送请勿回复。/div/body/html模板里的cid:logo引用内嵌图片发送时把logo图片嵌入邮件收件人打开邮件能直接看到图片不需要从外部加载。这个体验比外链图片好也避免被邮件客户端拦截。发送验证码的调用代码。packagemail// SendVerifyCode 发送验证码邮件func(s*MailSender)SendVerifyCode(to,codestring)error{returns.SendTemplate(TemplateEmail{To:to,Subject:您的验证码,Template:verify_code,InlineImg:/assets/logo.png,Data:map[string]interface{}{Title:注册验证码,Code:code,Expire:5,},})}三、批量发送与重试批量发邮件要控制发送频率。SMTP服务器对同一发送方有速率限制瞬间发几百封会被临时封禁。packagemailimport(fmttime)// SendBatch 批量发送邮件控制速率// emails: 邮件列表// ratePerMinute: 每分钟最多发送数量func(s*MailSender)SendBatch(emails[]TemplateEmail,ratePerMinuteint)[]error{// 计算每封邮件的间隔interval:time.Minute/time.Duration(ratePerMinute)errors:make([]error,0,len(emails))for_,email:rangeemails{// 发送单封err:s.SendTemplate(email)iferr!nil{errorsappend(errors,err)// 发送失败继续下一封不中断批量continue}// 速率控制等待间隔time.Sleep(interval)}returnerrors}// SendWithRetry 带重试的发送// 单封邮件发送失败自动重试3次func(s*MailSender)SendWithRetry(req TemplateEmail,maxRetryint)error{varlastErrerrorfori:0;imaxRetry;i{err:s.SendTemplate(req)iferrnil{returnnil// 发送成功}lastErrerr// 指数退避等待wait:time.Duration(1i)*time.Second time.Sleep(wait)}returnfmt.Errorf(重试%d次仍失败: %w,maxRetry,lastErr)}批量发送用令牌桶或固定间隔控制速率。失败重试用指数退避第一次等1秒第二次2秒第三次4秒避免短时间内反复冲击SMTP服务器。四、踩坑经验:SMTP连接被墙导致邮件堆积这个坑排查了好几个小时。系统上线后注册验证码功能偶发延迟用户反馈收不到验证码。查日志发现SMTP发送没有报错队列里积了2000多封验证码邮件状态全是发送中。排查发现机房防火墙把SMTP标准25端口封了。net/smtp的SendMail默认连25端口连接建立但数据发不出去卡在握手阶段。SendMail没有超时参数goroutine一直阻塞等待队列越积越多。解决方案分三步。第一改用465端口SSL连接465端口在防火墙白名单里。第二所有SMTP操作加超时。第三发送失败入死信队列不再无限阻塞。packagemailimport(contextfmttime)// FixSMTPConnection 修复后的连接配置funcNewFixedMailSender(config MailConfig)(*MailSender,error){// 强制使用465端口SSLconfig.Port465// 确保使用SSL/TLSreturnNewMailSender(config,./templates)}// SafeSend 安全发送带超时和死信处理func(s*MailSender)SafeSend(req TemplateEmail)error{// 用context控制超时ctx,cancel:context.WithTimeout(context.Background(),30*time.Second)defercancel()// 在goroutine中发送用channel接收结果result:make(chanerror,1)gofunc(){result-s.SendTemplate(req)}()// 等待结果或超时select{caseerr:-result:returnerrcase-ctx.Done():// 超时邮件入死信队列returnfmt.Errorf(发送超时邮件需入死信队列)}}这次踩坑后总结了几条邮件发送原则。端口首选465(SSL)或587(TLS)不用25端口。所有SMTP操作必须设超时net/smtp的SendMail没超时参数要么用gomail的WithTimeout要么自己用context控制。发送失败要有降级策略验证码场景可以降级到短信通知。批量发送要监控队列长度队列超过阈值告警避免静默堆积。五、对比分析特性net/smtpgomail第三方API(SendGrid)依赖标准库无依赖第三方库HTTP调用功能纯文本为主HTML/附件/内嵌图片全功能超时控制无原生支持支持HTTP超时模板渲染手动拼接配合html/template平台模板运维成本自建SMTP服务器自建SMTP服务器无需运维到达率依赖IP信誉依赖IP信誉高(专业投递)费用免费免费按量计费net/smtp适合简单场景纯文本邮件够用。gomail适合生产环境HTML模板和附件都支持API简洁。第三方API如SendGrid和阿里云邮件推送适合大批量发送到达率高但按量收费。自建SMTP的到达率取决于服务器IP信誉新IP容易被邮箱服务商当垃圾邮件需要预热IP信誉。总结发邮件用gomail库配合html/template渲染模板net/smtp标准库只适合简单纯文本场景。端口选465或587不用25端口。所有SMTP操作设超时用context控制避免goroutine阻塞。内嵌图片用CID引用邮件打开体验好。批量发送控速率配重试监控队列长度防止静默堆积。