Files
backend/internal/pkg/email/client.go

132 lines
2.8 KiB
Go
Raw Normal View History

package email
import (
"context"
"crypto/tls"
"fmt"
"strings"
"time"
gomail "gopkg.in/gomail.v2"
)
// Message 发信参数
type Message struct {
To []string
Cc []string
Bcc []string
ReplyTo []string
Subject string
TextBody string
HTMLBody string
Attachments []string
}
type Client interface {
IsEnabled() bool
Config() Config
Send(ctx context.Context, msg Message) error
}
type clientImpl struct {
cfg Config
}
func NewClient(cfg Config) Client {
return &clientImpl{cfg: cfg}
}
func (c *clientImpl) IsEnabled() bool {
return c.cfg.Enabled &&
strings.TrimSpace(c.cfg.Host) != "" &&
c.cfg.Port > 0 &&
strings.TrimSpace(c.cfg.FromAddress) != ""
}
func (c *clientImpl) Config() Config {
return c.cfg
}
func (c *clientImpl) Send(ctx context.Context, msg Message) error {
if !c.IsEnabled() {
return fmt.Errorf("email client is disabled or misconfigured")
}
if len(msg.To) == 0 {
return fmt.Errorf("email recipient is empty")
}
if strings.TrimSpace(msg.Subject) == "" {
return fmt.Errorf("email subject is empty")
}
if strings.TrimSpace(msg.TextBody) == "" && strings.TrimSpace(msg.HTMLBody) == "" {
return fmt.Errorf("email body is empty")
}
m := gomail.NewMessage()
m.SetAddressHeader("From", c.cfg.FromAddress, c.cfg.FromName)
m.SetHeader("To", msg.To...)
if len(msg.Cc) > 0 {
m.SetHeader("Cc", msg.Cc...)
}
if len(msg.Bcc) > 0 {
m.SetHeader("Bcc", msg.Bcc...)
}
if len(msg.ReplyTo) > 0 {
m.SetHeader("Reply-To", msg.ReplyTo...)
}
m.SetHeader("Subject", msg.Subject)
if strings.TrimSpace(msg.TextBody) != "" && strings.TrimSpace(msg.HTMLBody) != "" {
m.SetBody("text/plain", msg.TextBody)
m.AddAlternative("text/html", msg.HTMLBody)
} else if strings.TrimSpace(msg.HTMLBody) != "" {
m.SetBody("text/html", msg.HTMLBody)
} else {
m.SetBody("text/plain", msg.TextBody)
}
for _, attachment := range msg.Attachments {
if strings.TrimSpace(attachment) == "" {
continue
}
m.Attach(attachment)
}
timeout := c.cfg.TimeoutSeconds
if timeout <= 0 {
timeout = 15
}
dialer := gomail.NewDialer(c.cfg.Host, c.cfg.Port, c.cfg.Username, c.cfg.Password)
if c.cfg.UseTLS {
dialer.TLSConfig = &tls.Config{
ServerName: c.cfg.Host,
InsecureSkipVerify: c.cfg.InsecureSkipVerify,
}
// 465 端口通常要求直接 TLSImplicit TLS
if c.cfg.Port == 465 {
dialer.SSL = true
}
}
sendCtx := ctx
cancel := func() {}
if timeout > 0 {
sendCtx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
}
defer cancel()
done := make(chan error, 1)
go func() {
done <- dialer.DialAndSend(m)
}()
select {
case <-sendCtx.Done():
return fmt.Errorf("send email canceled: %w", sendCtx.Err())
case err := <-done:
if err != nil {
return fmt.Errorf("send email failed: %w", err)
}
return nil
}
}