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  }