gophish/mailer/mailer.go

221 lines
5.5 KiB
Go

package mailer
import (
"context"
"fmt"
"io"
"net/textproto"
"github.com/gophish/gomail"
log "github.com/gophish/gophish/logger"
"github.com/sirupsen/logrus"
)
// MaxReconnectAttempts is the maximum number of times we should reconnect to a server
var MaxReconnectAttempts = 10
// ErrMaxConnectAttempts is thrown when the maximum number of reconnect attempts
// is reached.
type ErrMaxConnectAttempts struct {
underlyingError error
}
// Error returns the wrapped error response
func (e *ErrMaxConnectAttempts) Error() string {
errString := "Max connection attempts exceeded"
if e.underlyingError != nil {
errString = fmt.Sprintf("%s - %s", errString, e.underlyingError.Error())
}
return errString
}
// Sender exposes the common operations required for sending email.
type Sender interface {
Send(from string, to []string, msg io.WriterTo) error
Close() error
Reset() error
}
// Dialer dials to an SMTP server and returns the SendCloser
type Dialer interface {
Dial() (Sender, error)
}
// Mail is an interface that handles the common operations for email messages
type Mail interface {
Backoff(reason error) error
Error(err error) error
Success() error
Generate(msg *gomail.Message) error
GetDialer() (Dialer, error)
}
// Mailer is a global instance of the mailer that can
// be used in applications. It is the responsibility of the application
// to call Mailer.Start()
var Mailer *MailWorker
func init() {
Mailer = NewMailWorker()
}
// MailWorker is the worker that receives slices of emails
// on a channel to send. It's assumed that every slice of emails received is meant
// to be sent to the same server.
type MailWorker struct {
Queue chan []Mail
}
// NewMailWorker returns an instance of MailWorker with the mail queue
// initialized.
func NewMailWorker() *MailWorker {
return &MailWorker{
Queue: make(chan []Mail),
}
}
// Start launches the mail worker to begin listening on the Queue channel
// for new slices of Mail instances to process.
func (mw *MailWorker) Start(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case ms := <-mw.Queue:
go func(ctx context.Context, ms []Mail) {
log.Infof("Mailer got %d mail to send", len(ms))
dialer, err := ms[0].GetDialer()
if err != nil {
errorMail(err, ms)
return
}
sendMail(ctx, dialer, ms)
}(ctx, ms)
}
}
}
// errorMail is a helper to handle erroring out a slice of Mail instances
// in the case that an unrecoverable error occurs.
func errorMail(err error, ms []Mail) {
for _, m := range ms {
m.Error(err)
}
}
// dialHost attempts to make a connection to the host specified by the Dialer.
// It returns MaxReconnectAttempts if the number of connection attempts has been
// exceeded.
func dialHost(ctx context.Context, dialer Dialer) (Sender, error) {
sendAttempt := 0
var sender Sender
var err error
for {
select {
case <-ctx.Done():
return nil, nil
default:
break
}
sender, err = dialer.Dial()
if err == nil {
break
}
sendAttempt++
if sendAttempt == MaxReconnectAttempts {
err = &ErrMaxConnectAttempts{
underlyingError: err,
}
break
}
}
return sender, err
}
// sendMail attempts to send the provided Mail instances.
// If the context is cancelled before all of the mail are sent,
// sendMail just returns and does not modify those emails.
func sendMail(ctx context.Context, dialer Dialer, ms []Mail) {
sender, err := dialHost(ctx, dialer)
if err != nil {
log.Warn(err)
errorMail(err, ms)
return
}
defer sender.Close()
message := gomail.NewMessage()
for i, m := range ms {
select {
case <-ctx.Done():
return
default:
break
}
message.Reset()
err = m.Generate(message)
if err != nil {
log.Warn(err)
m.Error(err)
continue
}
err = gomail.Send(sender, message)
if err != nil {
if te, ok := err.(*textproto.Error); ok {
switch {
// If it's a temporary error, we should backoff and try again later.
// We'll reset the connection so future messages don't incur a
// different error (see https://github.com/gophish/gophish/issues/787).
case te.Code >= 400 && te.Code <= 499:
log.WithFields(logrus.Fields{
"code": te.Code,
"email": message.GetHeader("To")[0],
}).Warn(err)
m.Backoff(err)
sender.Reset()
continue
// Otherwise, if it's a permanent error, we shouldn't backoff this message,
// since the RFC specifies that running the same commands won't work next time.
// We should reset our sender and error this message out.
case te.Code >= 500 && te.Code <= 599:
log.WithFields(logrus.Fields{
"code": te.Code,
"email": message.GetHeader("To")[0],
}).Warn(err)
m.Error(err)
sender.Reset()
continue
// If something else happened, let's just error out and reset the
// sender
default:
log.WithFields(logrus.Fields{
"code": "unknown",
"email": message.GetHeader("To")[0],
}).Warn(err)
m.Error(err)
sender.Reset()
continue
}
} else {
// This likely indicates that something happened to the underlying
// connection. We'll try to reconnect and, if that fails, we'll
// error out the remaining emails.
log.WithFields(logrus.Fields{
"email": message.GetHeader("To")[0],
}).Warn(err)
origErr := err
sender, err = dialHost(ctx, dialer)
if err != nil {
errorMail(err, ms[i:])
break
}
m.Backoff(origErr)
continue
}
}
log.WithFields(logrus.Fields{
"email": message.GetHeader("To")[0],
}).Info("Email sent")
m.Success()
}
}