目的
使用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
参考
- https://www.dennisthink.com/2019/01/04/53/
- https://www.kshuang.xyz/doku.php/operating_system:nix_testing_smtp_with_command_line
- https://cloud.tencent.com/developer/section/1143712#stage-100018578