refactor(email): Refactor the email sending

Remove SMTP connection pool in favor of direct connections

BREAKING CHANGE: Replace SMTP connection pool with direct connections

The SMTP connection pool has been removed in favor of creating new
connections for each email send operation. This change:

- Removes Pool struct and related code
- Adds new Mailer struct for handling email operations
- Updates all email sending functions to use new Mailer
- Removes pool size configuration
- Simplifies connection management

Rationale:
- SMTP servers often don't handle long-lived connections well
- Connection pool adds complexity without significant benefits
- Direct connections are more reliable for email sending
- Email sending is typically not high-frequency enough to warrant pooling

Migration:
- Replace getSmtpPool() calls with getMailer()
- Update any direct Pool usage to use Mailer instead
pull/269/head
Oganneson 10 months ago
parent 9b9b68f130
commit 940334ee36
No known key found for this signature in database
GPG Key ID: 343FC4256C7BA76A

@ -32,7 +32,7 @@ var (
model.SettingGroupEmail,
settings.WithAfterSetBool(func(bs settings.BoolSetting, b bool) {
if !b {
closeSmtpPool()
closeMailer()
}
}),
)
@ -140,7 +140,7 @@ func SendBindCaptchaEmail(userID, userEmail string) error {
return errors.New("email is empty")
}
pool, err := getSmtpPool()
m, err := getMailer()
if err != nil {
return err
}
@ -163,7 +163,7 @@ func SendBindCaptchaEmail(userID, userEmail string) error {
return err
}
return pool.SendEmail(
return m.SendEmail(
[]string{userEmail},
"SyncTV Verification Code",
out.String(),
@ -204,7 +204,7 @@ func SendTestEmail(username, email string) error {
return errors.New("email is empty")
}
pool, err := getSmtpPool()
m, err := getMailer()
if err != nil {
return err
}
@ -218,7 +218,7 @@ func SendTestEmail(username, email string) error {
return err
}
return pool.SendEmail(
return m.SendEmail(
[]string{email},
"SyncTV Test Email",
out.String(),
@ -234,7 +234,7 @@ func SendSignupCaptchaEmail(email string) error {
return errors.New("email is empty")
}
pool, err := getSmtpPool()
pool, err := getMailer()
if err != nil {
return err
}
@ -312,7 +312,7 @@ func SendRetrievePasswordCaptchaEmail(userID, email, host string) error {
}
u.Path = `web/auth/reset`
pool, err := getSmtpPool()
pool, err := getMailer()
if err != nil {
return err
}

@ -11,7 +11,7 @@ import (
)
var (
smtpPool *smtp.Pool
mailer *smtp.Mailer
configChanged bool
lock sync.Mutex
)
@ -94,30 +94,11 @@ var (
lock.Lock()
defer lock.Unlock()
if smtpPool != nil {
smtpPool.SetFrom(s)
if mailer != nil {
mailer.SetFrom(s)
}
}),
)
smtpPoolSize = settings.NewInt64Setting(
"smtp_pool_size",
10,
model.SettingGroupEmail,
settings.WithValidatorInt64(func(i int64) error {
if i <= 0 {
return errors.New("smtp pool size must be greater than 0")
}
if i > 100 {
return errors.New("smtp pool size must be less than 100")
}
return nil
}),
settings.WithAfterSetInt64(func(ss settings.Int64Setting, i int64) {
lock.Lock()
defer lock.Unlock()
configChanged = true
}),
)
)
func newSmtpConfig() *smtp.Config {
@ -131,39 +112,32 @@ func newSmtpConfig() *smtp.Config {
}
}
func newSmtpPool() (*smtp.Pool, error) {
return smtp.NewSMTPPool(newSmtpConfig(), int(smtpPoolSize.Get()))
func newMailer() (*smtp.Mailer, error) {
return smtp.NewMailer(newSmtpConfig())
}
func getSmtpPool() (*smtp.Pool, error) {
func getMailer() (*smtp.Mailer, error) {
lock.Lock()
defer lock.Unlock()
if configChanged {
configChanged = false
if smtpPool != nil {
smtpPool.Close()
smtpPool = nil
}
mailer = nil
}
if smtpPool == nil {
pool, err := newSmtpPool()
if mailer == nil {
m, err := newMailer()
if err != nil {
return nil, err
}
smtpPool = pool
mailer = m
}
return smtpPool, nil
return mailer, nil
}
func closeSmtpPool() {
func closeMailer() {
lock.Lock()
defer lock.Unlock()
if smtpPool != nil {
smtpPool.Close()
smtpPool = nil
}
mailer = nil
}

@ -0,0 +1,98 @@
package smtp
import (
"crypto/tls"
"errors"
"fmt"
"gopkg.in/gomail.v2"
"strings"
)
type Config struct {
Host string
Protocol string
Username string
Password string
From string
Port uint32
}
func validateSMTPConfig(c *Config) error {
if c == nil {
return errors.New("smtp config is nil")
}
if c.Host == "" {
return errors.New("smtp host is empty")
}
if c.Port == 0 {
return errors.New("smtp port is empty")
}
if c.Username == "" {
return errors.New("smtp username is empty")
}
if c.Password == "" {
return errors.New("smtp password is empty")
}
if c.From == "" {
return errors.New("smtp from is empty")
}
return nil
}
type Mailer struct {
config *Config
}
func NewMailer(c *Config) (*Mailer, error) {
if err := validateSMTPConfig(c); err != nil {
return nil, err
}
return &Mailer{config: c}, nil
}
func newDialer(c *Config) *gomail.Dialer {
d := gomail.NewDialer(c.Host, int(c.Port), c.Username, c.Password)
switch strings.ToUpper(c.Protocol) {
case "TLS": // 587
d.TLSConfig = &tls.Config{
ServerName: c.Host,
}
case "SSL": // 465
d.SSL = true
d.TLSConfig = &tls.Config{
ServerName: c.Host,
}
case "TCP": // PlainText
d.SSL = false
d.TLSConfig = nil
default:
d.TLSConfig = &tls.Config{
ServerName: c.Host,
}
}
return d
}
func (m *Mailer) SendEmail(to []string, subject, body string, opts ...func(*gomail.Message)) error {
msg := gomail.NewMessage()
msg.SetHeader("From", m.config.From)
msg.SetHeader("To", to...)
msg.SetHeader("Subject", subject)
msg.SetBody("text/html", body)
for _, opt := range opts {
if opt != nil {
opt(msg)
}
}
dialer := newDialer(m.config)
if err := dialer.DialAndSend(msg); err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}
func (m *Mailer) SetFrom(from string) {
m.config.From = from
}

@ -1,165 +0,0 @@
package smtp
import (
"crypto/tls"
"errors"
"fmt"
"gopkg.in/gomail.v2"
"runtime"
"strings"
"sync"
)
type Config struct {
Host string
Protocol string
Username string
Password string
From string
Port uint32
}
func validateSMTPConfig(c *Config) error {
if c == nil {
return errors.New("smtp config is nil")
}
if c.Host == "" {
return errors.New("smtp host is empty")
}
if c.Port == 0 {
return errors.New("smtp port is empty")
}
if c.Username == "" {
return errors.New("smtp username is empty")
}
if c.Password == "" {
return errors.New("smtp password is empty")
}
if c.From == "" {
return errors.New("smtp from is empty")
}
return nil
}
var ErrSMTPPoolClosed = errors.New("smtp pool is closed")
type Pool struct {
c *Config
senders []*gomail.Dialer
poolCap int
active int
mu sync.Mutex
closed bool
}
func NewSMTPPool(c *Config, poolCap int) (*Pool, error) {
err := validateSMTPConfig(c)
if err != nil {
return nil, err
}
return &Pool{
senders: make([]*gomail.Dialer, 0, poolCap),
c: c,
poolCap: poolCap,
}, nil
}
func newDialer(c *Config) *gomail.Dialer {
d := gomail.NewDialer(c.Host, int(c.Port), c.Username, c.Password)
switch strings.ToUpper(c.Protocol) {
case "TLS": // 587
d.TLSConfig = &tls.Config{
ServerName: c.Host,
}
case "SSL": // 465
d.SSL = true
d.TLSConfig = &tls.Config{
ServerName: c.Host,
}
case "TCP": // PlainText
d.SSL = false
d.TLSConfig = nil
default:
d.TLSConfig = &tls.Config{
ServerName: c.Host,
}
}
return d
}
func (p *Pool) Get() (*gomail.Dialer, error) {
p.mu.Lock()
if p.closed {
p.mu.Unlock()
return nil, ErrSMTPPoolClosed
}
if len(p.senders) > 0 {
dialer := p.senders[len(p.senders)-1]
p.senders = p.senders[:len(p.senders)-1]
p.active++
p.mu.Unlock()
return dialer, nil
}
if p.active >= p.poolCap {
p.mu.Unlock()
runtime.Gosched()
return p.Get()
}
dialer := newDialer(p.c)
p.active++
p.mu.Unlock()
return dialer, nil
}
func (p *Pool) Put(dialer *gomail.Dialer) {
if dialer == nil {
return
}
p.mu.Lock()
defer p.mu.Unlock()
p.active--
if p.closed {
return
}
p.senders = append(p.senders, dialer)
}
func (p *Pool) Close() {
p.mu.Lock()
defer p.mu.Unlock()
p.closed = true
p.senders = nil
}
func (p *Pool) SendEmail(to []string, subject, body string, opts ...func(*gomail.Message)) error {
dialer, err := p.Get()
if err != nil {
return err
}
defer p.Put(dialer)
m := gomail.NewMessage()
m.SetHeader("From", p.c.From)
m.SetHeader("To", to...)
m.SetHeader("Subject", subject)
m.SetBody("text/html", body)
for _, opt := range opts {
if opt != nil {
opt(m)
}
}
if err := dialer.DialAndSend(m); err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}
func (p *Pool) SetFrom(from string) {
p.mu.Lock()
defer p.mu.Unlock()
p.c.From = from
}
Loading…
Cancel
Save