go发送smtp邮件时的踩坑记录——auth login、x509: cannot validate certificate for错误 - Go语言中文社区

go发送smtp邮件时的踩坑记录——auth login、x509: cannot validate certificate for错误


最近在用go写一个小工具,一个小功能是用smtp发邮件,用公司内网的邮箱服务器实现踩了不少坑

想知道x509: cannot validate certificate for解决的直接看2.2.1,想知道auth login怎么实现看2.2.2

1 smtp协议

基础知识,回顾一下smtp协议的基本使用

1.1 命令行通过smtp协议发邮件

smtp协议网上资料很多,这里用最简单的方法过一遍,用的是qq邮箱

qq邮箱在使用smtp协议的时候,用的不是qq密码,而是一个叫授权码的东西,我们去qq邮箱设置——账户里找到生成授权码

他会让你用密保手机发短信到某个号码,照做即可获得一个16位字母的授权码,保存好

去一个在线加密base64的网站,我用的是这个在线加密解密

把用来发邮件的qq邮箱账号和授权码转成base64编码

现在打开命令行,连接qq的smtp服务器和端口,qq的是smtp.qq.com:25

telnet smtp.qq.com 25

要和他打个招呼,后面跟着的不一定要是smtp,我不是很清楚这个有什么区别,我试着是什么都行

helo smtp

接下来就是验证你的身份,我们实验auth login法

auth login

分两行,填入刚才转换成base64的账号和授权码,这里也可以把账号和auth login放在一行写,下一行再写密码

响应235 Authentication successful,表示登陆成功

现在开始配置好发件人和收件人

mail from:<你的发件邮箱>
rcpt to:<接收邮箱>

输入data,开始写邮件内容,写完后一个.表示邮件结束,返回250 Ok: queued as,邮件就发出去了

data
subject:填写邮件主题
<空一行>
填写邮件内容
...
邮件内容
.

1.2 smtp auth方式

之前用的是auth login方式,smtp还有很多其他方式,可参考这篇文章 SMTP(Login,Plain,CRAM-MD5)验证

用ehlo来代替helo命令,就可以查询这个邮件服务器支持的auth方式

我在qq邮箱和我公司邮件服务器上尝试ehlo,得到的返回如下

 

所以qq支持auth login和plain两种方式,我公司的邮件服务器只支持auth login,plain的格式是<NULL>账号<NULL>密码

 

2 如何用go发出一封auth login的邮件

2.1 官方是怎么说的

https://golang.org/pkg/net/smtp/#example_PlainAuth

官方godoc给出了一个plain验证方式的发邮件代码

package main

import (
	"log"
	"net/smtp"
)

func main() {
	// Set up authentication information.
	auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com")

	// Connect to the server, authenticate, set the sender and recipient,
	// and send the email all in one step.
	to := []string{"recipient@example.net"}
	msg := []byte("To: recipient@example.netrn" +
		"Subject: discount Gophers!rn" +
		"rn" +
		"This is the email body.rn")
	err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
	if err != nil {
		log.Fatal(err)
	}
}

把上面的收发件人邮箱改好,邮箱服务器的hostname、端口改好,我用的qq邮箱,如果你用别的邮箱,smtp的端口号也查一下,不一定是25

密码记得要写授权码

运行之后邮件就发出去了

看一下内部代码,smtp包里这个SendMail函数,注释是我自己写的,大部分和之前telnet走的流程一致

/*
	addr:	邮件 smtp 服务器地址
	a:			验证对象
	from:	发件箱
	to:		收件人邮箱列表
	msg:	发送的邮件信息
 */
func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
	// 检测收发件邮箱地址是否有回车和换行
	if err := validateLine(from); err != nil {
		return err
	}
	for _, recp := range to {
		if err := validateLine(recp); err != nil {
			return err
		}
	}

	// 和邮箱服务器建立 tcp 连接
	c, err := Dial(addr)
	if err != nil {
		return err
	}
	defer c.Close()

	// 发送helo信息
	if err = c.hello(); err != nil {
		return err
	}

	// 如果邮箱服务器支持 ssl/tls 加密
	if ok, _ := c.Extension("STARTTLS"); ok {
		config := &tls.Config{ServerName: c.serverName}  // tls 配置
		// 测试安全连接
		if testHookStartTLS != nil {
			testHookStartTLS(config)
		}
		// 开始 tls 连接
		if err = c.StartTLS(config); err != nil {
			return err
		}
	}

	// 验证
	if a != nil && c.ext != nil {
		// 若邮箱服务器不支持 auth,报错
		if _, ok := c.ext["AUTH"]; !ok {
			return errors.New("smtp: server doesn't support AUTH")
		}
		// 验证
		if err = c.Auth(a); err != nil {
			return err
		}
	}

	// 填写发件邮箱
	if err = c.Mail(from); err != nil {
		return err
	}

	// 填写收件邮箱
	for _, addr := range to {
		if err = c.Rcpt(addr); err != nil {
			return err
		}
	}

	// 邮件正文
	w, err := c.Data()
	if err != nil {
		return err
	}
	_, err = w.Write(msg)
	if err != nil {
		return err
	}
	err = w.Close()
	if err != nil {
		return err
	}
	return c.Quit()
}

2.2 然而换成我们公司的邮箱服务器就报错

2.2.1 证书错误

换上我们公司的邮箱服务器,报错

x509: cannot validate certificate for 10.141.72.4 because it doesn't contain any IP SANs

这篇文章说这个问题和证书有关,我猜测我们公司的邮箱服务器不能提供证书,所以报错,https://blog.csdn.net/zsd498537806/article/details/79290732

方法就是要修改代码,配置tls连接为跳过证书验证,我直接把smtp包复制了一份,命名为mySmtp/smtp,进行修改,修改注释的那一行就可以,增加InsecureSkipVerify为true的tls配置

func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
	if err := validateLine(from); err != nil {
		return err
	}
	for _, recp := range to {
		if err := validateLine(recp); err != nil {
			return err
		}
	}
	c, err := Dial(addr)
	if err != nil {
		return err
	}
	defer c.Close()
	if err = c.hello(); err != nil {
		return err
	}
	if ok, _ := c.Extension("STARTTLS"); ok {
		// 跳过证书验证
		config := &tls.Config{ServerName: c.serverName, InsecureSkipVerify: true}
		if testHookStartTLS != nil {
			testHookStartTLS(config)
		}
		if err = c.StartTLS(config); err != nil {
			return err
		}
	}
	if a != nil && c.ext != nil {
		if _, ok := c.ext["AUTH"]; !ok {
			return errors.New("smtp: server doesn't support AUTH")
		}
		if err = c.Auth(a); err != nil {
			return err
		}
	}
	if err = c.Mail(from); err != nil {
		return err
	}
	for _, addr := range to {
		if err = c.Rcpt(addr); err != nil {
			return err
		}
	}
	w, err := c.Data()
	if err != nil {
		return err
	}
	_, err = w.Write(msg)
	if err != nil {
		return err
	}
	err = w.Close()
	if err != nil {
		return err
	}
	return c.Quit()
}

另外原来smtp包里还有auth.go,这个文件也要一并复制到mySmtp包里

现在main函数里面smtp的调用都变成我们mySmtp包,运行之后

504 this command is not implemented

2.2.2 auth login

前面说过,smtp的验证方式有auth login和plain等,go给出的代码用的是plain方式验证,而我们公司的服务器只支持auth login

就是说我们还要修改一下auth部分的代码,看一下原来代码是如何auth的

/*
	身份验证
 */
func (c *Client) Auth(a Auth) error {
	// 发送 ehlo
	if err := c.hello(); err != nil {
		return err
	}
	encoding := base64.StdEncoding

	// 获取验证所需信息,mech 用于验证的命令,resp 是验证的账号、密码等信息
	mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
	if err != nil {
		c.Quit()
		return err
	}

	// base64 编码
	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 {
		// 返回码 334,表示期待用户继续输入信息
		case 334:
			msg, err = encoding.DecodeString(msg64)
		// 返回码 235,表示登陆成功
		case 235:
			msg = []byte(msg64)
		// 其他情况,错误
		default:
			err = &textproto.Error{Code: code, Msg: msg64}
		}
		// 如果返回码是 334,获取下一步验证所需信息
		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
}

通过代码看出,Auth这个接口有两个方法,Start和Next,我们构建auth login的Auth对象的时候写好这两个方法就可以了

为了进一步了解,看一下plain的Auth对象,这个包里还有CRAMMD5的验证方法,感兴趣可以自己看

/*
	验证服务器基本信息,返回验证所需信息
 */
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
	// 如果不是安全连接,也不是本地的服务器,报错,不允许不安全的连接
	if !server.TLS && !isLocalhost(server.Name) {
		return "", nil, errors.New("unencrypted connection")
	}
	// 如果服务器信息和 Auth 对象的服务器信息不一致,报错
	if server.Name != a.host {
		return "", nil, errors.New("wrong host name")
	}
	// 验证时需要的账号密码,x00表示<NULL>
	resp := []byte(a.identity + "x00" + a.username + "x00" + a.password)
	// "auth plain" 命令
	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
}

了解了这两个方法,以及smtp是如何调用这两个方法进行验证的,我们就可以写出自己的用于auth login的Auth代码了

/*
	auth login
 */
type loginAuth struct {
	username, password string
	host                         string
}

/*
	auth login 验证
 */
func LoginAuth(username, password, host string) Auth {
	return &loginAuth{username, password, host}
}

/*
	初步验证服务器信息,输入账号
 */
func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
	// 如果不是安全连接,也不是本地的服务器,报错,不允许不安全的连接
	if !server.TLS && !isLocalhost(server.Name) {
		return "", nil, errors.New("unencrypted connection")
	}
	// 如果服务器信息和 Auth 对象的服务器信息不一致,报错
	if server.Name != a.host {
		return "", nil, errors.New("wrong host name")
	}
	// 验证时需要的账号
	resp := []byte(a.username)
	// "auth login" 命令
	return "LOGIN", resp, nil
}

/*
	进一步进行验证,输入密码
 */
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
	// 如果服务器需要更多验证,报错
	if more {
		return []byte(a.password), nil
	}
	return nil, nil
}

主函数调用我们自己写的Auth和smtp,运行,发送成功

func main() {
	hostname := "邮箱IP"
	auth := mySmtp.LoginAuth("发件邮箱", "密码", hostname)

	// Connect to the server, authenticate, set the sender and recipient,
	// and send the email all in one step.
	to := []string{"收件人邮箱"}
	msg := []byte("To: 收件人邮箱rn" +
		"Subject: 测试!rn" +
		"rn" +
		"This is the email body.rn")
	err := mySmtp.SendMail("邮箱IP:SMTP端口", auth, "发件邮箱", to, msg)
	if err != nil {
		log.Fatal(err)
	}
}

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/jiangxuege/article/details/86621433
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-02 17:58:43
  • 阅读 ( 1877 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢