Files
go-common/email/email.go

282 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}