欢迎关注「全栈工程师修炼指南」公众号
点击 👇 下方卡片 即可关注我哟!
设为「星标⭐」每天带你 基础入门 到 进阶实践 再到 放弃学习!
“ 花开堪折直须折,莫待无花空折枝。 ”
作者主页:[ https://www.weiyigeek.top ]
博客:[ https://blog.weiyigeek.top ]
作者安全运维学习答疑交流群:请关注公众号回复【学习交流群】
文章目录:
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")
}
代码执行效果:
示例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
}
执行效果:
示例 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
如果此篇文章对你有帮助,请你将它分享给更多的人!
学习书籍推荐 往期发布文章
公众号回复【0008】获取【Ubuntu22.04安装与加固建脚本】
公众号回复【10001】获取【WinServer安全加固脚本】
公众号回复【1000】获取【PowerShell操作FTP脚本】
公众号回复【0015】获取【Jenkins学习之路汇总】
热文推荐
容灾恢复 | 记一次K8S集群中etcd数据快照的备份恢复实践
开发基础 | Golang语言的RESTfulAPI接口设计规范快速入门
企业实践 | 如何从VMWare ESXi Shell中挂载以及拷贝NTFS或者FAT32分区格式的USB闪存驱动器
网安等保-国产Linux操作系统银河麒麟KylinOS-V10SP3常规配置、系统优化与安全加固基线实践文档
硬件玩物 | 闲置物理主机安装群辉NAS-DSM-7.x系统实践试用初体验(保姆篇)
欢迎长按(扫描)二维码 获取更多渠道哟!
欢迎关注 【全栈工程师修炼指南】(^U^)ノ~YO
== 全栈工程师修炼指南 ==
微信沟通交流: weiyigeeker
关注回复【学习交流群】即可加入【安全运维沟通交流小群】
温馨提示: 由于作者水平有限,本章错漏缺点在所难免,希望读者批评指正,若有问题或建议请在文章末尾留下您宝贵的经验知识,或联系邮箱地址
master@weiyigeek.top 或 关注公众号 [全栈工程师修炼指南] 留言。
[全栈工程师修炼指南] 关注 企业运维实践、网络安全、系统运维、应用开发、物联网实战、全栈文章,尽在博客站点,谢谢支持!
点个【 赞 + 在 】看吧!
点击【"阅读原文"】获取更多有趣的知识!