简介
程序中经常需要发邮件,比如用户完成一笔交易之后会发送一封通知邮件给用户,用户可以将邮件作为交易的凭证,还有邮件用于通知性的业务,比如通知管理员系统异常等
Golang中可以通过开源库email来发送邮件,email是一个易用的,健壮的,灵活的Golang库,目标是提供更加人性化的email接口
注意:当前email库不支持STARTTLS认证的邮箱(2021/11/06),如outlook,可能后续会增加此特性(作者已经表示准备Merge相关的PR)
若想要在项目中实际使用的话建议使用功能较为完善的go-gomail/gomail
电子邮件
协议
在使用email库之前,先需要了解几个电子邮件协议:
- SMTP[1]:简单邮件传输协议(Simple Mail Transfer Protocol),它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。SMTP 服务器就是遵循 SMTP 协议的发送邮件服务器
 - POP3[2]:邮局协议(Post Office Protocol),POP3是其第三个版本,它规定怎样将个人计算机连接到Internet的邮件服务器和下载电子邮件的电子协议。它是因特网电子邮件的第一个离线协议标准,POP3允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时删除保存在邮件服务器上的邮件,而POP3服务器则是遵循POP3协议的接收邮件服务器,用来接收电子邮件的
 - IMAP[3]:交互式邮件存取协议(Internet Mail Access Protocol),即交互式邮件存取协议,它是跟POP3类似邮件访问标准协议之一。不同的是,开启了IMAP后,您在电子邮件客户端收取的邮件仍然保留在服务器上,同时在客户端上的操作都会反馈到服务器上,如:删除邮件,标记已读等,服务器上的邮件也会做相应的动作。所以无论从浏览器登录邮箱或者客户端软件登录邮箱,看到的邮件以及状态都是一致的
 
系统组成
电子邮件系统仅有协议是无法单独运作的,电子邮件系统主要有三个组成构件:
- 邮件用户代理:MUA(Mail User Agent),为用户提供一种可对邮件进行编辑、阅读、发送、存储及管理的工具
 - 邮件服务器
 - 邮件传输协议
 
其中的邮件服务器和邮件传输协议并称为MTA(Mail Transfer Agent,邮件传输代理),邮件服务器至少需要两种协议:用于发送邮件(SMTP)和接收邮件(POP3/IMAP)
发送流程
假设Alice@163.com给Bob@sina.com发送一封电子邮件,电子邮件不是直接发送到Bob的电脑上的,流程如下:
- Alice在邮箱客户端(MUA)编写并发送邮件,此时邮件被发送到163的邮箱服务器(MTA)
 - 163的邮箱服务器将邮件发送值sina的邮箱服务器(MTA)
 - sina的邮箱服务器(MTA)会将邮件发送到邮件投递代理(MDA,Mail Delivery Agent)中,邮件会存储在服务器上
 - Bob通过邮件客户端(MUA)从MDA中获取邮件
 
快速开始
这里以Gmail为例(需要暂时关闭安全访问,记得测试后开启):
// go-daily-lib-note/email/quick-start/main.go
package main
import (
	"log"
	"net/smtp"
	"github.com/jordan-wright/email"
)
func main() {
	e := email.NewEmail()
	e.From = "sender@gmail.com"
	e.To = []string{"receiver@qq.com"}
	e.Subject = "Awesome Email"
	e.Text = []byte("Text Body is, of course, supported!")
	err := e.Send("smtp.gmail.com:587", smtp.PlainAuth("", "sender@gmail.com", "password", "smtp.gmail.com"))
	if err != nil {
		log.Fatal("send email failed ", err)
	}
}
登录收件邮箱,可以看到刚刚发送的邮件:

由于Gmail是支持PlainAuth的,可以使用明文的user/pass验证;但是若邮箱服务不支持此验证方式将会报错:
504 5.7.4 Unrecognized authentication type
可以使用telnet连接邮件服务器,查看其支持的验证方式(以outlook为例) :
telnet smtp-mail.outlook.com 25
Trying 52.98.40.66...
Connected to smtp-mail.outlook.com.
Escape character is '^]'.
220 HKAPR*****.outlook.office365.com Microsoft ESMTP MAIL Service ready at Sat, 6 Nov 2021 12:42:51 +0000
helo
250 HKAPR*****.outlook.office365.com Hello [183.12.236.224]
ehlo
250-HKAPR04CA0010.outlook.office365.com Hello [183.12.236.224]
250-SIZE 157286400
250-PIPELINING
250-DSN
250-ENHANCEDSTATUSCODES
250-STARTTLS
250-8BITMIME
250-BINARYMIME
250-CHUNKING
250 SMTPUTF8
可以看到,此处outlook是不支持PlainAuth的
抄送
发送邮件有时需要抄送(CC, Carbon Copy)和秘密抄送(BCC, Blind Carbon Copy)
修改上面的例子,加上抄送和秘密抄送人:
// go-daily-lib-note/email/quick-start/main.go
// ...
e.Cc = []string{"test1@126.com", "test2@126.com"}
e.Bcc = []string{"secret@126.com"}
// ...
HTML格式
邮件正文可以使用HTML格式,修改上例:
// go-daily-lib-note/email/quick-start/main.go
// ...
e.HTML = []byte(`
<h1>Awesome Email</h1>
<p>Text Body is, of course, supported!</p>
`)
// ...
附件
使用AttachFile方法可以添加附件,修改上例:
// go-daily-lib-note/email/quick-start/main.go
// ...
file, err := os.OpenFile("text.txt", os.O_CREATE|os.O_WRONLY, 0666)
	defer file.Close()
	if err != nil {
		log.Fatal("create file error ")
	}
	file.WriteString("This is a mail attachment")
	e.AttachFile("text.txt")
// ...
连接池
每次调用Send方法时都会与SMTP服务器建立连接,如果发送频繁的话会有性能问题,email提供了连接池,可以服用连接
// 
package main
import (
	"fmt"
	"log"
	"net/smtp"
	"os"
	"strconv"
	"sync"
	"time"
	"github.com/jordan-wright/email"
)
func main() {
	ch := make(chan *email.Email, 10)
	pool, err := email.NewPool("smtp.gmail.com:587", 4, smtp.PlainAuth("", "xxx@gmail.com", "pass", "smtp.gmail.com"))
	if err != nil {
		log.Fatal("create pool failed:", err)
	}
	var wg sync.WaitGroup
	wg.Add(4)
	for i := 0; i < 4; i++ {
		go func() {
			defer wg.Done()
			for e := range ch {
				err := pool.Send(e, 10*time.Second)
				if err != nil {
					fmt.Fprintf(os.Stderr, "email: %v sent error:%v\n", e, err)
				}
			}
		}()
	}
	for i := 0; i < 10; i++ {
		e := email.NewEmail()
		e.From = "dreamjzwork@outlook.com"
		e.To = []string{"xxx@qq.com"}
		e.Cc = []string{"xxx@qq.com"}
		e.Bcc = []string{"xxx@gmail.com"}
		e.Subject = "Awesome Email:" + strconv.Itoa(i+1)
		e.Text = []byte("Text Body is, of course, supported!")
		e.HTML = []byte(`
<h1>Awesome Email</h1>
<p>Text Body is, of course, supported!</p>
`)
		ch <- e
	}
	close(ch)
	wg.Wait()
}
上例中创建4个goroutine发送一共10封邮件,为了等待邮件都完成或者失败,使用了Sync.WaitGroup
参考
Go 每日一库之 email darjun blog
jordan-wright/email github repo
电子邮件 维基百科
电子邮件 liaoxuefeng
什么是POP3、SMTP和IMAP? 163email help
How to send email using Outlook's SMTP servers? stackoverflow
简单邮件传输协议(英语:Simple Mail Transfer Protocol,缩写:SMTP)是一个在互联网上传输电子邮件的标准 ↩︎
邮局协议(英语:Post Office Protocol,缩写:POP)是TCP/IP协议族中的一员,由1996年5月发行之 RFC 1939 首次定义。此协议主要用于支持使用客户端远程管理在服务器上的电子邮件。最新版本为POP3,全名“Post Office Protocol - Version 3”,而提供了SSL加密的POP3协议被称为POP3S ↩︎
因特网信息访问协议(英语:Internet Message Access Protocol,缩写:IMAP;以前称作交互邮件访问协议)是一个应用层协议,用来从本地邮件客户端(如Microsoft Outlook、Outlook Express、Foxmail、Mozilla Thunderbird)访问远程服务器上的邮件 ↩︎
