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

背景

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

思路

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

具体实现思路:

  1. 首先根据设计图输出静态的HTML文件;
  2. 然后将HTML中需要变化的内容提取变量占位符;
  3. 利用 template 工具将 HTML 中的变量按照规则注入;
  4. 最终通过运行 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模板输出的内容到特定的邮箱。

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

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

  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)
      }
    }
相关推荐
小五Five几秒前
TypeScript项目中Axios的封装
开发语言·前端·javascript
前端每日三省2 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
凡人的AI工具箱15 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
java亮小白199723 分钟前
Spring循环依赖如何解决的?
java·后端·spring
chnming198731 分钟前
STL关联式容器之map
开发语言·c++
进击的六角龙33 分钟前
深入浅出:使用Python调用API实现智能天气预报
开发语言·python
檀越剑指大厂33 分钟前
【Python系列】浅析 Python 中的字典更新与应用场景
开发语言·python
2301_8112743140 分钟前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
湫ccc41 分钟前
Python简介以及解释器安装(保姆级教学)
开发语言·python
程序伍六七44 分钟前
day16
开发语言·c++