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 }