307 lines
6.9 KiB
Go
307 lines
6.9 KiB
Go
package email
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/tls"
|
||
"fmt"
|
||
"net"
|
||
"net/smtp"
|
||
"time"
|
||
|
||
"git.toowon.com/jimmy/go-commom/config"
|
||
)
|
||
|
||
// Email 邮件发送器
|
||
type Email struct {
|
||
config *config.EmailConfig
|
||
}
|
||
|
||
// NewEmail 创建邮件发送器
|
||
func NewEmail(cfg *config.EmailConfig) (*Email, error) {
|
||
if cfg == nil {
|
||
return nil, fmt.Errorf("email config is nil")
|
||
}
|
||
|
||
if cfg.Host == "" {
|
||
return nil, fmt.Errorf("email host is required")
|
||
}
|
||
|
||
if cfg.Username == "" {
|
||
return nil, fmt.Errorf("email username is required")
|
||
}
|
||
|
||
if cfg.Password == "" {
|
||
return nil, fmt.Errorf("email password is required")
|
||
}
|
||
|
||
// 设置默认值
|
||
if cfg.Port == 0 {
|
||
cfg.Port = 587
|
||
}
|
||
if cfg.From == "" {
|
||
cfg.From = cfg.Username
|
||
}
|
||
if cfg.Timeout == 0 {
|
||
cfg.Timeout = 30
|
||
}
|
||
|
||
return &Email{
|
||
config: cfg,
|
||
}, nil
|
||
}
|
||
|
||
// Message 邮件消息
|
||
type Message struct {
|
||
// To 收件人列表
|
||
To []string
|
||
|
||
// Cc 抄送列表(可选)
|
||
Cc []string
|
||
|
||
// Bcc 密送列表(可选)
|
||
Bcc []string
|
||
|
||
// Subject 主题
|
||
Subject string
|
||
|
||
// Body 正文(纯文本)
|
||
Body string
|
||
|
||
// HTMLBody HTML正文(可选,如果设置了会优先使用)
|
||
HTMLBody string
|
||
|
||
// Attachments 附件列表(可选)
|
||
Attachments []Attachment
|
||
}
|
||
|
||
// Attachment 附件
|
||
type Attachment struct {
|
||
// Filename 文件名
|
||
Filename string
|
||
|
||
// Content 文件内容
|
||
Content []byte
|
||
|
||
// ContentType 文件类型(如:application/pdf)
|
||
ContentType string
|
||
}
|
||
|
||
// SendRaw 发送原始邮件内容
|
||
// recipients: 收件人列表(To、Cc、Bcc的合并列表)
|
||
// body: 完整的邮件内容(MIME格式),由外部构建
|
||
func (e *Email) SendRaw(recipients []string, body []byte) error {
|
||
if len(recipients) == 0 {
|
||
return fmt.Errorf("recipients are required")
|
||
}
|
||
|
||
if len(body) == 0 {
|
||
return fmt.Errorf("email body is required")
|
||
}
|
||
|
||
// 连接SMTP服务器
|
||
addr := fmt.Sprintf("%s:%d", e.config.Host, e.config.Port)
|
||
auth := smtp.PlainAuth("", e.config.Username, e.config.Password, e.config.Host)
|
||
|
||
// 创建连接
|
||
conn, err := net.DialTimeout("tcp", addr, time.Duration(e.config.Timeout)*time.Second)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||
}
|
||
defer conn.Close()
|
||
|
||
// 创建SMTP客户端
|
||
client, err := smtp.NewClient(conn, e.config.Host)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create SMTP client: %w", err)
|
||
}
|
||
defer client.Close()
|
||
|
||
// TLS/SSL处理
|
||
if e.config.UseSSL {
|
||
// SSL模式(端口通常是465)
|
||
tlsConfig := &tls.Config{
|
||
ServerName: e.config.Host,
|
||
}
|
||
if err := client.StartTLS(tlsConfig); err != nil {
|
||
return fmt.Errorf("failed to start TLS: %w", err)
|
||
}
|
||
} else if e.config.UseTLS {
|
||
// TLS模式(STARTTLS,端口通常是587)
|
||
tlsConfig := &tls.Config{
|
||
ServerName: e.config.Host,
|
||
}
|
||
if err := client.StartTLS(tlsConfig); err != nil {
|
||
return fmt.Errorf("failed to start TLS: %w", err)
|
||
}
|
||
}
|
||
|
||
// 认证
|
||
if err := client.Auth(auth); err != nil {
|
||
return fmt.Errorf("failed to authenticate: %w", err)
|
||
}
|
||
|
||
// 设置发件人
|
||
if err := client.Mail(e.config.From); err != nil {
|
||
return fmt.Errorf("failed to set sender: %w", err)
|
||
}
|
||
|
||
// 设置收件人
|
||
for _, to := range recipients {
|
||
if err := client.Rcpt(to); err != nil {
|
||
return fmt.Errorf("failed to set recipient %s: %w", to, err)
|
||
}
|
||
}
|
||
|
||
// 发送邮件内容
|
||
writer, err := client.Data()
|
||
if err != nil {
|
||
return fmt.Errorf("failed to get data writer: %w", err)
|
||
}
|
||
|
||
_, err = writer.Write(body)
|
||
if err != nil {
|
||
writer.Close()
|
||
return fmt.Errorf("failed to write email body: %w", err)
|
||
}
|
||
|
||
err = writer.Close()
|
||
if err != nil {
|
||
return fmt.Errorf("failed to close writer: %w", err)
|
||
}
|
||
|
||
// 退出
|
||
if err := client.Quit(); err != nil {
|
||
return fmt.Errorf("failed to quit: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Send 发送邮件(使用Message结构,内部会构建邮件内容)
|
||
// 注意:如果需要完全控制邮件内容,请使用SendRaw方法
|
||
func (e *Email) Send(msg *Message) error {
|
||
if msg == nil {
|
||
return fmt.Errorf("message is nil")
|
||
}
|
||
|
||
if len(msg.To) == 0 {
|
||
return fmt.Errorf("recipients are required")
|
||
}
|
||
|
||
if msg.Subject == "" {
|
||
return fmt.Errorf("subject is required")
|
||
}
|
||
|
||
if msg.Body == "" && msg.HTMLBody == "" {
|
||
return fmt.Errorf("body or HTMLBody is required")
|
||
}
|
||
|
||
// 构建邮件内容
|
||
emailBody, err := e.buildEmailBody(msg)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to build email body: %w", err)
|
||
}
|
||
|
||
// 合并收件人列表
|
||
recipients := append(msg.To, msg.Cc...)
|
||
recipients = append(recipients, msg.Bcc...)
|
||
|
||
// 使用SendRaw发送
|
||
return e.SendRaw(recipients, emailBody)
|
||
}
|
||
|
||
// buildEmailBody 构建邮件内容
|
||
func (e *Email) buildEmailBody(msg *Message) ([]byte, error) {
|
||
var buf bytes.Buffer
|
||
|
||
// 邮件头
|
||
from := e.config.From
|
||
if e.config.FromName != "" {
|
||
from = fmt.Sprintf("%s <%s>", e.config.FromName, e.config.From)
|
||
}
|
||
buf.WriteString(fmt.Sprintf("From: %s\r\n", from))
|
||
|
||
// 收件人
|
||
buf.WriteString(fmt.Sprintf("To: %s\r\n", joinEmails(msg.To)))
|
||
|
||
// 抄送
|
||
if len(msg.Cc) > 0 {
|
||
buf.WriteString(fmt.Sprintf("Cc: %s\r\n", joinEmails(msg.Cc)))
|
||
}
|
||
|
||
// 主题
|
||
buf.WriteString(fmt.Sprintf("Subject: %s\r\n", msg.Subject))
|
||
|
||
// 内容类型
|
||
if msg.HTMLBody != "" {
|
||
// 多部分邮件(HTML + 纯文本)
|
||
boundary := "----=_Part_" + fmt.Sprint(time.Now().UnixNano())
|
||
buf.WriteString("MIME-Version: 1.0\r\n")
|
||
buf.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=\"%s\"\r\n", boundary))
|
||
buf.WriteString("\r\n")
|
||
|
||
// 纯文本部分
|
||
buf.WriteString("--" + boundary + "\r\n")
|
||
buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
|
||
buf.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
|
||
buf.WriteString("\r\n")
|
||
buf.WriteString(msg.Body)
|
||
buf.WriteString("\r\n")
|
||
|
||
// HTML部分
|
||
buf.WriteString("--" + boundary + "\r\n")
|
||
buf.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
|
||
buf.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
|
||
buf.WriteString("\r\n")
|
||
buf.WriteString(msg.HTMLBody)
|
||
buf.WriteString("\r\n")
|
||
|
||
buf.WriteString("--" + boundary + "--\r\n")
|
||
} else {
|
||
// 纯文本邮件
|
||
buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
|
||
buf.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
|
||
buf.WriteString("\r\n")
|
||
buf.WriteString(msg.Body)
|
||
buf.WriteString("\r\n")
|
||
}
|
||
|
||
return buf.Bytes(), nil
|
||
}
|
||
|
||
// joinEmails 连接邮箱地址
|
||
func joinEmails(emails []string) string {
|
||
if len(emails) == 0 {
|
||
return ""
|
||
}
|
||
result := emails[0]
|
||
for i := 1; i < len(emails); i++ {
|
||
result += ", " + emails[i]
|
||
}
|
||
return result
|
||
}
|
||
|
||
// SendSimple 发送简单邮件(便捷方法)
|
||
// to: 收件人
|
||
// subject: 主题
|
||
// body: 正文
|
||
func (e *Email) SendSimple(to []string, subject, body string) error {
|
||
return e.Send(&Message{
|
||
To: to,
|
||
Subject: subject,
|
||
Body: body,
|
||
})
|
||
}
|
||
|
||
// SendHTML 发送HTML邮件(便捷方法)
|
||
// to: 收件人
|
||
// subject: 主题
|
||
// htmlBody: HTML正文
|
||
func (e *Email) SendHTML(to []string, subject, htmlBody string) error {
|
||
return e.Send(&Message{
|
||
To: to,
|
||
Subject: subject,
|
||
HTMLBody: htmlBody,
|
||
})
|
||
}
|