github.com/gophish/gophish@v0.12.2-0.20230915144530-8e7929441393/models/maillog.go (about) 1 package models 2 3 import ( 4 "crypto/rand" 5 "errors" 6 "fmt" 7 "io" 8 "math" 9 "math/big" 10 "net/mail" 11 "os" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/gophish/gomail" 17 "github.com/gophish/gophish/config" 18 log "github.com/gophish/gophish/logger" 19 "github.com/gophish/gophish/mailer" 20 ) 21 22 // MaxSendAttempts set to 8 since we exponentially backoff after each failed send 23 // attempt. This will give us a maximum send delay of 256 minutes, or about 4.2 hours. 24 var MaxSendAttempts = 8 25 26 // ErrMaxSendAttempts is thrown when the maximum number of sending attempts for a given 27 // MailLog is exceeded. 28 var ErrMaxSendAttempts = errors.New("max send attempts exceeded") 29 30 // Attachments with these file extensions have inline disposition 31 var embeddedFileExtensions = []string{".jpg", ".jpeg", ".png", ".gif"} 32 33 // MailLog is a struct that holds information about an email that is to be 34 // sent out. 35 type MailLog struct { 36 Id int64 `json:"-"` 37 UserId int64 `json:"-"` 38 CampaignId int64 `json:"campaign_id"` 39 RId string `json:"id"` 40 SendDate time.Time `json:"send_date"` 41 SendAttempt int `json:"send_attempt"` 42 Processing bool `json:"-"` 43 44 cachedCampaign *Campaign 45 } 46 47 // GenerateMailLog creates a new maillog for the given campaign and 48 // result. It sets the initial send date to match the campaign's launch date. 49 func GenerateMailLog(c *Campaign, r *Result, sendDate time.Time) error { 50 m := &MailLog{ 51 UserId: c.UserId, 52 CampaignId: c.Id, 53 RId: r.RId, 54 SendDate: sendDate, 55 } 56 return db.Save(m).Error 57 } 58 59 // Backoff sets the MailLog SendDate to be the next entry in an exponential 60 // backoff. ErrMaxRetriesExceeded is thrown if this maillog has been retried 61 // too many times. Backoff also unlocks the maillog so that it can be processed 62 // again in the future. 63 func (m *MailLog) Backoff(reason error) error { 64 r, err := GetResult(m.RId) 65 if err != nil { 66 return err 67 } 68 if m.SendAttempt == MaxSendAttempts { 69 r.HandleEmailError(ErrMaxSendAttempts) 70 return ErrMaxSendAttempts 71 } 72 // Add an error, since we had to backoff because of a 73 // temporary error of some sort during the SMTP transaction 74 m.SendAttempt++ 75 backoffDuration := math.Pow(2, float64(m.SendAttempt)) 76 m.SendDate = m.SendDate.Add(time.Minute * time.Duration(backoffDuration)) 77 err = db.Save(m).Error 78 if err != nil { 79 return err 80 } 81 err = r.HandleEmailBackoff(reason, m.SendDate) 82 if err != nil { 83 return err 84 } 85 err = m.Unlock() 86 return err 87 } 88 89 // Unlock removes the processing flag so the maillog can be processed again 90 func (m *MailLog) Unlock() error { 91 m.Processing = false 92 return db.Save(&m).Error 93 } 94 95 // Lock sets the processing flag so that other processes cannot modify the maillog 96 func (m *MailLog) Lock() error { 97 m.Processing = true 98 return db.Save(&m).Error 99 } 100 101 // Error sets the error status on the models.Result that the 102 // maillog refers to. Since MailLog errors are permanent, 103 // this action also deletes the maillog. 104 func (m *MailLog) Error(e error) error { 105 r, err := GetResult(m.RId) 106 if err != nil { 107 log.Warn(err) 108 return err 109 } 110 err = r.HandleEmailError(e) 111 if err != nil { 112 log.Warn(err) 113 return err 114 } 115 err = db.Delete(m).Error 116 return err 117 } 118 119 // Success deletes the maillog from the database and updates the underlying 120 // campaign result. 121 func (m *MailLog) Success() error { 122 r, err := GetResult(m.RId) 123 if err != nil { 124 return err 125 } 126 err = r.HandleEmailSent() 127 if err != nil { 128 return err 129 } 130 err = db.Delete(m).Error 131 return err 132 } 133 134 // GetDialer returns a dialer based on the maillog campaign's SMTP configuration 135 func (m *MailLog) GetDialer() (mailer.Dialer, error) { 136 c := m.cachedCampaign 137 if c == nil { 138 campaign, err := GetCampaignMailContext(m.CampaignId, m.UserId) 139 if err != nil { 140 return nil, err 141 } 142 c = &campaign 143 } 144 return c.SMTP.GetDialer() 145 } 146 147 // CacheCampaign allows bulk-mail workers to cache the otherwise expensive 148 // campaign lookup operation by providing a pointer to the campaign here. 149 func (m *MailLog) CacheCampaign(campaign *Campaign) error { 150 if campaign.Id != m.CampaignId { 151 return fmt.Errorf("incorrect campaign provided for caching. expected %d got %d", m.CampaignId, campaign.Id) 152 } 153 m.cachedCampaign = campaign 154 return nil 155 } 156 157 func (m *MailLog) GetSmtpFrom() (string, error) { 158 c, err := GetCampaign(m.CampaignId, m.UserId) 159 if err != nil { 160 return "", err 161 } 162 163 f, err := mail.ParseAddress(c.SMTP.FromAddress) 164 return f.Address, err 165 } 166 167 // Generate fills in the details of a gomail.Message instance with 168 // the correct headers and body from the campaign and recipient listed in 169 // the maillog. We accept the gomail.Message as an argument so that the caller 170 // can choose to re-use the message across recipients. 171 func (m *MailLog) Generate(msg *gomail.Message) error { 172 r, err := GetResult(m.RId) 173 if err != nil { 174 return err 175 } 176 c := m.cachedCampaign 177 if c == nil { 178 campaign, err := GetCampaignMailContext(m.CampaignId, m.UserId) 179 if err != nil { 180 return err 181 } 182 c = &campaign 183 } 184 185 f, err := mail.ParseAddress(c.Template.EnvelopeSender) 186 if err != nil { 187 f, err = mail.ParseAddress(c.SMTP.FromAddress) 188 if err != nil { 189 return err 190 } 191 } 192 msg.SetAddressHeader("From", f.Address, f.Name) 193 194 ptx, err := NewPhishingTemplateContext(c, r.BaseRecipient, r.RId) 195 if err != nil { 196 return err 197 } 198 199 // Add the transparency headers 200 msg.SetHeader("X-Mailer", config.ServerName) 201 if conf.ContactAddress != "" { 202 msg.SetHeader("X-Gophish-Contact", conf.ContactAddress) 203 } 204 205 // Add Message-Id header as described in RFC 2822. 206 messageID, err := m.generateMessageID() 207 if err != nil { 208 return err 209 } 210 msg.SetHeader("Message-Id", messageID) 211 212 // Parse the customHeader templates 213 for _, header := range c.SMTP.Headers { 214 key, err := ExecuteTemplate(header.Key, ptx) 215 if err != nil { 216 log.Warn(err) 217 } 218 219 value, err := ExecuteTemplate(header.Value, ptx) 220 if err != nil { 221 log.Warn(err) 222 } 223 224 // Add our header immediately 225 msg.SetHeader(key, value) 226 } 227 228 // Parse remaining templates 229 subject, err := ExecuteTemplate(c.Template.Subject, ptx) 230 231 if err != nil { 232 log.Warn(err) 233 } 234 // don't set Subject header if the subject is empty 235 if subject != "" { 236 msg.SetHeader("Subject", subject) 237 } 238 239 msg.SetHeader("To", r.FormatAddress()) 240 if c.Template.Text != "" { 241 text, err := ExecuteTemplate(c.Template.Text, ptx) 242 if err != nil { 243 log.Warn(err) 244 } 245 msg.SetBody("text/plain", text) 246 } 247 if c.Template.HTML != "" { 248 html, err := ExecuteTemplate(c.Template.HTML, ptx) 249 if err != nil { 250 log.Warn(err) 251 } 252 if c.Template.Text == "" { 253 msg.SetBody("text/html", html) 254 } else { 255 msg.AddAlternative("text/html", html) 256 } 257 } 258 // Attach the files 259 for _, a := range c.Template.Attachments { 260 addAttachment(msg, a, ptx) 261 } 262 263 return nil 264 } 265 266 // GetQueuedMailLogs returns the mail logs that are queued up for the given minute. 267 func GetQueuedMailLogs(t time.Time) ([]*MailLog, error) { 268 ms := []*MailLog{} 269 err := db.Where("send_date <= ? AND processing = ?", t, false). 270 Find(&ms).Error 271 if err != nil { 272 log.Warn(err) 273 } 274 return ms, err 275 } 276 277 // GetMailLogsByCampaign returns all of the mail logs for a given campaign. 278 func GetMailLogsByCampaign(cid int64) ([]*MailLog, error) { 279 ms := []*MailLog{} 280 err := db.Where("campaign_id = ?", cid).Find(&ms).Error 281 return ms, err 282 } 283 284 // LockMailLogs locks or unlocks a slice of maillogs for processing. 285 func LockMailLogs(ms []*MailLog, lock bool) error { 286 tx := db.Begin() 287 for i := range ms { 288 ms[i].Processing = lock 289 err := tx.Save(ms[i]).Error 290 if err != nil { 291 tx.Rollback() 292 return err 293 } 294 } 295 tx.Commit() 296 return nil 297 } 298 299 // UnlockAllMailLogs removes the processing lock for all maillogs 300 // in the database. This is intended to be called when Gophish is started 301 // so that any previously locked maillogs can resume processing. 302 func UnlockAllMailLogs() error { 303 return db.Model(&MailLog{}).Update("processing", false).Error 304 } 305 306 var maxBigInt = big.NewInt(math.MaxInt64) 307 308 // generateMessageID generates and returns a string suitable for an RFC 2822 309 // compliant Message-ID, e.g.: 310 // <1444789264909237300.3464.1819418242800517193@DESKTOP01> 311 // 312 // The following parameters are used to generate a Message-ID: 313 // - The nanoseconds since Epoch 314 // - The calling PID 315 // - A cryptographically random int64 316 // - The sending hostname 317 func (m *MailLog) generateMessageID() (string, error) { 318 t := time.Now().UnixNano() 319 pid := os.Getpid() 320 rint, err := rand.Int(rand.Reader, maxBigInt) 321 if err != nil { 322 return "", err 323 } 324 h, err := os.Hostname() 325 // If we can't get the hostname, we'll use localhost 326 if err != nil { 327 h = "localhost.localdomain" 328 } 329 msgid := fmt.Sprintf("<%d.%d.%d@%s>", t, pid, rint, h) 330 return msgid, nil 331 } 332 333 // Check if an attachment should have inline disposition based on 334 // its file extension. 335 func shouldEmbedAttachment(name string) bool { 336 ext := filepath.Ext(name) 337 for _, v := range embeddedFileExtensions { 338 if strings.EqualFold(ext, v) { 339 return true 340 } 341 } 342 return false 343 } 344 345 // Add an attachment to a gomail message, with the Content-Disposition 346 // header set to inline or attachment depending on its file extension. 347 func addAttachment(msg *gomail.Message, a Attachment, ptx PhishingTemplateContext) { 348 copyFunc := gomail.SetCopyFunc(func(c Attachment) func(w io.Writer) error { 349 return func(w io.Writer) error { 350 reader, err := a.ApplyTemplate(ptx) 351 if err != nil { 352 return err 353 } 354 _, err = io.Copy(w, reader) 355 return err 356 } 357 }(a)) 358 if shouldEmbedAttachment(a.Name) { 359 msg.Embed(a.Name, copyFunc) 360 } else { 361 msg.Attach(a.Name, copyFunc) 362 } 363 }