github.com/gophish/gophish@v0.12.2-0.20230915144530-8e7929441393/models/campaign.go (about) 1 package models 2 3 import ( 4 "errors" 5 "net/url" 6 "time" 7 8 log "github.com/gophish/gophish/logger" 9 "github.com/gophish/gophish/webhook" 10 "github.com/jinzhu/gorm" 11 "github.com/sirupsen/logrus" 12 ) 13 14 // Campaign is a struct representing a created campaign 15 type Campaign struct { 16 Id int64 `json:"id"` 17 UserId int64 `json:"-"` 18 Name string `json:"name" sql:"not null"` 19 CreatedDate time.Time `json:"created_date"` 20 LaunchDate time.Time `json:"launch_date"` 21 SendByDate time.Time `json:"send_by_date"` 22 CompletedDate time.Time `json:"completed_date"` 23 TemplateId int64 `json:"-"` 24 Template Template `json:"template"` 25 PageId int64 `json:"-"` 26 Page Page `json:"page"` 27 Status string `json:"status"` 28 Results []Result `json:"results,omitempty"` 29 Groups []Group `json:"groups,omitempty"` 30 Events []Event `json:"timeline,omitempty"` 31 SMTPId int64 `json:"-"` 32 SMTP SMTP `json:"smtp"` 33 URL string `json:"url"` 34 } 35 36 // CampaignResults is a struct representing the results from a campaign 37 type CampaignResults struct { 38 Id int64 `json:"id"` 39 Name string `json:"name"` 40 Status string `json:"status"` 41 Results []Result `json:"results,omitempty"` 42 Events []Event `json:"timeline,omitempty"` 43 } 44 45 // CampaignSummaries is a struct representing the overview of campaigns 46 type CampaignSummaries struct { 47 Total int64 `json:"total"` 48 Campaigns []CampaignSummary `json:"campaigns"` 49 } 50 51 // CampaignSummary is a struct representing the overview of a single camaign 52 type CampaignSummary struct { 53 Id int64 `json:"id"` 54 CreatedDate time.Time `json:"created_date"` 55 LaunchDate time.Time `json:"launch_date"` 56 SendByDate time.Time `json:"send_by_date"` 57 CompletedDate time.Time `json:"completed_date"` 58 Status string `json:"status"` 59 Name string `json:"name"` 60 Stats CampaignStats `json:"stats"` 61 } 62 63 // CampaignStats is a struct representing the statistics for a single campaign 64 type CampaignStats struct { 65 Total int64 `json:"total"` 66 EmailsSent int64 `json:"sent"` 67 OpenedEmail int64 `json:"opened"` 68 ClickedLink int64 `json:"clicked"` 69 SubmittedData int64 `json:"submitted_data"` 70 EmailReported int64 `json:"email_reported"` 71 Error int64 `json:"error"` 72 } 73 74 // Event contains the fields for an event 75 // that occurs during the campaign 76 type Event struct { 77 Id int64 `json:"-"` 78 CampaignId int64 `json:"campaign_id"` 79 Email string `json:"email"` 80 Time time.Time `json:"time"` 81 Message string `json:"message"` 82 Details string `json:"details"` 83 } 84 85 // EventDetails is a struct that wraps common attributes we want to store 86 // in an event 87 type EventDetails struct { 88 Payload url.Values `json:"payload"` 89 Browser map[string]string `json:"browser"` 90 } 91 92 // EventError is a struct that wraps an error that occurs when sending an 93 // email to a recipient 94 type EventError struct { 95 Error string `json:"error"` 96 } 97 98 // ErrCampaignNameNotSpecified indicates there was no template given by the user 99 var ErrCampaignNameNotSpecified = errors.New("Campaign name not specified") 100 101 // ErrGroupNotSpecified indicates there was no template given by the user 102 var ErrGroupNotSpecified = errors.New("No groups specified") 103 104 // ErrTemplateNotSpecified indicates there was no template given by the user 105 var ErrTemplateNotSpecified = errors.New("No email template specified") 106 107 // ErrPageNotSpecified indicates a landing page was not provided for the campaign 108 var ErrPageNotSpecified = errors.New("No landing page specified") 109 110 // ErrSMTPNotSpecified indicates a sending profile was not provided for the campaign 111 var ErrSMTPNotSpecified = errors.New("No sending profile specified") 112 113 // ErrTemplateNotFound indicates the template specified does not exist in the database 114 var ErrTemplateNotFound = errors.New("Template not found") 115 116 // ErrGroupNotFound indicates a group specified by the user does not exist in the database 117 var ErrGroupNotFound = errors.New("Group not found") 118 119 // ErrPageNotFound indicates a page specified by the user does not exist in the database 120 var ErrPageNotFound = errors.New("Page not found") 121 122 // ErrSMTPNotFound indicates a sending profile specified by the user does not exist in the database 123 var ErrSMTPNotFound = errors.New("Sending profile not found") 124 125 // ErrInvalidSendByDate indicates that the user specified a send by date that occurs before the 126 // launch date 127 var ErrInvalidSendByDate = errors.New("The launch date must be before the \"send emails by\" date") 128 129 // RecipientParameter is the URL parameter that points to the result ID for a recipient. 130 const RecipientParameter = "rid" 131 132 // Validate checks to make sure there are no invalid fields in a submitted campaign 133 func (c *Campaign) Validate() error { 134 switch { 135 case c.Name == "": 136 return ErrCampaignNameNotSpecified 137 case len(c.Groups) == 0: 138 return ErrGroupNotSpecified 139 case c.Template.Name == "": 140 return ErrTemplateNotSpecified 141 case c.Page.Name == "": 142 return ErrPageNotSpecified 143 case c.SMTP.Name == "": 144 return ErrSMTPNotSpecified 145 case !c.SendByDate.IsZero() && !c.LaunchDate.IsZero() && c.SendByDate.Before(c.LaunchDate): 146 return ErrInvalidSendByDate 147 } 148 return nil 149 } 150 151 // UpdateStatus changes the campaign status appropriately 152 func (c *Campaign) UpdateStatus(s string) error { 153 // This could be made simpler, but I think there's a bug in gorm 154 return db.Table("campaigns").Where("id=?", c.Id).Update("status", s).Error 155 } 156 157 // AddEvent creates a new campaign event in the database 158 func AddEvent(e *Event, campaignID int64) error { 159 e.CampaignId = campaignID 160 e.Time = time.Now().UTC() 161 162 whs, err := GetActiveWebhooks() 163 if err == nil { 164 whEndPoints := []webhook.EndPoint{} 165 for _, wh := range whs { 166 whEndPoints = append(whEndPoints, webhook.EndPoint{ 167 URL: wh.URL, 168 Secret: wh.Secret, 169 }) 170 } 171 webhook.SendAll(whEndPoints, e) 172 } else { 173 log.Errorf("error getting active webhooks: %v", err) 174 } 175 176 return db.Save(e).Error 177 } 178 179 // getDetails retrieves the related attributes of the campaign 180 // from the database. If the Events and the Results are not available, 181 // an error is returned. Otherwise, the attribute name is set to [Deleted], 182 // indicating the user deleted the attribute (template, smtp, etc.) 183 func (c *Campaign) getDetails() error { 184 err := db.Model(c).Related(&c.Results).Error 185 if err != nil { 186 log.Warnf("%s: results not found for campaign", err) 187 return err 188 } 189 err = db.Model(c).Related(&c.Events).Error 190 if err != nil { 191 log.Warnf("%s: events not found for campaign", err) 192 return err 193 } 194 err = db.Table("templates").Where("id=?", c.TemplateId).Find(&c.Template).Error 195 if err != nil { 196 if err != gorm.ErrRecordNotFound { 197 return err 198 } 199 c.Template = Template{Name: "[Deleted]"} 200 log.Warnf("%s: template not found for campaign", err) 201 } 202 err = db.Where("template_id=?", c.Template.Id).Find(&c.Template.Attachments).Error 203 if err != nil && err != gorm.ErrRecordNotFound { 204 log.Warn(err) 205 return err 206 } 207 err = db.Table("pages").Where("id=?", c.PageId).Find(&c.Page).Error 208 if err != nil { 209 if err != gorm.ErrRecordNotFound { 210 return err 211 } 212 c.Page = Page{Name: "[Deleted]"} 213 log.Warnf("%s: page not found for campaign", err) 214 } 215 err = db.Table("smtp").Where("id=?", c.SMTPId).Find(&c.SMTP).Error 216 if err != nil { 217 // Check if the SMTP was deleted 218 if err != gorm.ErrRecordNotFound { 219 return err 220 } 221 c.SMTP = SMTP{Name: "[Deleted]"} 222 log.Warnf("%s: sending profile not found for campaign", err) 223 } 224 err = db.Where("smtp_id=?", c.SMTP.Id).Find(&c.SMTP.Headers).Error 225 if err != nil && err != gorm.ErrRecordNotFound { 226 log.Warn(err) 227 return err 228 } 229 return nil 230 } 231 232 // getBaseURL returns the Campaign's configured URL. 233 // This is used to implement the TemplateContext interface. 234 func (c *Campaign) getBaseURL() string { 235 return c.URL 236 } 237 238 // getFromAddress returns the Campaign's configured SMTP "From" address. 239 // This is used to implement the TemplateContext interface. 240 func (c *Campaign) getFromAddress() string { 241 return c.SMTP.FromAddress 242 } 243 244 // generateSendDate creates a sendDate 245 func (c *Campaign) generateSendDate(idx int, totalRecipients int) time.Time { 246 // If no send date is specified, just return the launch date 247 if c.SendByDate.IsZero() || c.SendByDate.Equal(c.LaunchDate) { 248 return c.LaunchDate 249 } 250 // Otherwise, we can calculate the range of minutes to send emails 251 // (since we only poll once per minute) 252 totalMinutes := c.SendByDate.Sub(c.LaunchDate).Minutes() 253 254 // Next, we can determine how many minutes should elapse between emails 255 minutesPerEmail := totalMinutes / float64(totalRecipients) 256 257 // Then, we can calculate the offset for this particular email 258 offset := int(minutesPerEmail * float64(idx)) 259 260 // Finally, we can just add this offset to the launch date to determine 261 // when the email should be sent 262 return c.LaunchDate.Add(time.Duration(offset) * time.Minute) 263 } 264 265 // getCampaignStats returns a CampaignStats object for the campaign with the given campaign ID. 266 // It also backfills numbers as appropriate with a running total, so that the values are aggregated. 267 func getCampaignStats(cid int64) (CampaignStats, error) { 268 s := CampaignStats{} 269 query := db.Table("results").Where("campaign_id = ?", cid) 270 err := query.Count(&s.Total).Error 271 if err != nil { 272 return s, err 273 } 274 query.Where("status=?", EventDataSubmit).Count(&s.SubmittedData) 275 if err != nil { 276 return s, err 277 } 278 query.Where("status=?", EventClicked).Count(&s.ClickedLink) 279 if err != nil { 280 return s, err 281 } 282 query.Where("reported=?", true).Count(&s.EmailReported) 283 if err != nil { 284 return s, err 285 } 286 // Every submitted data event implies they clicked the link 287 s.ClickedLink += s.SubmittedData 288 err = query.Where("status=?", EventOpened).Count(&s.OpenedEmail).Error 289 if err != nil { 290 return s, err 291 } 292 // Every clicked link event implies they opened the email 293 s.OpenedEmail += s.ClickedLink 294 err = query.Where("status=?", EventSent).Count(&s.EmailsSent).Error 295 if err != nil { 296 return s, err 297 } 298 // Every opened email event implies the email was sent 299 s.EmailsSent += s.OpenedEmail 300 err = query.Where("status=?", Error).Count(&s.Error).Error 301 return s, err 302 } 303 304 // GetCampaigns returns the campaigns owned by the given user. 305 func GetCampaigns(uid int64) ([]Campaign, error) { 306 cs := []Campaign{} 307 err := db.Model(&User{Id: uid}).Related(&cs).Error 308 if err != nil { 309 log.Error(err) 310 } 311 for i := range cs { 312 err = cs[i].getDetails() 313 if err != nil { 314 log.Error(err) 315 } 316 } 317 return cs, err 318 } 319 320 // GetCampaignSummaries gets the summary objects for all the campaigns 321 // owned by the current user 322 func GetCampaignSummaries(uid int64) (CampaignSummaries, error) { 323 overview := CampaignSummaries{} 324 cs := []CampaignSummary{} 325 // Get the basic campaign information 326 query := db.Table("campaigns").Where("user_id = ?", uid) 327 query = query.Select("id, name, created_date, launch_date, send_by_date, completed_date, status") 328 err := query.Scan(&cs).Error 329 if err != nil { 330 log.Error(err) 331 return overview, err 332 } 333 for i := range cs { 334 s, err := getCampaignStats(cs[i].Id) 335 if err != nil { 336 log.Error(err) 337 return overview, err 338 } 339 cs[i].Stats = s 340 } 341 overview.Total = int64(len(cs)) 342 overview.Campaigns = cs 343 return overview, nil 344 } 345 346 // GetCampaignSummary gets the summary object for a campaign specified by the campaign ID 347 func GetCampaignSummary(id int64, uid int64) (CampaignSummary, error) { 348 cs := CampaignSummary{} 349 query := db.Table("campaigns").Where("user_id = ? AND id = ?", uid, id) 350 query = query.Select("id, name, created_date, launch_date, send_by_date, completed_date, status") 351 err := query.Scan(&cs).Error 352 if err != nil { 353 log.Error(err) 354 return cs, err 355 } 356 s, err := getCampaignStats(cs.Id) 357 if err != nil { 358 log.Error(err) 359 return cs, err 360 } 361 cs.Stats = s 362 return cs, nil 363 } 364 365 // GetCampaignMailContext returns a campaign object with just the relevant 366 // data needed to generate and send emails. This includes the top-level 367 // metadata, the template, and the sending profile. 368 // 369 // This should only ever be used if you specifically want this lightweight 370 // context, since it returns a non-standard campaign object. 371 // ref: #1726 372 func GetCampaignMailContext(id int64, uid int64) (Campaign, error) { 373 c := Campaign{} 374 err := db.Where("id = ?", id).Where("user_id = ?", uid).Find(&c).Error 375 if err != nil { 376 return c, err 377 } 378 err = db.Table("smtp").Where("id=?", c.SMTPId).Find(&c.SMTP).Error 379 if err != nil { 380 return c, err 381 } 382 err = db.Where("smtp_id=?", c.SMTP.Id).Find(&c.SMTP.Headers).Error 383 if err != nil && err != gorm.ErrRecordNotFound { 384 return c, err 385 } 386 err = db.Table("templates").Where("id=?", c.TemplateId).Find(&c.Template).Error 387 if err != nil { 388 return c, err 389 } 390 err = db.Where("template_id=?", c.Template.Id).Find(&c.Template.Attachments).Error 391 if err != nil && err != gorm.ErrRecordNotFound { 392 return c, err 393 } 394 return c, nil 395 } 396 397 // GetCampaign returns the campaign, if it exists, specified by the given id and user_id. 398 func GetCampaign(id int64, uid int64) (Campaign, error) { 399 c := Campaign{} 400 err := db.Where("id = ?", id).Where("user_id = ?", uid).Find(&c).Error 401 if err != nil { 402 log.Errorf("%s: campaign not found", err) 403 return c, err 404 } 405 err = c.getDetails() 406 return c, err 407 } 408 409 // GetCampaignResults returns just the campaign results for the given campaign 410 func GetCampaignResults(id int64, uid int64) (CampaignResults, error) { 411 cr := CampaignResults{} 412 err := db.Table("campaigns").Where("id=? and user_id=?", id, uid).Find(&cr).Error 413 if err != nil { 414 log.WithFields(logrus.Fields{ 415 "campaign_id": id, 416 "error": err, 417 }).Error(err) 418 return cr, err 419 } 420 err = db.Table("results").Where("campaign_id=? and user_id=?", cr.Id, uid).Find(&cr.Results).Error 421 if err != nil { 422 log.Errorf("%s: results not found for campaign", err) 423 return cr, err 424 } 425 err = db.Table("events").Where("campaign_id=?", cr.Id).Find(&cr.Events).Error 426 if err != nil { 427 log.Errorf("%s: events not found for campaign", err) 428 return cr, err 429 } 430 return cr, err 431 } 432 433 // GetQueuedCampaigns returns the campaigns that are queued up for this given minute 434 func GetQueuedCampaigns(t time.Time) ([]Campaign, error) { 435 cs := []Campaign{} 436 err := db.Where("launch_date <= ?", t). 437 Where("status = ?", CampaignQueued).Find(&cs).Error 438 if err != nil { 439 log.Error(err) 440 } 441 log.Infof("Found %d Campaigns to run\n", len(cs)) 442 for i := range cs { 443 err = cs[i].getDetails() 444 if err != nil { 445 log.Error(err) 446 } 447 } 448 return cs, err 449 } 450 451 // PostCampaign inserts a campaign and all associated records into the database. 452 func PostCampaign(c *Campaign, uid int64) error { 453 err := c.Validate() 454 if err != nil { 455 return err 456 } 457 // Fill in the details 458 c.UserId = uid 459 c.CreatedDate = time.Now().UTC() 460 c.CompletedDate = time.Time{} 461 c.Status = CampaignQueued 462 if c.LaunchDate.IsZero() { 463 c.LaunchDate = c.CreatedDate 464 } else { 465 c.LaunchDate = c.LaunchDate.UTC() 466 } 467 if !c.SendByDate.IsZero() { 468 c.SendByDate = c.SendByDate.UTC() 469 } 470 if c.LaunchDate.Before(c.CreatedDate) || c.LaunchDate.Equal(c.CreatedDate) { 471 c.Status = CampaignInProgress 472 } 473 // Check to make sure all the groups already exist 474 // Also, later we'll need to know the total number of recipients (counting 475 // duplicates is ok for now), so we'll do that here to save a loop. 476 totalRecipients := 0 477 for i, g := range c.Groups { 478 c.Groups[i], err = GetGroupByName(g.Name, uid) 479 if err == gorm.ErrRecordNotFound { 480 log.WithFields(logrus.Fields{ 481 "group": g.Name, 482 }).Error("Group does not exist") 483 return ErrGroupNotFound 484 } else if err != nil { 485 log.Error(err) 486 return err 487 } 488 totalRecipients += len(c.Groups[i].Targets) 489 } 490 // Check to make sure the template exists 491 t, err := GetTemplateByName(c.Template.Name, uid) 492 if err == gorm.ErrRecordNotFound { 493 log.WithFields(logrus.Fields{ 494 "template": c.Template.Name, 495 }).Error("Template does not exist") 496 return ErrTemplateNotFound 497 } else if err != nil { 498 log.Error(err) 499 return err 500 } 501 c.Template = t 502 c.TemplateId = t.Id 503 // Check to make sure the page exists 504 p, err := GetPageByName(c.Page.Name, uid) 505 if err == gorm.ErrRecordNotFound { 506 log.WithFields(logrus.Fields{ 507 "page": c.Page.Name, 508 }).Error("Page does not exist") 509 return ErrPageNotFound 510 } else if err != nil { 511 log.Error(err) 512 return err 513 } 514 c.Page = p 515 c.PageId = p.Id 516 // Check to make sure the sending profile exists 517 s, err := GetSMTPByName(c.SMTP.Name, uid) 518 if err == gorm.ErrRecordNotFound { 519 log.WithFields(logrus.Fields{ 520 "smtp": c.SMTP.Name, 521 }).Error("Sending profile does not exist") 522 return ErrSMTPNotFound 523 } else if err != nil { 524 log.Error(err) 525 return err 526 } 527 c.SMTP = s 528 c.SMTPId = s.Id 529 // Insert into the DB 530 err = db.Save(c).Error 531 if err != nil { 532 log.Error(err) 533 return err 534 } 535 err = AddEvent(&Event{Message: "Campaign Created"}, c.Id) 536 if err != nil { 537 log.Error(err) 538 } 539 // Insert all the results 540 resultMap := make(map[string]bool) 541 recipientIndex := 0 542 tx := db.Begin() 543 for _, g := range c.Groups { 544 // Insert a result for each target in the group 545 for _, t := range g.Targets { 546 // Remove duplicate results - we should only 547 // send emails to unique email addresses. 548 if _, ok := resultMap[t.Email]; ok { 549 continue 550 } 551 resultMap[t.Email] = true 552 sendDate := c.generateSendDate(recipientIndex, totalRecipients) 553 r := &Result{ 554 BaseRecipient: BaseRecipient{ 555 Email: t.Email, 556 Position: t.Position, 557 FirstName: t.FirstName, 558 LastName: t.LastName, 559 }, 560 Status: StatusScheduled, 561 CampaignId: c.Id, 562 UserId: c.UserId, 563 SendDate: sendDate, 564 Reported: false, 565 ModifiedDate: c.CreatedDate, 566 } 567 err = r.GenerateId(tx) 568 if err != nil { 569 log.Error(err) 570 tx.Rollback() 571 return err 572 } 573 processing := false 574 if r.SendDate.Before(c.CreatedDate) || r.SendDate.Equal(c.CreatedDate) { 575 r.Status = StatusSending 576 processing = true 577 } 578 err = tx.Save(r).Error 579 if err != nil { 580 log.WithFields(logrus.Fields{ 581 "email": t.Email, 582 }).Errorf("error creating result: %v", err) 583 tx.Rollback() 584 return err 585 } 586 c.Results = append(c.Results, *r) 587 log.WithFields(logrus.Fields{ 588 "email": r.Email, 589 "send_date": sendDate, 590 }).Debug("creating maillog") 591 m := &MailLog{ 592 UserId: c.UserId, 593 CampaignId: c.Id, 594 RId: r.RId, 595 SendDate: sendDate, 596 Processing: processing, 597 } 598 err = tx.Save(m).Error 599 if err != nil { 600 log.WithFields(logrus.Fields{ 601 "email": t.Email, 602 }).Errorf("error creating maillog entry: %v", err) 603 tx.Rollback() 604 return err 605 } 606 recipientIndex++ 607 } 608 } 609 return tx.Commit().Error 610 } 611 612 //DeleteCampaign deletes the specified campaign 613 func DeleteCampaign(id int64) error { 614 log.WithFields(logrus.Fields{ 615 "campaign_id": id, 616 }).Info("Deleting campaign") 617 // Delete all the campaign results 618 err := db.Where("campaign_id=?", id).Delete(&Result{}).Error 619 if err != nil { 620 log.Error(err) 621 return err 622 } 623 err = db.Where("campaign_id=?", id).Delete(&Event{}).Error 624 if err != nil { 625 log.Error(err) 626 return err 627 } 628 err = db.Where("campaign_id=?", id).Delete(&MailLog{}).Error 629 if err != nil { 630 log.Error(err) 631 return err 632 } 633 // Delete the campaign 634 err = db.Delete(&Campaign{Id: id}).Error 635 if err != nil { 636 log.Error(err) 637 } 638 return err 639 } 640 641 // CompleteCampaign effectively "ends" a campaign. 642 // Any future emails clicked will return a simple "404" page. 643 func CompleteCampaign(id int64, uid int64) error { 644 log.WithFields(logrus.Fields{ 645 "campaign_id": id, 646 }).Info("Marking campaign as complete") 647 c, err := GetCampaign(id, uid) 648 if err != nil { 649 return err 650 } 651 // Delete any maillogs still set to be sent out, preventing future emails 652 err = db.Where("campaign_id=?", id).Delete(&MailLog{}).Error 653 if err != nil { 654 log.Error(err) 655 return err 656 } 657 // Don't overwrite original completed time 658 if c.Status == CampaignComplete { 659 return nil 660 } 661 // Mark the campaign as complete 662 c.CompletedDate = time.Now().UTC() 663 c.Status = CampaignComplete 664 err = db.Model(&Campaign{}).Where("id=? and user_id=?", id, uid). 665 Select([]string{"completed_date", "status"}).UpdateColumns(&c).Error 666 if err != nil { 667 log.Error(err) 668 } 669 return err 670 }