Skip to content

Commit

Permalink
Refactor the smtp provider.
Browse files Browse the repository at this point in the history
- Replace SMTP pool lib with `knadh/smtppool`.
- Replace raw JSON config parsing with a config struct.
- Add support for TLS/STARTTLS and multiple auth modes.
  • Loading branch information
knadh committed Jan 10, 2022
1 parent ebec1a9 commit 94e8c93
Showing 1 changed file with 75 additions and 62 deletions.
137 changes: 75 additions & 62 deletions internal/providers/smtp/smtp.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package main
package smtp

import (
"encoding/json"
"crypto/tls"
"errors"
"fmt"
"net/smtp"
"regexp"
"time"

"github.com/jordan-wright/email"
"github.com/knadh/otpgateway/v3/internal/models"
"github.com/knadh/smtppool"
)

const (
Expand All @@ -24,129 +24,142 @@ const (
// http://www.golangprograms.com/regular-expression-to-validate-email-address.html
var reMail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

// cfg represents an SMTP server's credentials.
type cfg struct {
Host string `json:"Host"`
Port int `json:"Port"`
AuthProtocol string `json:"AuthProtocol"`
User string `json:"User"`
Password string `json:"Password"`
FromEmail string `json:"FromEmail"`
SendTimeout int `json:"SendTimeout"`
MaxConns int `json:"MaxConns"`
// Config represents an SMTP server's credentials.
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
AuthProtocol string `json:"auth_protocol"`
Username string `json:"username"`
Password string `json:"password"`
FromEmail string `json:"from_email"`
Timeout time.Duration `json:"timeout"`
MaxConns int `json:"max_conns"`

// STARTTLS or TLS.
TLSType string `json:"tls_type"`
TLSSkipVerify bool `json:"tls_skip_verify"`
}

type emailer struct {
cfg cfg
timeout time.Duration
mailer *email.Pool
// SMTP is a generic SMTP e-mail provider.
type SMTP struct {
cfg Config
p *smtppool.Pool
}

// New creates and returns an e-mail Provider backend.
func New(jsonCfg []byte) (interface{}, error) {
var c cfg
if err := json.Unmarshal(jsonCfg, &c); err != nil {
return nil, fmt.Errorf("error reading config: %v", err)
func New(cfg Config) (*SMTP, error) {
if cfg.FromEmail == "" {
cfg.FromEmail = "otp@localhost"
}

if c.Host == "" {
c.Host = "127.0.0.1"
}
if c.Port == 0 {
c.Port = 25
}
if c.MaxConns == 0 {
c.MaxConns = 1
// Initialize the SMTP mailer.
var auth smtp.Auth
switch cfg.AuthProtocol {
case "login":
auth = &smtppool.LoginAuth{Username: cfg.Username, Password: cfg.Password}
case "cram":
auth = smtp.CRAMMD5Auth(cfg.Username, cfg.Password)
case "plain":
auth = smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
case "", "none":
default:
return nil, fmt.Errorf("unknown SMTP auth type '%s'", cfg.AuthProtocol)
}
if c.FromEmail == "" {
c.FromEmail = "otp@localhost"

opt := smtppool.Opt{
Host: cfg.Host,
Port: cfg.Port,
MaxConns: cfg.MaxConns,
IdleTimeout: time.Second * 10,
PoolWaitTimeout: cfg.Timeout,
Auth: auth,
}

// Initialize the SMTP mailer.
var auth smtp.Auth
if c.AuthProtocol == "cram" {
auth = smtp.CRAMMD5Auth(c.User, c.Password)
} else {
auth = smtp.PlainAuth("", c.User, c.Password, c.Host)
// TLS config.
if cfg.TLSType != "none" {
opt.TLSConfig = &tls.Config{}
if cfg.TLSSkipVerify {
opt.TLSConfig.InsecureSkipVerify = cfg.TLSSkipVerify
} else {
opt.TLSConfig.ServerName = cfg.Host
}

// SSL/TLS, not cfg.
if cfg.TLSType == "TLS" {
opt.SSL = true
}
}

pool, err := email.NewPool(fmt.Sprintf("%s:%d", c.Host, c.Port), c.MaxConns, auth)
pool, err := smtppool.New(opt)
if err != nil {
return nil, err
}

// Push timeout.
t := 5
if c.SendTimeout == 0 {
t = c.SendTimeout
}

return &emailer{
mailer: pool,
cfg: c,
timeout: time.Second * time.Duration(t),
return &SMTP{
p: pool,
cfg: cfg,
}, nil
}

// ID returns the Provider's ID.
func (e *emailer) ID() string {
func (s *SMTP) ID() string {
return providerID
}

// ChannelName returns the e-mail Provider's name.
func (e *emailer) ChannelName() string {
func (s *SMTP) ChannelName() string {
return channelName
}

// ChannelDesc returns help text for the e-mail verification Provider.
func (e *emailer) ChannelDesc() string {
func (s *SMTP) ChannelDesc() string {
return fmt.Sprintf(`
We've e-mailed you a %d digit code.
A %d digit code has been e-mailed to you.
Please check your e-mail and enter the code here
to complete the verification.`, maxOTPlen)
}

// AddressName returns the e-mail Provider's address name.
func (e *emailer) AddressName() string {
func (s *SMTP) AddressName() string {
return addressName
}

// AddressDesc returns the help text that is shown to the end users when
// they're asked to enter their addresses (eg: e-mail or phone), if the OTP
// registered without an address.
func (e *emailer) AddressDesc() string {
func (s *SMTP) AddressDesc() string {
return `Please enter the e-mail ID you want to verify`
}

// ValidateAddress "validates" an e-mail address.
func (e *emailer) ValidateAddress(to string) error {
func (s *SMTP) ValidateAddress(to string) error {
if !reMail.MatchString(to) {
return errors.New("invalid e-mail address")
}
return nil
}

// Push pushes an e-mail to the SMTP server.
func (e *emailer) Push(otp models.OTP, subject string, m []byte) error {
return e.mailer.Send(&email.Email{
From: e.cfg.FromEmail,
func (s *SMTP) Push(otp models.OTP, subject string, m []byte) error {
return s.p.Send(smtppool.Email{
From: s.cfg.FromEmail,
To: []string{otp.To},
Subject: subject,
HTML: m,
}, e.timeout)
})
}

// MaxAddressLen returns the maximum allowed length of the e-mail address.
func (e *emailer) MaxAddressLen() int {
func (s *SMTP) MaxAddressLen() int {
return maxAddressLen
}

// MaxOTPLen returns the maximum allowed length of the OTP value.
func (e *emailer) MaxOTPLen() int {
func (s *SMTP) MaxOTPLen() int {
return maxOTPlen
}

// MaxBodyLen returns the max permitted body size.
func (e *emailer) MaxBodyLen() int {
func (s *SMTP) MaxBodyLen() int {
return maxBodyLen
}

0 comments on commit 94e8c93

Please sign in to comment.