使用outlook邮箱以smtp方式发送邮件失败的经历

最近在工作用遇到了一个问题,使用outlook邮箱以smtp方式发送邮件失败,报错如下:

bash 复制代码
504 5.7.4 Unrecognized authentication type [TYCP286CA0234.JPNP286.PROD.OUTLOOK.COM 2023-10-28T10:42:16.656Z 08DBD767E4655426]

仅从错误信息来看,是无法识别的身份验证类型

使用其它邮件客户端登录、发送邮件正常,所以密码是没有问题的。

经过一同搜索和AI的帮助,总算搞清楚了这个问题的原因。其原因,是微软已经废弃了AUTH PLAIN(说直白一点,就是明文密码传输),而我这边之前在发送邮件时,使用的是AUTH PLAIN方式。

解决办法其实也很简单,下面给出两个解决方案:

方案1-使用gomail库

这里采用简单粗暴的方式,使用gomail库来发送邮件,

go 复制代码
	from := "******"
	password := "******""
	toEmail := "******"
	smtpHost := "smtp.office365.com"

	m := gomail.NewMessage()
	m.SetHeader("From", from)
	m.SetHeader("To", toEmail)
	m.SetAddressHeader("Cc", from, "Dan")
	m.SetHeader("Subject", "入学通知书")
	m.SetBody("text/html", "请于2020年9月1日前到学校报道:"+time.Now().Local().String())

	d := gomail.NewDialer(smtpHost, 587, from, password)

	// Send the email to Bob, Cora and Dan.
	if err := d.DialAndSend(m); err != nil {
		panic(err)
	}

这个库其实好久没更新了。

吐槽一下:github上其实不少库都是这样,好久没更新了,让人用起来总是不太放心。

方案2-仍使用官方原生库,修改一下AUTH方式

这里采用的是修改AUTH方式的方式,使用的是net/smtp库(这里适应的是参考资料中的方案), 修改后的代码如下:

go 复制代码
package maildemo

import (
	"bytes"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/smtp"
	"testing"
	"text/template"
)

type loginAuth struct {
	username, password string
}

func LoginAuth(username, password string) smtp.Auth {
	return &loginAuth{username, password}
}

func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
	return "LOGIN", []byte(a.username), nil
}

func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
	if more {
		switch string(fromServer) {
		case "Username:":
			return []byte(a.username), nil
		case "Password:":
			return []byte(a.password), nil
		default:
			return nil, errors.New("Unknown from server")
		}
	}
	return nil, nil
}

func TestSendMailV3(t *testing.T) {
	// 设置服务器信息和邮箱账号。
	from := "******"
	password := "******"
	toEmail := "******"

	// Receiver email address.
	to := []string{
		toEmail,
	}

	// smtp server configuration.
	smtpHost := "smtp.office365.com"
	smtpPort := "587"

	conn, err := net.Dial("tcp", "smtp.office365.com:587")
	if err != nil {
		println(err)
	}

	c, err := smtp.NewClient(conn, smtpHost)
	if err != nil {
		t.Error(err)
		return
	}

	tlsconfig := &tls.Config{
		ServerName: smtpHost,
	}

	b, s := c.Extension("PLAIN")

	t.Log(b, s)

	if err = c.StartTLS(tlsconfig); err != nil {
		t.Error(err)
		return
	}

	auth := LoginAuth(from, password)

	if err = c.Auth(auth); err != nil {
		println(err)
	}

	t0, err := template.ParseFiles("template.html")

	if err != nil {
		t.Error(err)
		return
	}

	var body bytes.Buffer

	mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
	body.Write([]byte(fmt.Sprintf("Subject: This is a test subject \n%s\n\n", mimeHeaders)))

	t0.Execute(&body, struct {
		Name    string
		Message string
	}{
		Name:    "Hasan Yousef",
		Message: "This is a test message in a HTML template",
	})

	// Sending email.
	err = smtp.SendMail(smtpHost+":"+smtpPort, auth, from, to, body.Bytes())
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Email Sent!")
}

为什么这样改就可以呢?这里不说到SMTP的认证方式。我们可以看到上面的代码在START方法中,返回的第一个参数是LOGIN,而我们看看官方源码中plain auth的Start方法(net/smtp/auth.go):

go 复制代码
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
	// Must have TLS, or else localhost server.
	// 必须有TLS,否则是本地服务器。
	// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
	// 注意:如果TLS不为true,则我们不能信任ServerInfo中的任何内容。
	// In particular, it doesn't matter if the server advertises PLAIN auth.
	// 特别是,服务器是否告知PLAIN auth都无关紧要。
	// That might just be the attacker saying
	// 那可能是攻击者发的。
	// "it's ok, you can trust me with your password."
	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
}

我们看到,其返回的第一个参数是PLAIN。这是SMTP支持的验证方式之一,但是安全性不高,所以微软已经废弃了。

我们其实可以在net/smtp代码中打断点,看一下邮件服务器返回的其支持的smtp验证方式,如下图所示(我们这里是在SendMail函数中打断点): 可以看到,其支持的验证方式有:

  • LOGIN
  • XOAUTH2 没有PLAIN,所以我们之前直接用PLAIN AUTH发送时才会报错。

我们再来多一点好奇心,看看其它邮箱服务商的邮件服务器支持的验证方式:

QQ邮箱

smtp服务器地址:smtp.qq.com,端口:587。 可以看到,其支持的验证方式有:

  • LOGIN
  • PLAIN
  • XOAUTH
  • XOAUTH2

新浪邮箱

smtp服务器地址:smtp.sina.com,端口587。 可以看到,其支持的验证方式有:

  • LOGIN
  • PLAIN

参考文档

本文由mdnice多平台发布

相关推荐
Marktowin2 小时前
Mybatis-Plus更新操作时的一个坑
java·后端
赵文宇3 小时前
CNCF Dragonfly 毕业啦!基于P2P的镜像和文件分发系统快速入门,在线体验
后端
程序员爱钓鱼3 小时前
Node.js 编程实战:即时聊天应用 —— WebSocket 实现实时通信
前端·后端·node.js
Libby博仙4 小时前
Spring Boot 条件化注解深度解析
java·spring boot·后端
源代码•宸4 小时前
Golang原理剖析(Map 源码梳理)
经验分享·后端·算法·leetcode·golang·map
小周在成长4 小时前
动态SQL与MyBatis动态SQL最佳实践
后端
瓦尔登湖懒羊羊4 小时前
TCP的自我介绍
后端
小周在成长4 小时前
MyBatis 动态SQL学习
后端
子非鱼9214 小时前
SpringBoot快速上手
java·spring boot·后端