Files
backend/internal/pkg/email/client.go
lan 4d8f2ec997 Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts.

Made-with: Cursor
2026-03-09 21:28:58 +08:00

132 lines
2.8 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 (
"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
}
}