Files
OpenFlare/backend/pkg/mail/mail.go
T

162 lines
4.1 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package mail
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"strings"
"time"
gomail "github.com/wneessen/go-mail"
golog "github.com/wneessen/go-mail/log"
)
const (
smtpSSLPort = 465 // SMTP SSL 端口
smtpDialTimeout = 5 * time.Second // SMTP 连接超时
)
// Config represents SMTP mail configuration
type Config struct {
Host string
Port int
Username string
Password string
FromName string // 可选发件人显示名称
InsecureSkipVerify bool // 是否跳过证书校验 (默认跳过以兼容自签名证书)
}
// Option modifies internal mail send options
type Option func(*clientOptions)
type clientOptions struct {
debugLogger golog.Logger
}
func withLogger(l golog.Logger) Option {
return func(co *clientOptions) {
co.debugLogger = l
}
}
// SendMail sends an HTML email using the provided config and message details
func SendMail(ctx context.Context, cfg Config, to, subject, body string) error {
return SendMailHTML(ctx, cfg, to, subject, body)
}
// SendMailHTML sends an HTML format email
func SendMailHTML(ctx context.Context, cfg Config, to, subject, body string) error {
return send(ctx, cfg, to, subject, body)
}
// SendMailWithLog sends a test email and records a detailed SMTP connection log
func SendMailWithLog(ctx context.Context, cfg Config, to, subject, body string) (string, error) {
var logBuf bytes.Buffer
logger := &bufferLogger{buf: &logBuf}
fmt.Fprintf(&logBuf, "[System] Connecting to %s:%d...\n", cfg.Host, cfg.Port)
err := send(ctx, cfg, to, subject, body, withLogger(logger))
if err != nil {
fmt.Fprintf(&logBuf, "[Error] Mail sending failed: %v\n", err)
return logBuf.String(), err
}
fmt.Fprintf(&logBuf, "[System] Mail sent successfully!\n")
return logBuf.String(), nil
}
func send(ctx context.Context, cfg Config, to, subject, body string, opts ...Option) error {
var co clientOptions
for _, opt := range opts {
opt(&co)
}
msg := gomail.NewMsg()
var err error
if cfg.FromName != "" {
err = msg.FromFormat(cfg.FromName, cfg.Username)
} else {
err = msg.From(cfg.Username)
}
if err != nil {
return fmt.Errorf(errCreateMailMessageFailed, err)
}
if err = msg.To(to); err != nil {
return fmt.Errorf(errCreateMailMessageFailed, err)
}
msg.Subject(subject)
msg.SetBodyString(gomail.TypeTextHTML, body)
clientOpts := []gomail.Option{
gomail.WithPort(cfg.Port),
gomail.WithTimeout(smtpDialTimeout),
}
tlsConfig := &tls.Config{
InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates
ServerName: cfg.Host,
}
clientOpts = append(clientOpts, gomail.WithTLSConfig(tlsConfig))
if cfg.Port == smtpSSLPort {
clientOpts = append(clientOpts, gomail.WithSSL())
} else {
clientOpts = append(clientOpts, gomail.WithTLSPolicy(gomail.TLSOpportunistic))
}
if cfg.Username != "" && cfg.Password != "" {
clientOpts = append(clientOpts,
gomail.WithSMTPAuth(gomail.SMTPAuthPlain),
gomail.WithUsername(cfg.Username),
gomail.WithPassword(cfg.Password),
)
}
if co.debugLogger != nil {
clientOpts = append(clientOpts, gomail.WithDebugLog(), gomail.WithLogger(co.debugLogger))
}
client, err := gomail.NewClient(cfg.Host, clientOpts...)
if err != nil {
return fmt.Errorf(errCreateMailClientFailed, err)
}
defer func() { _ = client.Close() }()
if err = client.DialAndSendWithContext(ctx, msg); err != nil {
return fmt.Errorf(errSendMailFailed, err)
}
return nil
}
type bufferLogger struct {
buf *bytes.Buffer
}
func (l *bufferLogger) log(level string, entry golog.Log) {
msg := fmt.Sprintf(entry.Format, entry.Messages...)
msg = strings.TrimRight(msg, "\r\n")
var dir string
switch entry.Direction {
case golog.DirClientToServer:
dir = "C"
case golog.DirServerToClient:
dir = "S"
default:
dir = level
}
fmt.Fprintf(l.buf, "[%s] %s\n", dir, msg)
}
func (l *bufferLogger) Debugf(e golog.Log) { l.log("Debug", e) }
func (l *bufferLogger) Infof(e golog.Log) { l.log("Info", e) }
func (l *bufferLogger) Warnf(e golog.Log) { l.log("Warn", e) }
func (l *bufferLogger) Errorf(e golog.Log) { l.log("Error", e) }