最近在用go写一个小工具,一个小功能是用smtp发邮件,用公司内网的邮箱服务器实现踩了不少坑
想知道x509: cannot validate certificate for解决的直接看2.2.1,想知道auth login怎么实现看2.2.2
基础知识,回顾一下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:填写邮件主题
<空一行>
填写邮件内容
...
邮件内容
.
之前用的是auth login方式,smtp还有很多其他方式,可参考这篇文章 SMTP(Login,Plain,CRAM-MD5)验证
用ehlo来代替helo命令,就可以查询这个邮件服务器支持的auth方式
我在qq邮箱和我公司邮件服务器上尝试ehlo,得到的返回如下
所以qq支持auth login和plain两种方式,我公司的邮件服务器只支持auth login,plain的格式是
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("", "[email protected]", "password", "mail.example.com")
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
to := []string{"[email protected]"}
msg := []byte("To: [email protected]\r\n" +
"Subject: discount Gophers!\r\n" +
"\r\n" +
"This is the email body.\r\n")
err := smtp.SendMail("mail.example.com:25", auth, "[email protected]", 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()
}
换上我们公司的邮箱服务器,报错
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
前面说过,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表示
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: 收件人邮箱\r\n" +
"Subject: 测试!\r\n" +
"\r\n" +
"This is the email body.\r\n")
err := mySmtp.SendMail("邮箱IP:SMTP端口", auth, "发件邮箱", to, msg)
if err != nil {
log.Fatal(err)
}
}