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 }