Files
Gogs/internal/email/message.go
ᴊᴏᴇ ᴄʜᴇɴ 94d6e53dc2 email: replace gomail with go-mail (#8164)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 17:44:01 -05:00

166 lines
3.9 KiB
Go

package email
import (
"crypto/tls"
"net"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/inbucket/html2text"
gomail "github.com/wneessen/go-mail"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
)
type message struct {
info string
msg *gomail.Msg
confirmChan chan struct{}
}
func newMessageFrom(to []string, from, subject, htmlBody string) (*message, error) {
log.Trace("NewMessageFrom (htmlBody):\n%s", htmlBody)
m := gomail.NewMsg()
if err := m.From(from); err != nil {
return nil, errors.Wrapf(err, "set From address %q", from)
}
if err := m.To(to...); err != nil {
return nil, errors.Wrap(err, "set To addresses")
}
m.Subject(conf.Email.SubjectPrefix + subject)
m.SetDate()
if conf.Email.UsePlainText || conf.Email.AddPlainTextAlt {
plainBody, err := html2text.FromString(htmlBody)
if err != nil {
return nil, errors.Wrap(err, "convert HTML to plain text")
}
if conf.Email.UsePlainText {
m.SetBodyString(gomail.TypeTextPlain, plainBody)
} else {
m.SetBodyString(gomail.TypeTextPlain, plainBody)
m.AddAlternativeString(gomail.TypeTextHTML, htmlBody)
}
} else {
m.SetBodyString(gomail.TypeTextHTML, htmlBody)
}
return &message{
msg: m,
confirmChan: make(chan struct{}),
}, nil
}
func newMessage(to []string, subject, body string) (*message, error) {
return newMessageFrom(to, conf.Email.From, subject, body)
}
func newSMTPClient() (*gomail.Client, error) {
opts := conf.Email
host, portStr, err := net.SplitHostPort(opts.Host)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, err
}
clientOpts := []gomail.Option{
gomail.WithPort(port),
}
if port == 465 {
clientOpts = append(clientOpts, gomail.WithSSL())
} else {
clientOpts = append(clientOpts, gomail.WithTLSPolicy(gomail.TLSOpportunistic))
}
if opts.HELOHostname != "" {
clientOpts = append(clientOpts, gomail.WithHELO(opts.HELOHostname))
}
tlsconfig := &tls.Config{
InsecureSkipVerify: opts.SkipVerify,
ServerName: host,
}
if opts.UseCertificate {
cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile)
if err != nil {
return nil, err
}
tlsconfig.Certificates = []tls.Certificate{cert}
}
clientOpts = append(clientOpts, gomail.WithTLSConfig(tlsconfig))
if len(opts.User) > 0 {
clientOpts = append(clientOpts,
gomail.WithSMTPAuth(gomail.SMTPAuthAutoDiscover),
gomail.WithUsername(opts.User),
gomail.WithPassword(opts.Password),
)
}
return gomail.NewClient(host, clientOpts...)
}
func sendMessage(msg *message) error {
client, err := newSMTPClient()
if err != nil {
return err
}
return client.DialAndSend(msg.msg)
}
func processMailQueue() {
for msg := range mailQueue {
to := strings.Join(msg.msg.GetToString(), ", ")
log.Trace("New e-mail sending request %s: %s", to, msg.info)
if err := sendMessage(msg); err != nil {
log.Error("Failed to send emails %s: %s - %v", to, msg.info, err)
} else {
log.Trace("E-mails sent %s: %s", to, msg.info)
}
msg.confirmChan <- struct{}{}
}
}
var mailQueue chan *message
// NewContext initializes settings for mailer.
func NewContext() {
// Need to check if mailQueue is nil because in during reinstall (user had installed
// before but switched install lock off), this function will be called again
// while mail queue is already processing tasks, and produces a race condition.
if !conf.Email.Enabled || mailQueue != nil {
return
}
mailQueue = make(chan *message, 1000)
go processMailQueue()
}
// send puts new message object into mail queue.
// It returns without confirmation (mail processed asynchronously) in normal cases,
// but waits/blocks under hook mode to make sure mail has been sent.
func send(msg *message) {
if !conf.Email.Enabled {
return
}
mailQueue <- msg
if conf.HookMode {
<-msg.confirmChan
return
}
go func() {
<-msg.confirmChan
}()
}