package email import ( "bytes" "crypto/tls" "fmt" "net" "net/smtp" "time" "github.com/go-common/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, }) }