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