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 822Tips:发送“密件抄送”消息的方法是,在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源码