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

简介: net/smtp包

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

相关文章
|
19天前
|
测试技术 Go 开发者
go-carbon v2.3.8 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
16 0
|
1月前
|
Cloud Native 安全 Java
云原生系列Go语言篇-标准库Part 2
使用Go进行开发的最大优势之一是其标准库。与Python类似,Go也采取了“内置电池”的理念,提供了构建应用程序所需的许多工具。由于Go是一种相对较新的语言,它附带了一个专注于现代编程环境中遇到的问题的库。
20 0
|
1月前
|
存储 Go
Go 浅析主流日志库:从设计层学习如何集成日志轮转与切割功能
本文将探讨几个热门的 go 日志库如 logrus、zap 和官网的 slog,我将分析这些库的的关键设计元素,探讨它们是如何支持日志轮转与切割功能的配置。
41 0
Go 浅析主流日志库:从设计层学习如何集成日志轮转与切割功能
|
2月前
|
中间件 Go 开发者
Go net http包
Go net http包
31 0
|
1月前
|
JSON Cloud Native Java
云原生系列Go语言篇-标准库Part 2
REST API将JSON奉为服务之通信的标准方式,Go 的标准库内置对Go 数据类型与 JSON 之间进行转换的支持。marshaling一词表示从 Go 数据类型转为另一种编码,而unmarshaling表示转换为 Go 数据类型。
23 0
|
5天前
|
Go 索引
Go 1.22 slices 库的更新:高效拼接、零化处理和越界插入优化
本文详细介绍了 Go 1.22 版本中 slices 库的更新内容,总结起来有三个方面:新增了 Concat 函数、对部分函数新增了零化处理的逻辑和对 Insert 函数进行了越界插入优化
39 1
Go 1.22 slices 库的更新:高效拼接、零化处理和越界插入优化
|
25天前
|
Go Unix 开发者
Go语言time库,时间和日期相关的操作方法
Go语言time库,时间和日期相关的操作方法
35 0
Go语言time库,时间和日期相关的操作方法
|
1月前
|
Go 开发者
Go语言插件开发:Pingo库实践
Go语言插件开发:Pingo库实践
13 0
|
1月前
|
编译器 Go
go语言中的math库
go语言中的math库
22 0
|
1月前
|
Unix Serverless 编译器
go语言time库
go语言time库
16 0

相关产品

  • 云迁移中心