2024-11-20 19:42:07 +08:00
|
|
|
package smtp
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
2025-10-30 14:13:18 +08:00
|
|
|
"crypto/tls"
|
2024-11-20 19:42:07 +08:00
|
|
|
"encoding/base64"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/smtp"
|
|
|
|
|
"os"
|
|
|
|
|
"path"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"code.yun.ink/pkg/mailx/interfaces"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 邮件发送的封装
|
|
|
|
|
// 1. 支持文本
|
|
|
|
|
// 2. 支持文件
|
|
|
|
|
|
|
|
|
|
type Smtp struct {
|
|
|
|
|
interfaces.DefaultEmail
|
2025-08-10 21:17:10 +08:00
|
|
|
// params *interfaces.EmailConfigDataSmtp
|
|
|
|
|
auth smtp.Auth
|
|
|
|
|
// logger loggerx.LoggerInterface
|
2024-11-20 19:42:07 +08:00
|
|
|
}
|
|
|
|
|
|
2025-08-10 21:17:10 +08:00
|
|
|
func NewSmtp() *Smtp {
|
|
|
|
|
smtp := &Smtp{}
|
|
|
|
|
smtp.Options = interfaces.DefaultOptions()
|
2025-08-30 18:14:09 +08:00
|
|
|
smtp.EmailType = interfaces.EmailTypeSmtp
|
2025-08-10 21:17:10 +08:00
|
|
|
return smtp
|
|
|
|
|
}
|
2024-11-20 19:42:07 +08:00
|
|
|
|
2025-08-10 21:17:10 +08:00
|
|
|
func (l *Smtp) SetOption(ctx context.Context, opt ...interfaces.Option) (interfaces.EmailInterface, error) {
|
|
|
|
|
|
|
|
|
|
for _, o := range opt {
|
|
|
|
|
o(&l.Options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
l.Options.Logger.Infof(ctx, "params:%+v", l.Options.Smtp)
|
|
|
|
|
|
|
|
|
|
if l.Options.Smtp == nil {
|
2024-11-20 19:42:07 +08:00
|
|
|
return nil, errors.New("not smtp")
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 21:17:10 +08:00
|
|
|
return l, nil
|
2024-11-20 19:42:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *Smtp) Send(ctx context.Context, message interfaces.Message) error {
|
2025-08-10 21:17:10 +08:00
|
|
|
if l.Options.Smtp == nil {
|
2024-11-20 19:42:07 +08:00
|
|
|
return errors.New("not init")
|
|
|
|
|
}
|
2025-10-30 14:13:18 +08:00
|
|
|
|
|
|
|
|
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 {
|
2024-11-20 19:42:07 +08:00
|
|
|
// .Auth()
|
|
|
|
|
buffer := bytes.NewBuffer(nil)
|
|
|
|
|
boundary := "YunBoundaryYun"
|
|
|
|
|
|
|
|
|
|
Header := make(map[string]string)
|
|
|
|
|
// Header["From"] = "BOP<" + message.Form + ">"
|
|
|
|
|
|
|
|
|
|
if message.Form != "" {
|
|
|
|
|
Header["From"] = message.Form
|
|
|
|
|
} else {
|
2025-08-10 21:17:10 +08:00
|
|
|
Header["From"] = l.Options.Smtp.Username
|
2024-11-20 19:42:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(message.To) > 0 {
|
|
|
|
|
str := ""
|
|
|
|
|
for _, val := range message.To {
|
|
|
|
|
name := ""
|
|
|
|
|
s := strings.Split(val, "@")
|
|
|
|
|
if len(s) > 0 {
|
|
|
|
|
name = s[0]
|
|
|
|
|
}
|
|
|
|
|
str = str + "," + name + "<" + val + ">"
|
|
|
|
|
}
|
|
|
|
|
Header["To"] = strings.Trim(str, ",")
|
|
|
|
|
// Header["To"] = strings.Join(message.To, ",")
|
|
|
|
|
}
|
|
|
|
|
if len(message.Cc) > 0 {
|
|
|
|
|
str := ""
|
|
|
|
|
for _, val := range message.Cc {
|
|
|
|
|
name := ""
|
|
|
|
|
s := strings.Split(val, "@")
|
|
|
|
|
if len(s) > 0 {
|
|
|
|
|
name = s[0]
|
|
|
|
|
}
|
|
|
|
|
str = str + "," + name + "<" + val + ">"
|
|
|
|
|
}
|
|
|
|
|
Header["Cc"] = strings.Trim(str, ",")
|
|
|
|
|
// Header["Cc"] = strings.Join(message.Cc, ",")
|
|
|
|
|
}
|
|
|
|
|
if len(message.Bcc) > 0 {
|
|
|
|
|
str := ""
|
|
|
|
|
for _, val := range message.Bcc {
|
|
|
|
|
name := ""
|
|
|
|
|
s := strings.Split(val, "@")
|
|
|
|
|
if len(s) > 0 {
|
|
|
|
|
name = s[0]
|
|
|
|
|
}
|
|
|
|
|
str = str + "," + name + "<" + val + ">"
|
|
|
|
|
}
|
|
|
|
|
Header["Bcc"] = strings.Trim(str, ",")
|
|
|
|
|
// Header["Bcc"] = strings.Join(message.Bcc, ",")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Header["Subject"] = message.Subject
|
|
|
|
|
Header["Content-Type"] = "multipart/mixed; charset=UTF-8; boundary=" + boundary
|
|
|
|
|
Header["Date"] = time.Now().String()
|
|
|
|
|
Header["Reply-To"] = message.ReplyTo
|
|
|
|
|
|
|
|
|
|
Header["X-Priority"] = "3"
|
|
|
|
|
l.writeHeader(buffer, Header)
|
|
|
|
|
|
|
|
|
|
body := "--" + boundary + "\r\n"
|
|
|
|
|
// body += "Content-Type: text/plain; charset=UTF-8 \r\n"
|
|
|
|
|
body += "Content-Type: text/html;charset=utf-8\r\n"
|
|
|
|
|
body += "Content-Transfer-Encoding:quoted-printable\r\n\r\n"
|
|
|
|
|
// body += "<html><body><h1>huang</h1><h2>xin</h2></body></html>\r\n"
|
|
|
|
|
|
|
|
|
|
// body += "<html><body>" + message.Body + "</body></html>\r\n"
|
|
|
|
|
|
|
|
|
|
body += message.Body + "\r\n"
|
|
|
|
|
|
|
|
|
|
// body += "--" + boundary + "--\r\n\r\n"
|
|
|
|
|
buffer.WriteString(body)
|
|
|
|
|
|
|
|
|
|
for _, value := range message.Attachment {
|
|
|
|
|
newBuf := bytes.NewBuffer(nil)
|
|
|
|
|
err := l.writeFile(newBuf, value.Content)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Println("file err:", err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f_name := path.Base(value.Content)
|
|
|
|
|
attachment := "--" + boundary + "\r\n"
|
|
|
|
|
attachment += "Content-Transfer-Encoding:base64\r\n"
|
|
|
|
|
attachment += "Content-Disposition:attachment;filename=" + f_name + "\r\n"
|
|
|
|
|
attachment += "Content-Type: application/octet-stream;charset=utf-8;name=" + f_name + "\r\n"
|
|
|
|
|
// attachment += "Contment-Type:" + message.attachment.contentType + ";name=\"" + message.attachment.name + "\"\r\n"
|
|
|
|
|
buffer.WriteString(attachment)
|
|
|
|
|
|
|
|
|
|
buffer.WriteString(newBuf.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buffer.WriteString("\r\n--" + boundary + "--\r\n")
|
|
|
|
|
b := buffer.Bytes()
|
2025-10-30 14:13:18 +08:00
|
|
|
l.auth = smtp.PlainAuth("", l.Options.Smtp.Username, l.Options.Smtp.Password, l.Options.Smtp.Host)
|
2025-08-10 21:17:10 +08:00
|
|
|
err := smtp.SendMail(l.Options.Smtp.Host+":"+l.Options.Smtp.Port, l.auth, l.Options.Smtp.Username, message.To, b)
|
2024-11-20 19:42:07 +08:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:13:18 +08:00
|
|
|
// 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()
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-20 19:42:07 +08:00
|
|
|
// 格式化header
|
|
|
|
|
func (l *Smtp) writeHeader(buffer *bytes.Buffer, Header map[string]string) string {
|
|
|
|
|
header := ""
|
|
|
|
|
// header := "Content-Type: multipart/mixed;charset=UTF-8;boundary=\"YunBoundaryYun\" \r\n"
|
|
|
|
|
for key, value := range Header {
|
|
|
|
|
if value != "" {
|
|
|
|
|
header += key + ": " + value + "\r\n"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
header += "\r\n"
|
|
|
|
|
buffer.WriteString(header)
|
|
|
|
|
return header
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化文件
|
|
|
|
|
func (l *Smtp) writeFile(buffer *bytes.Buffer, fileName string) error {
|
|
|
|
|
file, err := os.ReadFile(fileName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
payload := make([]byte, base64.StdEncoding.EncodedLen(len(file)))
|
|
|
|
|
base64.StdEncoding.Encode(payload, file)
|
|
|
|
|
buffer.WriteString("\r\n")
|
|
|
|
|
for index, line := 0, len(payload); index < line; index++ {
|
|
|
|
|
buffer.WriteByte(payload[index])
|
|
|
|
|
if (index+1)%76 == 0 {
|
|
|
|
|
buffer.WriteString("\r\n")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|