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