github.com/merlinepedra/gophish1@v0.9.0/mailer/mailer.go (about)

     1  package mailer
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/textproto"
     8  
     9  	"github.com/gophish/gomail"
    10  	log "github.com/gophish/gophish/logger"
    11  	"github.com/sirupsen/logrus"
    12  )
    13  
    14  // MaxReconnectAttempts is the maximum number of times we should reconnect to a server
    15  var MaxReconnectAttempts = 10
    16  
    17  // ErrMaxConnectAttempts is thrown when the maximum number of reconnect attempts
    18  // is reached.
    19  type ErrMaxConnectAttempts struct {
    20  	underlyingError error
    21  }
    22  
    23  // Error returns the wrapped error response
    24  func (e *ErrMaxConnectAttempts) Error() string {
    25  	errString := "Max connection attempts exceeded"
    26  	if e.underlyingError != nil {
    27  		errString = fmt.Sprintf("%s - %s", errString, e.underlyingError.Error())
    28  	}
    29  	return errString
    30  }
    31  
    32  // Mailer is an interface that defines an object used to queue and
    33  // send mailer.Mail instances.
    34  type Mailer interface {
    35  	Start(ctx context.Context)
    36  	Queue([]Mail)
    37  }
    38  
    39  // Sender exposes the common operations required for sending email.
    40  type Sender interface {
    41  	Send(from string, to []string, msg io.WriterTo) error
    42  	Close() error
    43  	Reset() error
    44  }
    45  
    46  // Dialer dials to an SMTP server and returns the SendCloser
    47  type Dialer interface {
    48  	Dial() (Sender, error)
    49  }
    50  
    51  // Mail is an interface that handles the common operations for email messages
    52  type Mail interface {
    53  	Backoff(reason error) error
    54  	Error(err error) error
    55  	Success() error
    56  	Generate(msg *gomail.Message) error
    57  	GetDialer() (Dialer, error)
    58  }
    59  
    60  // MailWorker is the worker that receives slices of emails
    61  // on a channel to send. It's assumed that every slice of emails received is meant
    62  // to be sent to the same server.
    63  type MailWorker struct {
    64  	queue chan []Mail
    65  }
    66  
    67  // NewMailWorker returns an instance of MailWorker with the mail queue
    68  // initialized.
    69  func NewMailWorker() *MailWorker {
    70  	return &MailWorker{
    71  		queue: make(chan []Mail),
    72  	}
    73  }
    74  
    75  // Start launches the mail worker to begin listening on the Queue channel
    76  // for new slices of Mail instances to process.
    77  func (mw *MailWorker) Start(ctx context.Context) {
    78  	for {
    79  		select {
    80  		case <-ctx.Done():
    81  			return
    82  		case ms := <-mw.queue:
    83  			go func(ctx context.Context, ms []Mail) {
    84  				dialer, err := ms[0].GetDialer()
    85  				if err != nil {
    86  					errorMail(err, ms)
    87  					return
    88  				}
    89  				sendMail(ctx, dialer, ms)
    90  			}(ctx, ms)
    91  		}
    92  	}
    93  }
    94  
    95  // Queue sends the provided mail to the internal queue for processing.
    96  func (mw *MailWorker) Queue(ms []Mail) {
    97  	mw.queue <- ms
    98  }
    99  
   100  // errorMail is a helper to handle erroring out a slice of Mail instances
   101  // in the case that an unrecoverable error occurs.
   102  func errorMail(err error, ms []Mail) {
   103  	for _, m := range ms {
   104  		m.Error(err)
   105  	}
   106  }
   107  
   108  // dialHost attempts to make a connection to the host specified by the Dialer.
   109  // It returns MaxReconnectAttempts if the number of connection attempts has been
   110  // exceeded.
   111  func dialHost(ctx context.Context, dialer Dialer) (Sender, error) {
   112  	sendAttempt := 0
   113  	var sender Sender
   114  	var err error
   115  	for {
   116  		select {
   117  		case <-ctx.Done():
   118  			return nil, nil
   119  		default:
   120  			break
   121  		}
   122  		sender, err = dialer.Dial()
   123  		if err == nil {
   124  			break
   125  		}
   126  		sendAttempt++
   127  		if sendAttempt == MaxReconnectAttempts {
   128  			err = &ErrMaxConnectAttempts{
   129  				underlyingError: err,
   130  			}
   131  			break
   132  		}
   133  	}
   134  	return sender, err
   135  }
   136  
   137  // sendMail attempts to send the provided Mail instances.
   138  // If the context is cancelled before all of the mail are sent,
   139  // sendMail just returns and does not modify those emails.
   140  func sendMail(ctx context.Context, dialer Dialer, ms []Mail) {
   141  	sender, err := dialHost(ctx, dialer)
   142  	if err != nil {
   143  		log.Warn(err)
   144  		errorMail(err, ms)
   145  		return
   146  	}
   147  	defer sender.Close()
   148  	message := gomail.NewMessage()
   149  	for i, m := range ms {
   150  		select {
   151  		case <-ctx.Done():
   152  			return
   153  		default:
   154  			break
   155  		}
   156  		message.Reset()
   157  		err = m.Generate(message)
   158  		if err != nil {
   159  			log.Warn(err)
   160  			m.Error(err)
   161  			continue
   162  		}
   163  		err = gomail.Send(sender, message)
   164  		if err != nil {
   165  			if te, ok := err.(*textproto.Error); ok {
   166  				switch {
   167  				// If it's a temporary error, we should backoff and try again later.
   168  				// We'll reset the connection so future messages don't incur a
   169  				// different error (see https://github.com/gophish/gophish/issues/787).
   170  				case te.Code >= 400 && te.Code <= 499:
   171  					log.WithFields(logrus.Fields{
   172  						"code":  te.Code,
   173  						"email": message.GetHeader("To")[0],
   174  					}).Warn(err)
   175  					m.Backoff(err)
   176  					sender.Reset()
   177  					continue
   178  				// Otherwise, if it's a permanent error, we shouldn't backoff this message,
   179  				// since the RFC specifies that running the same commands won't work next time.
   180  				// We should reset our sender and error this message out.
   181  				case te.Code >= 500 && te.Code <= 599:
   182  					log.WithFields(logrus.Fields{
   183  						"code":  te.Code,
   184  						"email": message.GetHeader("To")[0],
   185  					}).Warn(err)
   186  					m.Error(err)
   187  					sender.Reset()
   188  					continue
   189  				// If something else happened, let's just error out and reset the
   190  				// sender
   191  				default:
   192  					log.WithFields(logrus.Fields{
   193  						"code":  "unknown",
   194  						"email": message.GetHeader("To")[0],
   195  					}).Warn(err)
   196  					m.Error(err)
   197  					sender.Reset()
   198  					continue
   199  				}
   200  			} else {
   201  				// This likely indicates that something happened to the underlying
   202  				// connection. We'll try to reconnect and, if that fails, we'll
   203  				// error out the remaining emails.
   204  				log.WithFields(logrus.Fields{
   205  					"email": message.GetHeader("To")[0],
   206  				}).Warn(err)
   207  				origErr := err
   208  				sender, err = dialHost(ctx, dialer)
   209  				if err != nil {
   210  					errorMail(err, ms[i:])
   211  					break
   212  				}
   213  				m.Backoff(origErr)
   214  				continue
   215  			}
   216  		}
   217  		log.WithFields(logrus.Fields{
   218  			"email": message.GetHeader("To")[0],
   219  		}).Info("Email sent")
   220  		m.Success()
   221  	}
   222  }