背景
邮件是常见的触达用户的途径,本文详细介绍基于 golang 的模版引擎构建漂亮的邮件内容,并且发送给模板用户。
思路
go 内置了 html/template 模块,类似 ejs 模块引擎。利用 template 能力可以将变量动态的注入到HTML字符串中,最终获得成功注入变量的字符串内容。
具体实现思路:
- 首先根据设计图输出静态的HTML文件;
- 然后将HTML中需要变化的内容提取变量占位符;
- 利用 template 工具将 HTML 中的变量按照规则注入;
- 最终通过运行 template 引擎,获得最终的动态HMTL内容;
邮件内容模版
1. 输出静态HTML文件
根据 figma 设计图编写对应的 HTML 代码;根据产品要求,内容需要动态变化的地方提取变量,方便后续的动态内容注入。
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
发送邮件
邮件传输需要遵循特定的协议,其中使用SMTP协议即可完成邮件的发送和回复。golang 有现成的工具库支持了邮件发送,我们接下来将实现发送上面Template模板输出的内容到特定的邮箱。
- 安装依赖库
javascript
go get github.com/jordan-wright/email
-
获取邮件服务商的邮件授权码,比如使用QQ的企业账号
根据下面截图开启SMTP服务,生成授权码
-
使用授权码,开始发送邮件
javascriptimport ( "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) } }