package email import ( "bytes" "crypto/tls" "fmt" "net" "net/smtp" "time" "git.toowon.com/jimmy/go-common/config" ) // Email 邮件发送器 type Email struct { config *config.EmailConfig } // NewEmail 创建邮件发送器 func NewEmail(cfg *config.Config) *Email { if cfg == nil || cfg.Email == nil { return &Email{config: nil} } return &Email{config: cfg.Email} } // getEmailConfig 获取邮件配置(内部方法) func (e *Email) getEmailConfig() (*config.EmailConfig, error) { if e.config == nil { return nil, fmt.Errorf("email config is nil") } if e.config.Host == "" { return nil, fmt.Errorf("email host is required") } if e.config.Username == "" { return nil, fmt.Errorf("email username is required") } if e.config.Password == "" { return nil, fmt.Errorf("email password is required") } // 设置默认值 if e.config.Port == 0 { e.config.Port = 587 } if e.config.From == "" { e.config.From = e.config.Username } if e.config.Timeout == 0 { e.config.Timeout = 30 } return e.config, nil } // Message 邮件消息 type Message struct { // To 收件人列表 To []string // Cc 抄送列表(可选) Cc []string // Bcc 密送列表(可选) Bcc []string // Subject 主题 Subject string // Body 正文(纯文本) Body string // HTMLBody HTML正文(可选,如果设置了会优先使用) HTMLBody string } // SendEmail 发送邮件 // to: 收件人列表 // subject: 邮件主题 // body: 邮件正文(纯文本) // htmlBody: HTML正文(可选,如果设置了会优先使用) func (e *Email) SendEmail(to []string, subject, body string, htmlBody ...string) error { cfg, err := e.getEmailConfig() if err != nil { return err } msg := &Message{ To: to, Subject: subject, Body: body, } if len(htmlBody) > 0 && htmlBody[0] != "" { msg.HTMLBody = htmlBody[0] } return e.send(msg, cfg) } // send 发送邮件(内部方法) func (e *Email) send(msg *Message, cfg *config.EmailConfig) 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, cfg) if err != nil { return fmt.Errorf("failed to build email body: %w", err) } // 合并收件人列表 recipients := append(msg.To, msg.Cc...) recipients = append(recipients, msg.Bcc...) // 连接SMTP服务器 addr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port)) auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host) // 创建连接 conn, err := net.DialTimeout("tcp", addr, time.Duration(cfg.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, cfg.Host) if err != nil { return fmt.Errorf("failed to create SMTP client: %w", err) } defer client.Close() // TLS/SSL处理 if cfg.UseSSL { // SSL模式(端口通常是465) tlsConfig := &tls.Config{ ServerName: cfg.Host, } if err := client.StartTLS(tlsConfig); err != nil { return fmt.Errorf("failed to start TLS: %w", err) } } else if cfg.UseTLS { // TLS模式(STARTTLS,端口通常是587) tlsConfig := &tls.Config{ ServerName: cfg.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(cfg.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(emailBody) 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 } // buildEmailBody 构建邮件内容 func (e *Email) buildEmailBody(msg *Message, cfg *config.EmailConfig) ([]byte, error) { var buf bytes.Buffer // 邮件头 from := cfg.From if cfg.FromName != "" { from = fmt.Sprintf("%s <%s>", cfg.FromName, cfg.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 }