Go - net/smtp发送邮件

目的

使用go简单实现发送邮件的逻辑



基础

SMTP

简单邮件传输协议(英语:Simple Mail Transfer Protocol,缩写:SMTP

可以简单的使用telnet来连接测试一个SMTP服务器



代码

package main

import (
	"errors"
	"fmt"
	"net/smtp"
)

// 自行实现 smtp.Auth 接口
type loginAuth struct {
	username, password string
}

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

func (l loginAuth) Next(fromServer []byte, more bool) (toServer []byte, err error) {
	if more {
		switch string(fromServer) {
		case "Username:":
			return []byte(l.username), nil
		case "Password:":
			return []byte(l.password), nil
		default:
			return nil, errors.New("unknown field: " + string(fromServer))
		}
	}
	return nil, nil
}

func getLoginAuth(name, pw string) smtp.Auth {
	return &loginAuth{
		username: name,
		password: pw,
	}
}

type Email struct {
	Subject  string
	Contents string
}

const (
	EMAIL_USER_NAME     = "xxx"
	EMAIL_USER_PASSWORD = "xxxx"
	EMAIL_USER_ADDR     = "xxxx@example.com"
	EMAIL_ADDR_HOST     = "mail.example.com"
	EMAIL_ADDR_PORT     = "xx"
)

func sendEmail(address []string, data *Email) error {
	addr := fmt.Sprintf("%s:%s", EMAIL_ADDR_HOST, EMAIL_ADDR_PORT)
	auth := getLoginAuth(EMAIL_USER_NAME, EMAIL_USER_PASSWORD)
	for _, val := range address {
		// msg : RFC 822-style
		msg := "To:" + val + "\r\n" + "Subject: " + data.Subject + "\r\n\r\n" + data.Contents + "\r\n"
		err := smtp.SendMail(addr, auth, EMAIL_USER_ADDR, []string{val}, []byte(msg))
		if err != nil {
			return err
		}
	}
	return nil
}

func main() {
	fmt.Println(sendEmail([]string{"xxxxxxx@example.com"}, &Email{
		Subject:  "email test title",
		Contents: "email test contents",
	}))
}

注:关于msg的格式可以自行搜索RFC 822



疑问

在查找资料写的时候发现,为什么telnet连接后获取不到LOGIN认证方式,但是代码可以用LOGIN方式认证成功。

telnet mail.qianxin.com 587
220 SRV-MAIL05.ESG.360ES.CN Microsoft ESMTP MAIL Service ready at Thu, 3 Dec 2020 20:03:07 +0800
ehlo localhost
250-SRV-MAIL05.ESG.360ES.CN Hello [10.91.130.40]
250-SIZE 125829120
250-PIPELINING
250-DSN
250-ENHANCEDSTATUSCODES
250-STARTTLS
250-AUTH GSSAPI NTLM
250-8BITMIME
250-BINARYMIME
250 CHUNKING

查看sendMail源码会发现整体发送流程如下

func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
	// 1. 参数校验
	if err := validateLine(from); err != nil {
		return err
	}
	for _, recp := range to {
		if err := validateLine(recp); err != nil {
			return err
		}
	}
	//2. 建立 tcp 连接
	c, err := Dial(addr)
	if err != nil {
		return err
	}
	defer c.Close()
	
	//3. 发送 ehlo 获取信息
	if err = c.hello(); err != nil {
		return err
	}
	//4. 如果支持tls 则重新获取信息
	if ok, _ := c.Extension("STARTTLS"); ok {
		config := &tls.Config{ServerName: c.serverName}
		if testHookStartTLS != nil {
			testHookStartTLS(config)
		}
		if err = c.StartTLS(config); err != nil {
			return err
		}
	}
	//5. 用户身份验证
	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
		}
	}
	//6. 准备数据 发送邮件
	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()
}

通过了解SendMail源码可以看出,可能是由于STARTTLS导致。所以在STARTTLS前后打印c.auth即可发现

STARTTLS前返回支持GSSAPI NTLM两种,开启了TLS后返回了GSSAPI NTLM LOGIN三种,所以代码中可以通过LOGIN方式进行认证。

再搜了搜发现,可以使用openssl来测试开启TLS后的效果,此时就拥有了LOGIN认证方式,并且可以成功认证。

[root@node ~]# openssl s_client -starttls smtp -crlf -connect mail.example.com:111
...
---
250 CHUNKING

ehlo mail.qianxin.com
...
250-AUTH GSSAPI NTLM LOGIN


AUTH LOGIN base64编码后得用户名
334 UGFzc3dvcmQ6
base64编码后的密码
235 2.7.0 Authentication successful


参考

  1. https://www.dennisthink.com/2019/01/04/53/
  2. https://www.kshuang.xyz/doku.php/operating_system:nix_testing_smtp_with_command_line
  3. https://cloud.tencent.com/developer/section/1143712#stage-100018578
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值