132 lines
2.8 KiB
Go
132 lines
2.8 KiB
Go
|
|
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 端口通常要求直接 TLS(Implicit 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
|
|||
|
|
}
|
|||
|
|
}
|