最近在用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 mainimport ("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.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)}
}

把上面的收发件人邮箱改好,邮箱服务器的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 {// 发送 ehloif 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 []byteswitch 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 AUTHc.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 stringhost                         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)}
}

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

  1. 乐视体感摄像头开发踩坑记录

    乐视三合一体感相机开发踩坑记录 第一次用Cmake,以下如有错误请大佬指正 开发环境: Linux ARM(树莓派4) AstraSDK-v2.1.3 Arm/Arm64(https://orbbec ...

  2. 几乎完美安装! NVIDIA Jetson Nano B01 Ubuntu 18.04.3 LTS 的 ROS 安装和菜鸟的踩坑记录【会继续完善】

    几乎完美安装! NVIDIA Jetson Nano B01 Ubuntu 18.04.3 LTS 的 ROS 安装和菜鸟的踩坑记录 NVIDIA Jetson Nano B01技术规格 Ubuntu ...

  3. YOLOv5在无人机/遥感场景下做旋转目标检测时进行的适应性改建详解(踩坑记录)...

    作者丨略略略@知乎(已授权) 来源丨https://zhuanlan.zhihu.com/p/358441134 编辑丨极市平台 文章开头直接放上我自己的项目代码: https://github.co ...

  4. 日常踩坑记录-汇总版

    开发踩坑记录,不定时更新 心得 RTFM 严谨的去思考问题,处理问题 严格要求自己的代码编写习惯与风格 注意 单词拼写 20200207 mybatis plus 自带insert插入异常 sql i ...

  5. webview进行下载踩坑记录

    webview下载操作的踩坑记录 背景记录 由于公司需要, 需要在在webview中执行下载操作, 而且下载完成之后还需要跳转到自动安装页面~~~~ 接下来就是踩坑报告 1.webview执行下载操作 ...

  6. 微信客服机器人(踩坑记录、SpringBoot、企业微信)

    微信客服机器人(踩坑记录.SpringBoot.企业微信) 转载请注明出处:https://www.jjput.com/archives/wei-xin-ke-fu-ji-qi-ren 总体流程 当有 ...

  7. CMT2380/HC32L110入门踩坑记录

    CMT2380/HC32L110入门踩坑记录 写在前面 1.空白工程启动文件的问题 2.RTC时钟问题 3.UART格式化输出的问题 4.SysTick进行延时 5.SW调试卡住或运行后卡住 6.pr ...

  8. uc浏览器请求被拦截报跨域踩坑记录

    记录下开发时uc浏览器请求被拦截时遇到的问题 请求在uc浏览器出现跨域问题 app使用uniapp开发,使用plus.runtime.launchApplication来打开并跳转指定页面,并在Xco ...

  9. 【踩坑记录】实体机器人运行Cartographer 3D Slam(深度摄像头)--未解决

    [运行背景] ROS1.0  20.04 noetic 机器人:NXRobo SPARK-T 安装cartographer请看: [安装学习]安装Cartographer ROS(noetic)_Ho ...

最新文章

  1. python设置函数执行时间
  2. Boost:标准地图比较的测试程序
  3. 【数据库】13种会导致索引失效语句写法
  4. 在.Net中,如何创建一个后台执行的进程?
  5. 查看分支编码_MySQL分支数据库MariaDB之CentOS安装教程
  6. (五)Netty之Selector选择器
  7. javaweb实训第三天下午——Web基础-Servlet交互JSP原理及运用
  8. python和java学哪个好-Python和Java该学哪个?还在纠结的你看过来呀~
  9. 工科数学分析大作业(三) 傅里叶级数
  10. 线性代数学习笔记——第十二讲——求解矩阵方程
  11. 网红“小红书”,电商销售新模式
  12. 图书馆机器人索书号识别
  13. Spring @InitBinder注解
  14. 区块链的4种技术创新
  15. vimeo下载_通过Vimeo的API喜欢,关注列表和上传
  16. oracle查询timestamp范围,Oracle TIMESTAMP数据类型
  17. UE4之局域网游戏如何更改配置文件
  18. Ubuntu:apt 配置阿里源
  19. linux快速同步多台服务器之间的数据scp和rsync命令用法
  20. 新鲜出炉: Zadig V1.1.0 发布!

热门文章

  1. Java并发HashMap报错ConcurrentModificationException解决方案
  2. 隔直电容选取及大小选择
  3. JDK下载(jdk1.8下载与安装教程)
  4. Linux 压缩、解压缩命令
  5. php mail 垃圾邮件,如何避免我的邮件从PHP邮件()被标记为垃圾邮件? - 程序园
  6. 基于XBee进行ZigBee组网(二)——ZigBee网络与XCTU的使用
  7. python opencv resize函数_OpenCV尺寸调整函数resize
  8. 关于使用正则表达式进行文本替换
  9. 最近做到的一些有意思的数学题目(博弈,双人玩游戏)
  10. 类的成员函数作为函数指针