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源码

相关文章
|
23天前
|
人工智能 物联网 开发工具
.NET技术:多元语言、丰富库与跨平台能力引领软件开发新纪元。
`【7月更文挑战第4天】.NET技术:多元语言、丰富库与跨平台能力引领软件开发新纪元。从企业应用、云服务到游戏开发,其角色日益凸显。随着微软的持续创新与社区合作,未来.NET将在物联网、AI等领域拓宽应用,开发者应把握趋势,共创未来。`
20 0
|
2月前
|
C#
一个库帮你轻松的创建漂亮的.NET控制台应用程序
一个库帮你轻松的创建漂亮的.NET控制台应用程序
|
14天前
|
存储 对象存储 Python
`openpyxl`是一个用于读写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。它不需要Microsoft Excel,也不需要.NET或COM组件。
`openpyxl`是一个用于读写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。它不需要Microsoft Excel,也不需要.NET或COM组件。
|
1月前
|
NoSQL Go Redis
如何使用 Go 和 `go-redis/redis` 库连接到 Redis 并执行一些基本操作
如何使用 Go 和 `go-redis/redis` 库连接到 Redis 并执行一些基本操作
20 1
|
21天前
|
Linux API C#
.NET开源、跨平台、使用简单的面部识别库
.NET开源、跨平台、使用简单的面部识别库
|
1月前
|
NoSQL 安全 Go
Go 语言 mongox 库:简化操作、安全、高效、可扩展、BSON 构建
go mongox 是一个基于泛型的库,扩展了 MongoDB 的官方库。通过泛型技术,它实现了结构体与 MongoDB 集合的绑定,旨在提供类型安全和简化的数据操作。 go mongox 还引入链式调用,让文档操作更流畅,并且提供了丰富的 BSON 构建器和内置函数,简化了 BSON 数据的构建。 此外,它还支持插件化编程和内置多种钩子函数,为数据库操作前后的自定义逻辑提供灵活性,增强了应用的可扩展性和可维护性。
68 6
|
29天前
|
JSON 开发框架 API
【推荐100个unity插件之20】一个强大的JSON处理库——Newtonsoft.Json(也称为Json.NET)
【推荐100个unity插件之20】一个强大的JSON处理库——Newtonsoft.Json(也称为Json.NET)
51 0
|
1月前
|
NoSQL 大数据 Redis
分享5款.NET开源免费的Redis客户端组件库
分享5款.NET开源免费的Redis客户端组件库
|
2月前
|
Java 测试技术 Go
使用go的内置运行时库调试和优化程序
【5月更文挑战第18天】在本文中,作者探讨了如何为运行时程序添加剖析支持以优化性能。他们面临的问题是一个程序需要找出平方根为整数且逆序平方等于其逆序的数字。他们首先展示了原始代码,然后使用`runtime`库进行优化,将计算和调用功能分离,同时记录CPU和内存使用情况。
42 4
|
2月前
|
JSON Java Go
使用go语言中的内置库调试性能
【5月更文挑战第21天】本文介绍Go 语言提供了内置的 expvar 模块来输出度量数据,帮助定位性能瓶颈。与 pprof 不同,expvar 专注于应用的宏观状态,通过 HTTP 接口 `/debug/vars` 提供标准的 JSON 格式数据,包括自定义度量和内存统计等。通过 expvar,开发者可以轻松监控应用状态,如消息处理速率、内存使用等,而无需像 C++ 或 Java 那样手动实现。
36 0
使用go语言中的内置库调试性能