Skip to content

Commit

Permalink
fix(utils/smtp): Refactor SMTP pool using gomail.v2 to resolve STARTT…
Browse files Browse the repository at this point in the history
…LS compatibility issues

- Refactor SMTP connection pool implementation
- Optimize pool resource management
  • Loading branch information
Ogannesson committed Dec 9, 2024
1 parent f93d12c commit 8aaf1f1
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 76 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ require (
golang.org/x/text v0.20.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
modernc.org/libc v1.61.2 // indirect
modernc.org/mathutil v1.6.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -522,10 +522,14 @@ google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWyw
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
137 changes: 61 additions & 76 deletions utils/smtp/smtpool.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package smtp

import (
"crypto/tls"
"errors"
"fmt"
"gopkg.in/gomail.v2"
"runtime"
"strings"
"sync"

"github.com/emersion/go-sasl"
smtp "github.com/emersion/go-smtp"
)

type Config struct {
Expand Down Expand Up @@ -42,38 +41,11 @@ func validateSMTPConfig(c *Config) error {
return nil
}

func newSMTPClient(c *Config) (*smtp.Client, error) {
var (
cli *smtp.Client
err error
)

switch strings.ToUpper(c.Protocol) {
case "TLS": // 587
cli, err = smtp.DialStartTLS(fmt.Sprintf("%s:%d", c.Host, c.Port), nil)
case "SSL": // 465
cli, err = smtp.DialTLS(fmt.Sprintf("%s:%d", c.Host, c.Port), nil)
default:
cli, err = smtp.Dial(fmt.Sprintf("%s:%d", c.Host, c.Port))
}
if err != nil {
return nil, fmt.Errorf("dial smtp server failed: %w", err)
}

err = cli.Auth(sasl.NewLoginClient(c.Username, c.Password))
if err != nil {
cli.Close()
return nil, fmt.Errorf("auth failed: %w", err)
}

return cli, nil
}

var ErrSMTPPoolClosed = errors.New("smtp pool is closed")

type Pool struct {
c *Config
clients []*smtp.Client
senders []*gomail.Dialer
poolCap int
active int
mu sync.Mutex
Expand All @@ -86,95 +58,108 @@ func NewSMTPPool(c *Config, poolCap int) (*Pool, error) {
return nil, err
}
return &Pool{
clients: make([]*smtp.Client, 0, poolCap),
senders: make([]*gomail.Dialer, 0, poolCap),
c: c,
poolCap: poolCap,
}, nil
}

func (p *Pool) Get() (*smtp.Client, error) {
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.clients) > 0 {
cli := p.clients[len(p.clients)-1]
p.clients = p.clients[:len(p.clients)-1]
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()
if cli.Noop() != nil {
cli.Close()
p.mu.Lock()
p.active--
p.mu.Unlock()
return p.Get()
}
return cli, nil
return dialer, nil
}

if p.active >= p.poolCap {
p.mu.Unlock()
runtime.Gosched()
return p.Get()
}

cli, err := newSMTPClient(p.c)
if err != nil {
p.mu.Unlock()
return nil, err
}

dialer := newDialer(p.c)
p.active++
p.mu.Unlock()
return cli, nil
return dialer, nil
}

func (p *Pool) Put(cli *smtp.Client) {
if cli == nil {
func (p *Pool) Put(dialer *gomail.Dialer) {
if dialer == nil {
return
}

noopErr := cli.Noop()

p.mu.Lock()
defer p.mu.Unlock()

p.active--

if p.closed || noopErr != nil {
cli.Close()
if p.closed {
return
}

p.clients = append(p.clients, cli)
p.senders = append(p.senders, dialer)
}

func (p *Pool) Close() {
p.mu.Lock()
defer p.mu.Unlock()

p.closed = true

for _, cli := range p.clients {
cli.Close()
}
p.clients = nil
p.senders = nil
}

func (p *Pool) SendEmail(to []string, subject, body string, opts ...FormatMailOption) error {
cli, err := p.Get()
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(cli)
return SendEmail(cli, p.c.From, to, subject, body, opts...)
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
}

0 comments on commit 8aaf1f1

Please sign in to comment.