From e0939d872823f4517022295158f493c18abd2f22 Mon Sep 17 00:00:00 2001 From: Yun Date: Thu, 30 Oct 2025 14:13:18 +0800 Subject: [PATCH] =?UTF-8?q?smtp=E6=94=AF=E6=8C=81ssl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interfaces/consts.go | 1 + smtp/smtp.go | 84 +++++++++++++++++++++++++++++++++++++++++++- smtp/smtp_test.go | 48 ++++++++++++++++--------- 3 files changed, 115 insertions(+), 18 deletions(-) diff --git a/interfaces/consts.go b/interfaces/consts.go index a834ed7..7dbb428 100644 --- a/interfaces/consts.go +++ b/interfaces/consts.go @@ -7,6 +7,7 @@ type EmialConfigDataMailgun struct { } type EmailConfigDataSmtp struct { + IsSSL bool `json:"is_ssl"` // 是否SSL Username string `json:"username"` // 邮箱账号 Password string `json:"password"` // 授权码 Host string `json:"host"` // SMTP 服务器【默认smtpdm.aliyun.com】 diff --git a/smtp/smtp.go b/smtp/smtp.go index 065b597..f5598a9 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -3,6 +3,7 @@ package smtp import ( "bytes" "context" + "crypto/tls" "encoding/base64" "errors" "fmt" @@ -45,7 +46,6 @@ func (l *Smtp) SetOption(ctx context.Context, opt ...interfaces.Option) (interfa return nil, errors.New("not smtp") } - l.auth = smtp.PlainAuth("", l.Options.Smtp.Username, l.Options.Smtp.Password, l.Options.Smtp.Host) return l, nil } @@ -53,6 +53,16 @@ func (l *Smtp) Send(ctx context.Context, message interfaces.Message) error { if l.Options.Smtp == nil { return errors.New("not init") } + + if l.Options.Smtp.IsSSL { + // + return l.SendSSL(ctx, message) + } + return l.SendPlain(ctx, message) +} + +// 明文 +func (l *Smtp) SendPlain(ctx context.Context, message interfaces.Message) error { // .Auth() buffer := bytes.NewBuffer(nil) boundary := "YunBoundaryYun" @@ -148,10 +158,82 @@ func (l *Smtp) Send(ctx context.Context, message interfaces.Message) error { buffer.WriteString("\r\n--" + boundary + "--\r\n") b := buffer.Bytes() + l.auth = smtp.PlainAuth("", l.Options.Smtp.Username, l.Options.Smtp.Password, l.Options.Smtp.Host) err := smtp.SendMail(l.Options.Smtp.Host+":"+l.Options.Smtp.Port, l.auth, l.Options.Smtp.Username, message.To, b) return err } +// SSL +func (l *Smtp) SendSSL(ctx context.Context, message interfaces.Message) error { + + // 邮件内容格式(必须符合SMTP协议规范) + contentType := "Content-Type: text/html; charset=UTF-8" + msg := []byte("To: " + message.To[0] + "\r\n" + + "From: " + message.Form + "\r\n" + + "Subject: " + message.Subject + "\r\n" + + contentType + "\r\n\r\n" + + message.Body) + + // 解析收件人(支持多个) + // toList := strings.Split(message.To, ",") + + // 1. 建立TCP连接(SSL通常用465端口) + if l.Options.Smtp.Port == "" { + l.Options.Smtp.Port = "465" + } + host := l.Options.Smtp.Host + ":" + l.Options.Smtp.Port + + conn, err := tls.Dial("tcp", host, &tls.Config{ + InsecureSkipVerify: false, // 生产环境不建议跳过证书验证 + ServerName: strings.Split(host, ":")[0], // 服务器域名(用于证书验证) + }) + if err != nil { + return fmt.Errorf("连接服务器失败: %v", err) + } + defer conn.Close() + + // 2. 创建SMTP客户端 + client, err := smtp.NewClient(conn, strings.Split(host, ":")[0]) + if err != nil { + return fmt.Errorf("创建SMTP客户端失败: %v", err) + } + + // 3. 身份验证(使用登录邮箱和授权码) + auth := smtp.PlainAuth("", l.Options.Smtp.Username, l.Options.Smtp.Password, strings.Split(host, ":")[0]) + if err := client.Auth(auth); err != nil { + return fmt.Errorf("身份验证失败: %v", err) + } + + // 4. 设置发件人 + if err := client.Mail(l.Options.Smtp.Username); err != nil { + return fmt.Errorf("设置发件人失败: %v", err) + } + + // 5. 设置收件人(支持多个) + for _, addr := range message.To { + if err := client.Rcpt(addr); err != nil { + return fmt.Errorf("设置收件人 %s 失败: %v", addr, err) + } + } + + // 6. 发送邮件内容 + w, err := client.Data() + if err != nil { + return fmt.Errorf("获取数据写入流失败: %v", err) + } + _, err = w.Write(msg) + if err != nil { + return fmt.Errorf("写入邮件内容失败: %v", err) + } + err = w.Close() + if err != nil { + return fmt.Errorf("关闭写入流失败: %v", err) + } + + // 7. 退出SMTP会话 + return client.Quit() +} + // 格式化header func (l *Smtp) writeHeader(buffer *bytes.Buffer, Header map[string]string) string { header := "" diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index b71d060..bc559d7 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3,8 +3,6 @@ package smtp_test import ( "context" "fmt" - "io" - "net/http" "testing" "code.yun.ink/pkg/mailx/interfaces" @@ -15,35 +13,51 @@ func TestMail(t *testing.T) { sm := smtp.NewSmtp() ctx := context.Background() + // ini, err := sm.SetOption(ctx, interfaces.SetSmtp(&interfaces.EmailConfigDataSmtp{ + // Username: "support@email.blueoceanpay.com", + // Password: "SupporT2017", + // ReplyTo: "", + // Host: "smtpdm-ap-southeast-1.aliyun.com", + // Port: "80", + // })) + // if err != nil { + // t.Fatal(err) + // } + + ini, err := sm.SetOption(ctx, interfaces.SetSmtp(&interfaces.EmailConfigDataSmtp{ - Username: "support@email.blueoceanpay.com", - Password: "SupporT2017", + IsSSL: true, + Username: "demo@yunink.net", + Password: "aAhVPzHydLoc9Fj8", ReplyTo: "", - Host: "smtpdm-ap-southeast-1.aliyun.com", - Port: "80", + Host: "smtp.exmail.qq.com", + Port: "465", })) if err != nil { t.Fatal(err) } - req, err := http.Get("https://baidu.com") - if err != nil { - t.Fatal(err) - } - defer req.Body.Close() - by, err := io.ReadAll(req.Body) - if err != nil { - t.Fatal(err) - } + + // req, err := http.Get("https://baidu.com") + // if err != nil { + // t.Fatal(err) + // } + // defer req.Body.Close() + + // by, err := io.ReadAll(req.Body) + // if err != nil { + // t.Fatal(err) + // } msg := interfaces.Message{ + Form: "demo@yunink.net", To: []string{"995116474@qq.com"}, Cc: []string{"287852692@qq.com"}, Bcc: []string{"1362716835@qq.com"}, ReplyTo: "huangxinyun@dreaminglife.cn", - Subject: "test mail", - Body: string(by), + Subject: "测试邮件", + Body: "这是测试邮件", //string(by), Attachment: []interfaces.MessageAttachment{ // { // Name: "/code/statistic/out.xlsx",