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 }