Golang 使用 Template 引擎构建漂亮的邮件内容并且发送邮件

背景

邮件是常见的触达用户的途径,本文详细介绍基于 golang 的模版引擎构建漂亮的邮件内容,并且发送给模板用户。

思路

go 内置了 html/template 模块,类似 ejs 模块引擎。利用 template 能力可以将变量动态的注入到HTML字符串中,最终获得成功注入变量的字符串内容。

具体实现思路:

  1. 首先根据设计图输出静态的HTML文件;
  2. 然后将HTML中需要变化的内容提取变量占位符;
  3. 利用 template 工具将 HTML 中的变量按照规则注入;
  4. 最终通过运行 template 引擎,获得最终的动态HMTL内容;

邮件内容模版

1. 输出静态HTML文件

根据 figma 设计图编写对应的 HTML 代码;根据产品要求,内容需要动态变化的地方提取变量,方便后续的动态内容注入。

Untitled.png

javascript
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="x-apple-disable-message-reformatting"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>乐闻世界 - 列表</title> </head> <body style="margin: 0;padding: 0;line-height: inherit;margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #f6f6f6;color: #333333"> <table style="vertical-align: top;border-collapse: collapse;line-height: inherit;color: #000000;border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #f6f6f6;width:100%" cellpadding="0" cellspacing="0"> <tbody style="line-height: inherit;"> <tr style="vertical-align: top;border-collapse: collapse;line-height: inherit;vertical-align: top"> <td style="vertical-align: top;border-collapse: collapse;line-height: inherit;color: #000000;word-break: break-word;border-collapse: collapse !important;vertical-align: top"> <!-- 邮件主体内容 --> <div style="line-height: inherit;padding-top: 24px;background-color: transparent"> <div style="line-height: inherit;Margin: 0 auto;min-width: 320px;max-width: 620px;min-height: 512px; overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;"> <div style="line-height: inherit;border-collapse: collapse;display: table;width: 100%;background-color: transparent;"> <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 16px 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:620px;"><tr style="background-color: transparent;"><![endif]--> <!--[if (mso)|(IE)]><td align="center" width="620" style="background-color: #ffffff;width: 620px;padding: 32px 0px 24px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]--> <div style="line-height: inherit;max-width: 620px;min-width: 320px;display: table-cell;vertical-align: top;"> <div style="line-height: inherit;background-color: #ffffff;width: 100% !important;border-radius: 8px;"> <!--[if (!mso)&(!IE)]><!--> <div style="line-height: inherit;padding: 48px 36px 96px 36px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"> <!--<![endif]--> <!-- LOGO--> {{if .Picture}} <table style="line-height: inherit;color: #000000;font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody style="line-height: inherit;"> <tr style="line-height: inherit;"> <td style="line-height: inherit;color: #000000;overflow-wrap:break-word;word-break:break-word;padding:0px;font-family:arial,helvetica,sans-serif;" align="left"> <img align="center" border="0" src="{{.Picture}}" alt="乐闻世界" title="乐闻世界" style="line-height: inherit;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: inline-block !important;border: none;height: auto;float: none;width: 60%;max-width: 300px;margin-bottom: 48px;" width="173.6" /> </td> </tr> </tbody> </table> {{end}} <!-- 邮件内容 --> <table style="vertical-align: top;border-collapse: collapse;line-height: inherit;color: #000000;font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody> <tr style="vertical-align: top;border-collapse: collapse;"> <td style="vertical-align: top;border-collapse: collapse;line-height: inherit;color: #000000;overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left"> <div style="line-height: 26px; text-align: left; word-wrap: break-word;font-size: 16px;font-weight: 500;"> <p style="margin: 0;font-size: 16px; line-height: 26px;">{{.Title}}</p> <p style="margin: 0;font-size: 16px; line-height: 26px;"> <br />{{.Desc}}</p> {{if .Logs}} <!-- 警示语 --> <p style="font-size: 14px;font-weight: 600; line-height: 22px;color: #999999;margin: 24px 0;"> ⚠️ {{.Warning}} </p> {{end}} </div> </td> </tr> </tbody> </table> <!-- 日志内容 --> {{range .Logs}} <div style="line-height: inherit;border-top:1px dashed #BDBDBD;padding-top: 24px;"> <div style="line-height: 26px; text-align: left; word-wrap: break-word;font-size: 16px;font-weight: 500;"> {{if .IsRespondent}} <p style="margin: 0;font-size: 14px;font-weight: 700; line-height: 22px;color: #338AFF;"> {{.Title}}</p> {{else}} <p style="margin: 0;font-size: 14px;font-weight: 700; line-height: 22px;color: #6ABF40;"> {{.Title}}</p> {{end}} <p style="margin: 0;font-size: 14px;font-weight: 400; line-height: 22px;color: #999999"> {{.Time}} </p> <p style="margin: 0;font-size: 14px;font-weight: 400; line-height: 22px;color: #333333;margin-top: 8px;"> {{.Content}} </p> </div> </div> {{if .AttachFiles}} <table style="vertical-align: top;border-collapse: collapse;line-height: inherit;color: #000000;margin-bottom:24px;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody style="line-height: inherit;"> {{range .AttachFiles}} <tr style="vertical-align: top;border-collapse: collapse;line-height: inherit;overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left"> {{range .}} <td style="vertical-align: top;border-collapse: collapse;line-height: inherit;color: #000000;overflow-wrap:break-word;word-break:break-word;padding:0px;font-family:arial,helvetica,sans-serif;" align="left"> <img align="center" border="0" src="{{.}}" alt="乐闻世界" title="乐闻世界" style="line-height: inherit;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: inline-block !important;border: none;height: auto;float: none;width: 80%;max-width: 300px;margin-top: 8px;" /> </td> {{end}} </tr> {{end}} </tbody> </table> {{end}} {{end}} </div> </div> </div> </div> </div> </div> </td> </tr> </tbody> </table> </body> </html>

2. Template 注入变量

根据 HTML 模版中提取的变量占位符,定义业务动态变量内容。

javascript
package main import ( "fmt" "net/http" "text/template" ) type TicketLog struct { Title string Time string Content string IsRespondent bool AttachFiles [][]string // 两个一组 } type TicketInfo struct { Picture string Title string Desc string Warning string Logs []TicketLog } func renderHtml(responseWriter http.ResponseWriter, request *http.Request) { // 解析指定文件生成模板对象 tmpl, err := template.ParseFiles("./templates/levenx.html") if err != nil { fmt.Println("create template failed, err:", err) return } responseWriter.Header().Set("Content-Type", "text/html; charset=utf-8") ticketLogs := []TicketLog{ { Title: "[乐闻的回复]", Time: "2022.05.05 05:05:05", Content: "这是被投诉方的回复这是被投诉方的回复 这是被投诉方的回复 这是被投诉方的回复 这是被投诉方的回复 这是被投诉方的回复 这是被投诉方的回复", IsRespondent: true, AttachFiles: [][]string{ {"http://localhost:3000/static/attach.png", "http://localhost:3000/static/attach.png"}, {"http://localhost:3000/static/attach.png", "http://localhost:3000/static/attach.png"}, }, }, { Title: "[客服的回复]", Time: "2022.05.05 05:05:05", Content: "对不起,给您的使用带来了困扰,我们会尽快解决你的问题。", IsRespondent: false, }, } ticketInfo := TicketInfo{ Picture: "http://localhost:3000/static/logo.png", Title: "乐闻的工单", Desc: "对于您的工单12121,如果您对回复有任何疑问,可以直接回复此邮件。", Warning: "请不要修改邮件标题,否则我们的团队无法收到您的回复", Logs: ticketLogs, } tmpl.Execute(responseWriter, ticketInfo) }

3. 启动 Http 服务器,向网页输出动态HTML内容

启动 http server,支持静态资源,static 文件夹中的所有静态资源都可以通过http服务访问到。

javascript
func main() { fs := http.FileServer(http.Dir("assets/")) http.Handle("/static/", http.StripPrefix("/static/", fs)) http.HandleFunc("/", renderHtml) err := http.ListenAndServe("127.0.0.1:3000", nil) if err != nil { fmt.Println("HTTP server failed,err:", err) return } fmt.Print("访问 http://localhost:3000") }

尝试访问 http://localhost:3000

Untitled.png

发送邮件

邮件传输需要遵循特定的协议,其中使用SMTP协议即可完成邮件的发送和回复。golang 有现成的工具库支持了邮件发送,我们接下来将实现发送上面Template模板输出的内容到特定的邮箱。

  1. 安装依赖库
javascript
go get github.com/jordan-wright/email
  1. 获取邮件服务商的邮件授权码,比如使用QQ的企业账号

    根据下面截图开启SMTP服务,生成授权码

    Untitled.png

    Untitled.png

  2. 使用授权码,开始发送邮件

    javascript
    import ( "fmt" "net/http" "text/template" "net/smtp" "github.com/jordan-wright/email" "log" "bytes" ) func sendMail() { body := new(bytes.Buffer) tmpl.Execute(body, ticketInfo) e := email.NewEmail() //设置发送方的邮箱 e.From = "乐闻 <1025534801@qq.com>" // 设置接收方的邮箱 e.To = []string{"接受邮件的邮箱"} //设置主题 e.Subject = "乐闻的工单" //设置文件发送的内容 e.HTML = body.Bytes() auth := smtp.PlainAuth("", "发送邮件的邮箱", "授权码", "smtp.qq.com"); //设置服务器相关的配置 error := e.Send("smtp.qq.com:25",auth) if error != nil { log.Fatal(error) } }

    Untitled.png