net/smtp包
简介
协议简介
SMTP协议发送邮件流指令程示意图:
包简介
SMTP包是实现SMTP(Simple Mail Transfer Protocol)协议的一个包,遵守了RFC5321,同时相关拓展也准守了相关RFC文档
函数
SendMail
smtp包封装了一个发送邮件的方法SendMail,调用这个方法可以直接发送邮件,这个方法里面按照smtp协议的请求步骤进行了封装,所以调用者不必了解smtp协议的具体发送步骤就可以直接发送邮件。SendMail函数和net/smtp软件包不支持DKIM签名,MIME附件(请参阅mime/multipart软件包)或其他邮件功能。更高级别的程序包存在于标准库之外
func SendMail(addr string, a Auth, from string, to []string, msg []byte) error
查看示例
package main
import (
"log"
"net/smtp"
)
func main() {
// 设置PlainAuth验证的账号和smtp服务器信息
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com")
// 设置发送人,数组里的每个邮件地址都会进行RCPT调用
to := []string{"recipient@example.net"}
// 编写发送的消息
msg := []byte("To: recipient@example.net\r\n" +
"Subject: discount Gophers!\r\n" +
"\r\n" +
"This is the email body.\r\n")
// 调用函数发送邮件
err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
if err != nil {
log.Fatal(err)
}
}
上面的
msg里的内容需准守smtp协议的内容规范,To代码接收邮件的人,Subject代表邮件的主题。内容部分以符号.结尾。
参考RFC 822 Tips:发送“密件抄送”消息的方法是,在to参数中包括电子邮件地址,但在msg标头中不包括该电子邮件地址。也就是说只进行RCPT调用,而不在消息中注明该地址。
Auth接口
Auth接口有两个方法,Start和Next。
-
Start方法表示开始开始对服务器进行身份验证。它返回认证协议的名称,以及可选地包含在发送到服务器的初始AUTH消息中的数据。它可以返回proto ==“”来表示跳过身份验证。如果返回的error不为nil,则SMTP客户端会终止身份验证尝试并关闭连接。 -
Next方法表示接下来继续进行身份验证,服务器发送formServer数据,当more字段为true时,表示希望接收响应数据,此时数据以[]byte数据格式返回。当more字段为false时,返回nil。如果返回的error不为nil,则SMTP客户端会终止身份验证尝试并关闭连接。
查看Auth接口源码
type Auth interface {
Start(server *ServerInfo) (proto string, toServer []byte, err error)
Next(fromServer []byte, more bool) (toServer []byte, err error)
}
CRAMMD5Auth
CRAMMD5Auth返回实现RFC 2195中定义的CRAM-MD5身份验证机制的Auth 。
CRAMMD5Auth实现了Auth接口的两个方法
点击查看CRAMMD5Auth源码
type cramMD5Auth struct {
username, secret string
}
// 提供外部调用的方法,传入username和secret,返回的Auth使用给定的用户名和密码使用质询-响应机制对服务器进行身份验证。
func CRAMMD5Auth(username, secret string) Auth {
return &cramMD5Auth{username, secret}
}
// 实现Auth的Start方法,返回协议名`CRAM-MD5`
func (a *cramMD5Auth) Start(server *ServerInfo) (string, []byte, error) {
return "CRAM-MD5", nil, nil
}
// 实现Auth的Next方法,进行加密
func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
d := hmac.New(md5.New, []byte(a.secret))
d.Write(fromServer)
s := make([]byte, 0, d.Size())
return []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil
}
return nil, nil
}
PlainAuth
PlainAuth返回实现RFC 4616定义的Auth。
仅当连接使用
TLS或连接到本地主机时,PlainAuth才会发送凭据。否则,身份验证将失败并显示错误,而不发送凭据。
PlainAuth实现了Auth接口的两个方法
查看PlainAuth源码
type plainAuth struct {
identity, username, password string
host string
}
// 暴露给外部调用的方法,一般identity为空字符串。传入账号名,密码,和smtp服务器地址
func PlainAuth(identity, username, password, host string) Auth {
return &plainAuth{identity, username, password, host}
}
// 判断是否是本地地址
func isLocalhost(name string) bool {
return name == "localhost" || name == "127.0.0.1" || name == "::1"
}
// 实现Auth接口的Start方法,返回PLAIN协议和发送到服务器的初始AUTH消息中的数据
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection")
}
if server.Name != a.host {
return "", nil, errors.New("wrong host name")
}
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
return "PLAIN", resp, nil
}
func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
return nil, errors.New("unexpected server challenge")
}
return nil, nil
}
Client结构体
查看Client结构体
type Client struct {
// 客户端使用的textproto.Conn。它被导出以允许客户端添加扩展。
Text *textproto.Conn
// 保留一个对连接的引用,以便于以后创建TLS链接
conn net.Conn
// 客户端是否正在使用TLS
tls bool
serverName string
// 支持的拓展的Map
ext map[string]string
// 支持auth的机制
auth []string
localName string
// 是否已经调用 HELLO/EHLO
didHello bool
// HELO响应的错误
helloError error
}
Dial
此函数将会调用[net.Dial函数,与传入的SMTP地址(地址需包含端口,例如:smtp.qq.com:25)建立TCP链接,然后通过调用NewClient函数返回一个SMTP客户端
查看Dial函数源码
func Dial(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return NewClient(conn, host)
}
NewClient
此函可以使用现有与SMTP服务器建立的TCP连接,返回一个新的SMTP客户端
查看NewClient函数源码
func NewClient(conn net.Conn, host string) (*Client, error) {
text := textproto.NewConn(conn)
_, _, err := text.ReadResponse(220)
if err != nil {
text.Close()
return nil, err
}
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
_, c.tls = conn.(*tls.Conn)
return c, nil
}
(*Client) Auth
Auth使用提供的身份验证机制对客户端进行身份验证,当验证失败会关闭客户端连接,只有支持AUTH拓展的SMTP服务器才能使用此功能。
查看(*Client) Auth源码
func (c *Client) Auth(a Auth) error {
if err := c.hello(); err != nil {
return err
}
encoding := base64.StdEncoding
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
if err != nil {
c.Quit()
return err
}
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))
for err == nil {
var msg []byte
switch code {
case 334:
msg, err = encoding.DecodeString(msg64)
case 235:
// the last message isn't base64 because it isn't a challenge
msg = []byte(msg64)
default:
err = &textproto.Error{Code: code, Msg: msg64}
}
if err == nil {
resp, err = a.Next(msg, code == 334)
}
if err != nil {
// abort the AUTH
c.cmd(501, "*")
c.Quit()
break
}
if resp == nil {
break
}
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err = c.cmd(0, string(resp64))
}
return err
}
(*Client) Close
此方法用于关闭客户端与SMTP服务器的连接
查看(*Client) Close源码
func (d *dataCloser) Close() error {
d.WriteCloser.Close()
_, _, err := d.c.Text.ReadResponse(250)
return err
}
(*Client) Data
此方法向SMTP服务器发出SMTP的DATA命令,并返回可用于写入邮件头和正文的写入器。调用者应在调用任何其他方法之前关闭写入器。在调用Data之前,必须先进行一次或多次对Rcpt的调用。
返回的io.WriteCloser写入数据时应遵守RFC 822规范
查看(*Client) Data源码
func (c *Client) Data() (io.WriteCloser, error) {
_, _, err := c.cmd(354, "DATA")
if err != nil {
return nil, err
}
return &dataCloser{c, c.Text.DotWriter()}, nil
}
(*Client) Extension
此方法用于查询SMTP服务器是否支持传入的拓展(传入的拓展名不区分大小写),当支持拓展时会返回true和对应SMTP服务器该key下的value值字符串,如果不支持就会返回false和空字符串。
例如:
// 查询QQ邮箱的`SMTP`服务器是否支持`AUTH`拓展
client,_:=smtp.Dial("smtp.qq.com:25")
b,p:=client.Extension("auth")
log.Println(b)
log.Println(p)
// 输出
true
LOGIN PLAIN
查看(*Client) Extension源码
func (c *Client) Extension(ext string) (bool, string) {
if err := c.hello(); err != nil {
return false, ""
}
if c.ext == nil {
return false, ""
}
ext = strings.ToUpper(ext)
param, ok := c.ext[ext]
return ok, param
}
(*Client) Hello
此方法会向SMTP服务器发送HELO/EHLO指令,需要在调用任何方法前调用它
查看(*Client) Hello源码
func (c *Client) hello() error {
if !c.didHello {
c.didHello = true
err := c.ehlo()
if err != nil {
c.helloError = c.helo()
}
}
return c.helloError
}
(*Client) Mail
Mail使用提供的电子邮件地址向服务器发出MAIL命令。 如果服务器支持8BITMIME扩展名,则Mail将添加BODY = 8BITMIME参数。 如果服务器支持SMTPUTF8扩展名,则Mail将添加SMTPUTF8参数。 这将启动邮件事务,然后进行一个或多个Rcpt调用。
查看(*Client) Mail源码
func (c *Client) Mail(from string) error {
if err := validateLine(from); err != nil {
return err
}
if err := c.hello(); err != nil {
return err
}
cmdStr := "MAIL FROM:<%s>"
if c.ext != nil {
if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME"
}
if _, ok := c.ext["SMTPUTF8"]; ok {
cmdStr += " SMTPUTF8"
}
}
_, _, err := c.cmd(250, cmdStr, from)
return err
}
(*Client) Noop
此方法会向SMTP服务器发送NOOP指令,其他不做任何操作,只是检查与SMTP服务器的连接是否正常
查看(*Client) Noop源码
func (c *Client) Noop() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "NOOP")
return err
}
(*Client) Quit
向SMTP服务器器发送QUIT指令,关闭客户端与SMTP服务器的连接
查看(*Client) Quit源码
func (c *Client) Quit() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(221, "QUIT")
if err != nil {
return err
}
return c.Text.Close()
}
(*Client) Rcpt
向SMTP服务器对要接收的邮件地址发送RCPT指令
需先调用
查看(*Client) Rcpt源码
func (c *Client) Rcpt(to string) error {
if err := validateLine(to); err != nil {
return err
}
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
return err
}
(*Client) Reset
将RSET命令发送到SMTP服务器,从而中止当前的邮件事务。
查看(*Client) Reset源码
func (c *Client) Reset() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "RSET")
return err
}
(*Client) StartTLS
发送STARTTLS命令并加密接下来所有的通信。仅发布STARTTLS扩展的SMTP服务器支持此功能。
示例:
// 开启QQ邮箱的`TLS`
client,err:=smtp.Dial("smtp.qq.com:25")
config := &tls.Config{ServerName: "smtp.qq.com"}
client.StartTLS(config)
查看(*Client) Reset源码
func (c *Client) StartTLS(config *tls.Config) error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(220, "STARTTLS")
if err != nil {
return err
}
c.conn = tls.Client(c.conn, config)
c.Text = textproto.NewConn(c.conn)
c.tls = true
return c.ehlo()
}
(*Client) TLSConnectionState
检查客户端的TLS连接状态,如果TLS没有连接,则第二个参数返回false。
查看(*Client) Reset源码
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
tc, ok := c.conn.(*tls.Conn)
if !ok {
return
}
return tc.ConnectionState(), true
}
(*Client) Verify
检查邮件地址是否有效,返回error为nil时则证明地址有效。
一般不使用此方法
查看(*Client) Reset源码
func (c *Client) Verify(addr string) error {
if err := validateLine(addr); err != nil {
return err
}
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "VRFY %s", addr)
return err
}