Go开发学习 | 如何使用Gomail.v2模块包发送邮箱验证码消息及附件学习记录

欢迎关注「全栈工程师修炼指南」公众号

点击 👇 下方卡片 即可关注我哟!

设为星标⭐每天带你 基础入门 到 进阶实践 再到 放弃学习

  花开堪折直须折,莫待无花空折枝 


作者主页:[ https://www.weiyigeek.top ]  

博客:[ https://blog.weiyigeek.top ]

作者安全运维学习答疑交流群:请关注公众号回复【学习交流群

原文地址:

Go开发学习 | 如何使用Gomail.v2模块包发送邮箱验证码消息及附件学习记录在某些系统中往往需要实时的监控应用的健康信息以及关键操作信息的发送,若要使用Go语言实现上述报警信息的发送,通常会在企业中使用邮件的形式或者Webhook钩子的形式进行预警(例如,钉钉、企业微信)推荐,本章针对gomail模块进行实践学习。https://mp.weixin.qq.com/s/fHv8u1gqJpKfjBkcaohZ0Q


文章目录:

0x00 前言简述

0x01 常用模块

    • Gomail 模块 - 邮件发送模块

      • 示例演示

        • 示例1.gomail快速上手实践

        • 示例2.gomail邮件消息群发

        • 示例3.自定义方法封装Gomail实现发送文本、HTML以及附件

        • 示例4.Gomail发送HTML注册找回验证码

        • 示例 5.正式环境中我们一般会用管道 channel 来创建一个邮件发送服务.

      • 入坑出坑


0x00 前言简述

描述: 在某些系统中往往需要实时的监控应用的健康信息以及关键操作信息的发送,若要使用Go语言实现上述报警信息的发送,通常会在企业中使用邮件的形式或者Webhook钩子的形式进行预警(例如,钉钉、企业微信)推荐,当然你也可以使用openwechat项目实现个人微信推送以及go-cqhttp项目实现QQ推送,不论你使用何种方式实现信息发送都是可以请根据自身的实际情况进行选择。


0x01 常用模块

Gomail 模块 - 邮件发送模块

描述: Gomail是一个简单高效的发送电子邮件go语言的模块包,其使用SMTP服务器发送电子邮件含附件,请注意它需要 Go 1.2 或更高版本。
项目地址: https://github.com/go-gomail/gomail
文档地址: https://pkg.go.dev/gopkg.in/gomail.v2
支持特性:

  • 附件发送

  • 嵌入的图像

  • 网页和文本模板

  • 特殊字符的自动编码

  • SSL 和 TLS

  • 使用相同的 SMTP 连接发送多封电子邮件

示例演示

示例1.gomail快速上手实践

描述: 使用 gopkg.in/gomail.v2发送邮件一般有 9 个步骤,分别如下

  •  首先构建一个 Message 对象,也就是邮件对象

  •  填充发件人 From

  •  填充收件人 To

  •  填充抄送 Cc

  •  设置邮件标题 Subject

  •  设置邮件正文

  •  如果有需要,可以添加附件

  •  实例化一个邮件发送器

  •  连接到邮件服务器并发送,此处主要有两个方法 mailer.NewDialer 和 mailer.Send 或者通过管道形式。

package main

import (
  mailer "gopkg.in/gomail.v2"
)

func main() {
    // 1. 首先构建一个 Message 对象,也就是邮件对象
    msg := mailer.NewMessage()
    // 2. 填充 From,注意第一个字母要大写
    msg.SetHeader("From", "from_address@example.com")
    // 3. 填充 To
    msg.SetHeader("To", "to_address@example.com")
    // 4. 如果需要可以填充 cc,也就是抄送
    msg.SetHeader("Cc", "cc_address@example.com")
    // 5. 设置邮件标题
    msg.SetHeader("Subject", "Go 语言发送邮件")
    // 6. 设置要发送的邮件正文
    // 第一个参数是类型,第二个参数是内容
    // 如果是 html,第一个参数则是 `text/html` 如果是文本则是"text/plain"
    msg.SetBody("text/html", "<h3>欢迎来到简单教程</h3><p>简单教程,简单编程</p>")
    // 7. 添加附件,注意,这个附件是完整路径
    // msg.Attach("/Users/yufei/Downloads/1.jpg")
    // 到此,邮件消息构建完毕

    // 8. 创建 smtp 实例
    // 如果你的阿里云企业邮箱则是密码,否则一般情况下国内国外使用的都是授权码(例如,腾讯云企业邮箱)
    // 请注意 DialAndSend() 方法是一次性的,也就是连接邮件服务器,发送邮件,然后关闭连接。
    // dialer := mailer.NewDialer("smtp.mxhichina.com", 465, "阿里云企业邮箱账号", "阿里云企业邮箱密码")
    dialer := mailer.NewDialer("smtp.exmail.qq.com", 465, "腾讯云企业邮箱账号", "腾讯云企业邮箱授权码")
    // 9. 发送邮件,连接邮件服务器,发送完就关闭
    if err := dialer.DialAndSend(msg); err != nil {
        panic(err)
    }
}

示例2.gomail邮件消息群发

上述代码中的 DialAndSend() 方法是一次性的,也就是连接邮件服务器,发送邮件,然后关闭连接。
如果你有很多封邮件要发,那么正确的方法应该是 创建一个连接器然后不断的发,可以参考如下代码。

代码示例

package main
import (
  "gopkg.in/gomail.v2"
  "log"
)

type Address struct {
  Name, Address string
}
func main() {
    // 先连接到邮件服务器
    dialer := gomail.NewDialer("smtp.exmail.qq.com", 465, "腾讯云企业邮箱账号", "腾讯云企业邮箱授权码")

    // dialer 的 sock 通道
    sock, err := dialer.Dial()
    if err != nil {
      panic(err)
    }

    // 收件人
    list := []Address{
      Address{Name: "管理员", Address: "master@weiyigeek.top"},
      Address{Name: "测试人员", Address: "test@weiyigeek.top"},
    }

    // 然后发送多封邮件
    msg := gomail.NewMessage()
    for _, r := range list {
        msg.SetHeader("From", "from_address@example.com")
        msg.SetHeader("To", msg.FormatAddress(r.Address, r.Name))
        msg.SetHeader("Subject", "Go 语言发送邮件")
        msg.SetBody("text/html", "<h3>欢迎来到简单教程</h3><p>简单教程,简单编程</p>")
        // 关键点
        if err := gomail.Send(sock, msg); err != nil {
            log.Printf("Could not send email to %q: %v", r.Address, err)
        }
        // 千万不要忘记调用这个
        msg.Reset()
    }
}

<br/>

示例3.自定义方法封装Gomail实现发送文本、HTML以及附件

代码示例:

// 邮件对象&邮件信息结构体以及模板body缓存区
type Emailer struct {
	host, user, pass string
	port             int
	d                *gomail.Dialer
	m                *gomail.Message
	bodyBuffer       bytes.Buffer
}

var Email *Emailer

// Emailer 构造函数
func NewEmailer(host string, port int, user, pass string) *Emailer {
	Email = &Emailer{
		host: host,
		port: port,
		user: user,
		pass: pass,
		d:    gomail.NewDialer(host, port, user, pass),
		m:    gomail.NewMessage(),
	}
	return Email
}

// Setup 初始化邮件函数
func (e *Emailer) Setup() *gomail.Dialer {
	if e.d == nil {
		// 实例化gomail邮件连接对象
		emailhost := setting.Conf.Get("email.host").(string)
		emailport := setting.Conf.Get("email.port").(int)
		emailuser := fmt.Sprintf("%s", setting.Conf.Get("email.user"))
		emailpass := fmt.Sprintf("%s", setting.Conf.Get("email.pass"))

		// 若有错误就关闭连接
		var s gomail.SendCloser
		defer func() {
			if err := recover(); err != nil {
				fmt.Println("gomail.NewDialer connect failed!")
				s.Close()
				panic(err)
			}
		}()

		// 创建 smtp 实例
		e.d = gomail.NewDialer(emailhost, emailport, emailuser, emailpass)
		// 允许跳过不安全的认证
		// d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
		return e.d
	}
	return e.d
}

// MailDialAndSend 发送邮件后立即关闭连接。
func (e *Emailer) MailDialAndSend(msg *gomail.Message) (string, error) {
	if err := e.d.DialAndSend(msg); err != nil {
		return "邮件发送失败!", err
	} else {
		msg.Reset()
		return "邮件发送成功!", nil
	}
}

// SendMsg text文本以及HTML格式邮件信息发送
func (e *Emailer) SendMsg(to []string, cc, subject, body, sendtype string) (string, error) {
	// 初始化连接验证
	e.Setup()

	// 构建一个 Message 对象也就是邮件对象
	e.m = gomail.NewMessage()

	// 设置发信人,收信人、抄送
	e.m.SetHeader("From", setting.Conf.Get("email.user").(string))
	e.m.SetHeader("To", to...)
	if cc != "" {
		chaosong := strings.Split(cc, ",")
		e.m.SetAddressHeader("Cc", chaosong[0], chaosong[1])
	}

	// 设置邮件标题与正文
	if subject != "" && body != "" {
		// 邮件标题
		e.m.SetHeader("Subject", subject)
		// 判断发送邮件的类型设置对应正文
		if sendtype == "text" {
			e.m.SetBody("text/plain", body)
		} else if sendtype == "html" {
			e.m.SetBody("text/html", body)
		}
	} else {
		return "邮件 subject 或 body 字段不能为空", errors.New("Email Message The subject or body field cannot be empty")
	}

	// 发送邮件
	if err := e.d.DialAndSend(e.m); err != nil {
		return "邮件发送失败!", err
	} else {
		e.m.Reset()
		return "邮件发送成功!", err
	}
}


func (e *Emailer) SendAttachMsg(to []string, cc, subject, body, file, filename string) (string, error) {
	// 初始化连接验证
	e.Setup()

	// 构建一个 Message 对象也就是邮件对象
	e.m = gomail.NewMessage()

	// 设置发信人,收信人、抄送
	e.m.SetHeader("From", setting.Conf.Get("email.user").(string))
	e.m.SetHeader("To", to...)
	if cc != "" {
		chaosong := strings.Split(cc, ",")
		e.m.SetAddressHeader("Cc", chaosong[0], chaosong[1])
	}
	// 设置邮件标题与正文
	e.m.SetHeader("Subject", subject)
	e.m.SetBody("text/html", body)

	// 添加附件,注意附件是需要传入完整路径
	if file != "" && len(file) > 0 {
		e.m.Attach(file,
			gomail.Rename(filename),
			gomail.SetHeader(map[string][]string{
				"Content-Disposition": {
					fmt.Sprintf(`attachment; filename="%s"`, mime.QEncoding.Encode("UTF-8", filename)),
				},
			}),
		)
	}

	// 发送邮件
	if err := e.d.DialAndSend(e.m); err != nil {
		e.m.Reset()
		return "邮件发送失败!", err
	} else {
		e.m.Reset()
		return "邮件发送成功!", nil
	}
}


func main() {
  // 执行 Emailer 的构造函数
  obj := NewEmailer("smtp.exmail.qq.com", 465, "腾讯云企业邮箱账号", "腾讯云企业邮箱授权码")

  // 定义要发送的邮箱地址数组
  mailTo := []String{
    "master@weiyigeek.top",
    "weiyigeek@qq.com",
  }

  // 调用示例1: 文本信息发送
  obj.SendMsg(mailTo,"chaosong@weiyigeek.top","文本邮件示例","来自【全栈工程师修炼指南】公众号的消息","text")
  
  // 调用示例2: HTML信息发送带
  obj.EmailMsg(mailTo,"chaosong@weiyigeek.top","网页邮件示例","来自<b>【全栈工程师修炼指南】</b>公众号的消息","html")

  // 调用示例3: HTML信息发送带附件
  obj.SendAttachMsg(mailTo,"chaosong@weiyigeek.top","网页邮件示例","来自<b>【全栈工程师修炼指南】</b>公众号的消息","/app/devops/res/tmp/weiyigeek.docx","公众号文档.docx")

}

代码执行效果:

b2daa2b0a075657e2ca2809c83c9e427.png

bd0e99be74acb8f088126f4b695dba5d.png

示例4.Gomail发送HTML注册找回验证码

描述: 我们需要在上节代码文件中加入如下函数。

HTML模板文件:

<!-- template\email\TemplateVerifiy.html -->
<div style="background:#fff">
    <table width="100%" border="0" cellspacing="0" cellpadding="0">
        <thead>
        <tr>
            <td valign="middle" style="padding-left:30px;background-color:#415A94;color:#fff;padding:20px 40px;font-size: 21px;">{{.SiteName}}</td>
        </tr>
        </thead>
        <tbody>
        <tr style="padding:40px 40px 0 40px;display:table-cell">
            <td style="font-size:24px;line-height:1.5;color:#000;margin-top:40px">邮箱验证码</td>
        </tr>
        <tr>
            <td style="font-size:14px;color:#333;padding:24px 40px 0 40px">
                尊敬的 <b> {{.UserName}} </b>用户您好!
                <br>
                <br>
                您的验证码是:<b>{{.UserCode}} </b>,请在 <b>{{.UserCodeTime}} 分钟内</b>进行验证, 过期将失效!
                <br> 
                如果该验证码不为您本人申请,请无视。
            </td>
        </tr>
        <tr style="padding:40px;display:table-cell">
        </tr>
        </tbody>
    </table>
</div>
<div>
    <table width="100%" border="0" cellspacing="0" cellpadding="0">
        <tbody>
        <tr>
            <td style="padding:20px 40px;font-size:12px;color:#999;line-height:20px;background:#f7f7f7"><a href="{{.SiteAddr}}" style="font-size:14px;color:#929292" rel="noopener" target="_blank">返回 {{.SiteName}} </a></td>
        </tr>
        </tbody>
    </table>
</div>

关键函数

// TemplateVerifiy 发送注册、登录、找回密码验证码模板字符串
func (e *Emailer) TemplateVerifiy(tpl *template.Template, body ...string) string {
	e.bodyBuffer.Reset()
	tpl.Execute(&e.bodyBuffer, struct {
		SiteName     string
		UserName     string
		UserCode     string
		UserCodeTime string
		SiteAddr     string
	}{
		SiteName:     body[0],
		UserName:     body[1],
		UserCode:     body[2],
		UserCodeTime: body[3],
		SiteAddr:     body[4],
	})
	return e.bodyBuffer.String()
}

// 此处值得学习利用反射实现传入string字符串解析为对应函数以及call参数进行执行,
func (e *Emailer) reflectFunc(tpl *template.Template, tplname string, body ...string) string {
	var t Emailer
	ref := reflect.ValueOf(&t)
	refFunc := ref.MethodByName(tplname)
	fmt.Printf("Kind : %s, Type : %s\n", refFunc.Kind(), refFunc.Type())
	refVal := make([]reflect.Value, 0)
	refVal = append(refVal, reflect.ValueOf(tpl))
	for _, v := range body {
		fmt.Println(v)
		refVal = append(refVal, reflect.ValueOf(v))
	}
	fmt.Println(refVal)
	res := refFunc.Call(refVal)
	return res[0].String()

}

// 模板信息发送
func (e *Emailer) TemplateMsg(to []string, tplname, subject string, bodys ...string) (string, error) {
	// 初始化连接验证
	e.Setup()

	// 构建一个 Message 对象也就是邮件对象
	e.m = gomail.NewMessage()

	// 设置发信人,收信人、抄送
	e.m.SetHeader("From", setting.Conf.Get("email.user").(string), "全栈工程师")
	e.m.SetHeader("To", to...)
	e.m.SetHeader("Subject", subject)

	// 模板读取并返回template对象
	tpl, err := template.ParseFiles(fmt.Sprintf("./template/email/%s.html", tplname))
	if err != nil {
		return "传入的模板文件名称有误", err
	}
	body := e.reflectFunc(tpl, tplname, bodys...)
	e.m.SetBody("text/html", body)

	// 邮件发送
	res, err := e.MailDialAndSend(e.m)
	return res, err
}

执行效果:
718be75da04c8321df42ab0e035e76e5.png

示例 5.正式环境中我们一般会用管道 channel 来创建一个邮件发送服务.

package main
import (
    mailer "gopkg.in/gomail.v2"
    "log"
    "time"
)
type Address struct {
    Name, Address string
}
func main() {
  // 收件人
  list := []Address{
    Address{Name: "管理员", Address: "master@weiyigeek.top"},
    Address{Name: "测试人员", Address: "test@weiyigeek.top"},
  }
  ch := make(chan *mailer.Message)
  go func() {
      dialer := mailer.NewDialer("smtp.exmail.qq.com", 465, "腾讯云企业邮箱账号", "腾讯云企业邮箱授权码")
      var sock mailer.SendCloser
      var err error
      var lasted int64 = 0
      open := false
      for {
          select {
          case msg, ok := <-ch:
              log.Printf("%v\n", msg)
              if !ok {
                  return
              }
              if !open {
                  if sock, err = dialer.Dial(); err != nil {
                      panic(err)
                  }
                  open = true
              }
              if err := mailer.Send(sock, msg); err != nil {
                  lasted = time.Now().Unix()
                  log.Printf("发送错误:%s\n", err.Error())
              }
          // 如果 30s 没有再发送邮件则关闭
          case <-time.Tick(30 * time.Second):
              if open && time.Now().Unix()- lasted > 30 {
                  log.Printf("30 秒没有任何邮件,直接关闭")
                  if err := sock.Close(); err != nil {
                      panic(err)
                  }
                  open = false
              }
          }
      }
  }()

  // 然后利用循环遍历发送多封邮件
  for _, r := range list {
      msg := mailer.NewMessage()
      msg.SetHeader("From", "from_address@example.com")
      msg.SetHeader("To", msg.FormatAddress(r.Address, r.Name))
      msg.SetHeader("Subject", "Go 语言发送邮件")
      msg.SetBody("text/html", "<h3>欢迎来到简单教程</h3><p>简单教程,简单编程</p>")
      ch <- msg
  }
  
  // 程序休眠30秒
  time.Sleep(120 * time.Second)

  // 程序接受后关闭管道
  close(ch)
}

入坑出坑

问题1.使用Gomail时报x509:由未知颁发机构签名的证书解决办法.
描述: 如果收到此错误,则表示SMTP服务器使用的证书不是被运行 Gomail 的客户端视为有效。
解决办法: 作为快速解决方法,您可以使用绕过对服务器的证书链和主机名的验证SetTLSConfig, 但请注意这是不安全的其不应在生产中使用。

package main

import (
	"crypto/tls"

	"gopkg.in/gomail.v2"
)

func main() {
	d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
	d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
  // Send emails using d.
  ......
}

亲,文章就要看完了,不关注一下作者吗?

问题2.gomail 执行时报 could not send email 1: 550 Error: content rejected.http://mail.qq.com/zh_CN/help/content/rejectedmail.html 错误解决办法。
描述: 上述QQ邮箱返回的退信信息,QQ邮箱认为您的邮件内容涉及群发的垃圾邮件而拒绝,由于发信smtp服务器中主题或者内容包含关键敏感字已经频繁发送邮件也会触发,则报如上所示信息。
解决办法: 删除敏感关键字,重新发送即可,尽量不同时且减少推销性文字,否则将被认为是垃圾邮件。

# 建议
联系收件方将您发信地址加入白名单;
更改邮件的主题和内容,避免出现广告和推广之类的字眼;
一次发送的收件人数不要超过100人;
如果还是退信,建议联系收件方管理员核实具体的退信原因。

本文至此完毕,更多技术文章,尽情等待下篇好文!

原文地址: https://blog.weiyigeek.top/2023/5-18-739.html

如果此篇文章对你有帮助,请你将它分享给更多的人! 

6f14f36a86bf13b2d75c7890bf934c80.gif

1b8f19a3d624c344509f463c64136bb8.png 学习书籍推荐 往期发布文章 ad8ee82577caca37b4f842fb3aabcc1d.png

公众号回复【0008】获取【Ubuntu22.04安装与加固建脚本】

公众号回复【10001】获取【WinServer安全加固脚本】

公众号回复【1000】获取【PowerShell操作FTP脚本】

公众号回复【0015】获取【Jenkins学习之路汇总】

 热文推荐  

欢迎长按(扫描)二维码 取更多渠道哟

欢迎关注 【全栈工程师修炼指南】(^U^)ノ~YO

== 全栈工程师修炼指南 ==

微信沟通交流: weiyigeeker 

关注回复【学习交流群】即可加入【安全运维沟通交流小群

温馨提示: 由于作者水平有限,本章错漏缺点在所难免,希望读者批评指正,若有问题或建议请在文章末尾留下您宝贵的经验知识,或联系邮箱地址

master@weiyigeek.top 或 关注公众号 [全栈工程师修炼指南] 留言。

[全栈工程师修炼指南]  关注 企业运维实践、网络安全、系统运维、应用开发、物联网实战、全栈文章,尽在博客站点,谢谢支持!

点个【 赞 + 在 】看吧!

 点击【"阅读原文"】获取更多有趣的知识!   

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈工程师修炼指南

原创不易,赞赏鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值