go标准库net/smtp|Go主题月

1,413 阅读8分钟

net/smtp包

简介

协议简介

SMTP协议发送邮件流指令程示意图:

包简介

SMTP包是实现SMTP(Simple Mail Transfer Protocol)协议的一个包,遵守了RFC5321,同时相关拓展也准守了相关RFC文档

8BITMIME RFC 1652
AUTH RFC 2554
STARTTLS RFC 3207

函数

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接口有两个方法,StartNext

  • 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服务器发出SMTPDATA命令,并返回可用于写入邮件头和正文的写入器。调用者应在调用任何其他方法之前关闭写入器。在调用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指令

需先调用Mail方法,然后再调用此方法

查看(*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

检查邮件地址是否有效,返回errornil时则证明地址有效。

一般不使用此方法

查看(*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
}