Add Support for Timed Campaigns (#1184)

This builds on the work from @c-f in #1090 to fully add support for "timed" campaigns, in which the emails are spaced apart as opposed to all being sent at once.
1205-drop-campaigns
Jordan Wright 2018-09-02 11:17:52 -05:00 committed by GitHub
parent 9f334281ab
commit 7dcf30f277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 304 additions and 100 deletions

View File

@ -2,7 +2,6 @@ language: go
sudo: false sudo: false
go: go:
- 1.8
- 1.9 - 1.9
- tip - tip

View File

@ -0,0 +1,6 @@
-- +goose Up
-- SQL in this section is executed when the migration is applied.
ALTER TABLE campaigns ADD COLUMN send_by_date DATETIME;
-- +goose Down
-- SQL in this section is executed when the migration is rolled back.

View File

@ -0,0 +1,6 @@
-- +goose Up
-- SQL in this section is executed when the migration is applied.
ALTER TABLE campaigns ADD COLUMN send_by_date DATETIME;
-- +goose Down
-- SQL in this section is executed when the migration is rolled back.

View File

@ -83,7 +83,6 @@ func (mw *MailWorker) Start(ctx context.Context) {
return return
case ms := <-mw.Queue: case ms := <-mw.Queue:
go func(ctx context.Context, ms []Mail) { go func(ctx context.Context, ms []Mail) {
log.Infof("Mailer got %d mail to send", len(ms))
dialer, err := ms[0].GetDialer() dialer, err := ms[0].GetDialer()
if err != nil { if err != nil {
errorMail(err, ms) errorMail(err, ms)

View File

@ -17,6 +17,7 @@ type Campaign struct {
Name string `json:"name" sql:"not null"` Name string `json:"name" sql:"not null"`
CreatedDate time.Time `json:"created_date"` CreatedDate time.Time `json:"created_date"`
LaunchDate time.Time `json:"launch_date"` LaunchDate time.Time `json:"launch_date"`
SendByDate time.Time `json:"send_by_date"`
CompletedDate time.Time `json:"completed_date"` CompletedDate time.Time `json:"completed_date"`
TemplateId int64 `json:"-"` TemplateId int64 `json:"-"`
Template Template `json:"template"` Template Template `json:"template"`
@ -52,6 +53,7 @@ type CampaignSummary struct {
Id int64 `json:"id"` Id int64 `json:"id"`
CreatedDate time.Time `json:"created_date"` CreatedDate time.Time `json:"created_date"`
LaunchDate time.Time `json:"launch_date"` LaunchDate time.Time `json:"launch_date"`
SendByDate time.Time `json:"send_by_date"`
CompletedDate time.Time `json:"completed_date"` CompletedDate time.Time `json:"completed_date"`
Status string `json:"status"` Status string `json:"status"`
Name string `json:"name"` Name string `json:"name"`
@ -120,6 +122,10 @@ var ErrPageNotFound = errors.New("Page not found")
// ErrSMTPNotFound indicates a sending profile specified by the user does not exist in the database // ErrSMTPNotFound indicates a sending profile specified by the user does not exist in the database
var ErrSMTPNotFound = errors.New("Sending profile not found") var ErrSMTPNotFound = errors.New("Sending profile not found")
// ErrInvalidSendByDate indicates that the user specified a send by date that occurs before the
// launch date
var ErrInvalidSendByDate = errors.New("The launch date must be before the \"send emails by\" date")
// RecipientParameter is the URL parameter that points to the result ID for a recipient. // RecipientParameter is the URL parameter that points to the result ID for a recipient.
const RecipientParameter = "rid" const RecipientParameter = "rid"
@ -136,6 +142,8 @@ func (c *Campaign) Validate() error {
return ErrPageNotSpecified return ErrPageNotSpecified
case c.SMTP.Name == "": case c.SMTP.Name == "":
return ErrSMTPNotSpecified return ErrSMTPNotSpecified
case !c.SendByDate.IsZero() && !c.LaunchDate.IsZero() && c.SendByDate.Before(c.LaunchDate):
return ErrInvalidSendByDate
} }
return nil return nil
} }
@ -218,6 +226,27 @@ func (c *Campaign) getFromAddress() string {
return c.SMTP.FromAddress return c.SMTP.FromAddress
} }
// generateSendDate creates a sendDate
func (c *Campaign) generateSendDate(idx int, totalRecipients int) time.Time {
// If no send date is specified, just return the launch date
if c.SendByDate.IsZero() || c.SendByDate.Equal(c.LaunchDate) {
return c.LaunchDate
}
// Otherwise, we can calculate the range of minutes to send emails
// (since we only poll once per minute)
totalMinutes := c.SendByDate.Sub(c.LaunchDate).Minutes()
// Next, we can determine how many minutes should elapse between emails
minutesPerEmail := totalMinutes / float64(totalRecipients)
// Then, we can calculate the offset for this particular email
offset := int(minutesPerEmail * float64(idx))
// Finally, we can just add this offset to the launch date to determine
// when the email should be sent
return c.LaunchDate.Add(time.Duration(offset) * time.Minute)
}
// getCampaignStats returns a CampaignStats object for the campaign with the given campaign ID. // getCampaignStats returns a CampaignStats object for the campaign with the given campaign ID.
// It also backfills numbers as appropriate with a running total, so that the values are aggregated. // It also backfills numbers as appropriate with a running total, so that the values are aggregated.
func getCampaignStats(cid int64) (CampaignStats, error) { func getCampaignStats(cid int64) (CampaignStats, error) {
@ -387,10 +416,16 @@ func PostCampaign(c *Campaign, uid int64) error {
} else { } else {
c.LaunchDate = c.LaunchDate.UTC() c.LaunchDate = c.LaunchDate.UTC()
} }
if !c.SendByDate.IsZero() {
c.SendByDate = c.SendByDate.UTC()
}
if c.LaunchDate.Before(c.CreatedDate) || c.LaunchDate.Equal(c.CreatedDate) { if c.LaunchDate.Before(c.CreatedDate) || c.LaunchDate.Equal(c.CreatedDate) {
c.Status = CAMPAIGN_IN_PROGRESS c.Status = CAMPAIGN_IN_PROGRESS
} }
// Check to make sure all the groups already exist // Check to make sure all the groups already exist
// Also, later we'll need to know the total number of recipients (counting
// duplicates is ok for now), so we'll do that here to save a loop.
totalRecipients := 0
for i, g := range c.Groups { for i, g := range c.Groups {
c.Groups[i], err = GetGroupByName(g.Name, uid) c.Groups[i], err = GetGroupByName(g.Name, uid)
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
@ -402,6 +437,7 @@ func PostCampaign(c *Campaign, uid int64) error {
log.Error(err) log.Error(err)
return err return err
} }
totalRecipients += len(c.Groups[i].Targets)
} }
// Check to make sure the template exists // Check to make sure the template exists
t, err := GetTemplateByName(c.Template.Name, uid) t, err := GetTemplateByName(c.Template.Name, uid)
@ -454,6 +490,7 @@ func PostCampaign(c *Campaign, uid int64) error {
} }
// Insert all the results // Insert all the results
resultMap := make(map[string]bool) resultMap := make(map[string]bool)
recipientIndex := 0
for _, g := range c.Groups { for _, g := range c.Groups {
// Insert a result for each target in the group // Insert a result for each target in the group
for _, t := range g.Targets { for _, t := range g.Targets {
@ -463,6 +500,7 @@ func PostCampaign(c *Campaign, uid int64) error {
continue continue
} }
resultMap[t.Email] = true resultMap[t.Email] = true
sendDate := c.generateSendDate(recipientIndex, totalRecipients)
r := &Result{ r := &Result{
BaseRecipient: BaseRecipient{ BaseRecipient: BaseRecipient{
Email: t.Email, Email: t.Email,
@ -473,11 +511,11 @@ func PostCampaign(c *Campaign, uid int64) error {
Status: STATUS_SCHEDULED, Status: STATUS_SCHEDULED,
CampaignId: c.Id, CampaignId: c.Id,
UserId: c.UserId, UserId: c.UserId,
SendDate: c.LaunchDate, SendDate: sendDate,
Reported: false, Reported: false,
ModifiedDate: c.CreatedDate, ModifiedDate: c.CreatedDate,
} }
if c.Status == CAMPAIGN_IN_PROGRESS { if r.SendDate.Before(c.CreatedDate) || r.SendDate.Equal(c.CreatedDate) {
r.Status = STATUS_SENDING r.Status = STATUS_SENDING
} }
err = r.GenerateId() err = r.GenerateId()
@ -492,11 +530,13 @@ func PostCampaign(c *Campaign, uid int64) error {
}).Error(err) }).Error(err)
} }
c.Results = append(c.Results, *r) c.Results = append(c.Results, *r)
err = GenerateMailLog(c, r) log.Infof("Creating maillog for %s to send at %s\n", r.Email, sendDate)
err = GenerateMailLog(c, r, sendDate)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
continue continue
} }
recipientIndex++
} }
} }
err = db.Save(c).Error err = db.Save(c).Error

81
models/campaign_test.go Normal file
View File

@ -0,0 +1,81 @@
package models
import (
"time"
check "gopkg.in/check.v1"
)
func (s *ModelsSuite) TestGenerateSendDate(c *check.C) {
campaign := s.createCampaignDependencies(c)
// Test that if no launch date is provided, the campaign's creation date
// is used.
err := PostCampaign(&campaign, campaign.UserId)
c.Assert(err, check.Equals, nil)
c.Assert(campaign.LaunchDate, check.Equals, campaign.CreatedDate)
ms, err := GetMailLogsByCampaign(campaign.Id)
c.Assert(err, check.Equals, nil)
for _, m := range ms {
c.Assert(m.SendDate, check.Equals, campaign.CreatedDate)
}
// Test that if no send date is provided, all the emails are sent at the
// campaign's launch date
campaign = s.createCampaignDependencies(c)
campaign.LaunchDate = time.Now().UTC()
err = PostCampaign(&campaign, campaign.UserId)
c.Assert(err, check.Equals, nil)
ms, err = GetMailLogsByCampaign(campaign.Id)
c.Assert(err, check.Equals, nil)
for _, m := range ms {
c.Assert(m.SendDate, check.Equals, campaign.LaunchDate)
}
// Finally, test that if a send date is provided, the emails are staggered
// correctly.
campaign = s.createCampaignDependencies(c)
campaign.LaunchDate = time.Now().UTC()
campaign.SendByDate = campaign.LaunchDate.Add(2 * time.Minute)
err = PostCampaign(&campaign, campaign.UserId)
c.Assert(err, check.Equals, nil)
ms, err = GetMailLogsByCampaign(campaign.Id)
c.Assert(err, check.Equals, nil)
sendingOffset := 2 / float64(len(ms))
for i, m := range ms {
expectedOffset := int(sendingOffset * float64(i))
expectedDate := campaign.LaunchDate.Add(time.Duration(expectedOffset) * time.Minute)
c.Assert(m.SendDate, check.Equals, expectedDate)
}
}
func (s *ModelsSuite) TestCampaignDateValidation(c *check.C) {
campaign := s.createCampaignDependencies(c)
// If both are zero, then the campaign should start immediately with no
// send by date
err := campaign.Validate()
c.Assert(err, check.Equals, nil)
// If the launch date is specified, then the send date is optional
campaign = s.createCampaignDependencies(c)
campaign.LaunchDate = time.Now().UTC()
err = campaign.Validate()
c.Assert(err, check.Equals, nil)
// If the send date is greater than the launch date, then there's no
//problem
campaign = s.createCampaignDependencies(c)
campaign.LaunchDate = time.Now().UTC()
campaign.SendByDate = campaign.LaunchDate.Add(1 * time.Minute)
err = campaign.Validate()
c.Assert(err, check.Equals, nil)
// If the send date is less than the launch date, then there's an issue
campaign = s.createCampaignDependencies(c)
campaign.LaunchDate = time.Now().UTC()
campaign.SendByDate = campaign.LaunchDate.Add(-1 * time.Minute)
err = campaign.Validate()
c.Assert(err, check.Equals, ErrInvalidSendByDate)
}

View File

@ -38,12 +38,12 @@ type MailLog struct {
// GenerateMailLog creates a new maillog for the given campaign and // GenerateMailLog creates a new maillog for the given campaign and
// result. It sets the initial send date to match the campaign's launch date. // result. It sets the initial send date to match the campaign's launch date.
func GenerateMailLog(c *Campaign, r *Result) error { func GenerateMailLog(c *Campaign, r *Result, sendDate time.Time) error {
m := &MailLog{ m := &MailLog{
UserId: c.UserId, UserId: c.UserId,
CampaignId: c.Id, CampaignId: c.Id,
RId: r.RId, RId: r.RId,
SendDate: c.LaunchDate, SendDate: sendDate,
} }
err = db.Save(m).Error err = db.Save(m).Error
return err return err

View File

@ -193,7 +193,7 @@ func (s *ModelsSuite) TestGenerateMailLog(ch *check.C) {
result := Result{ result := Result{
RId: "abc1234", RId: "abc1234",
} }
err := GenerateMailLog(&campaign, &result) err := GenerateMailLog(&campaign, &result, campaign.LaunchDate)
ch.Assert(err, check.Equals, nil) ch.Assert(err, check.Equals, nil)
m := MailLog{} m := MailLog{}

View File

@ -47,6 +47,8 @@ func (s *ModelsSuite) createCampaignDependencies(ch *check.C, optional ...string
group.Targets = []Target{ group.Targets = []Target{
Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}}, Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}}, Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
Target{BaseRecipient: BaseRecipient{Email: "test3@example.com", FirstName: "Second", LastName: "Example"}},
Target{BaseRecipient: BaseRecipient{Email: "test4@example.com", FirstName: "Second", LastName: "Example"}},
} }
group.UserId = 1 group.UserId = 1
ch.Assert(PostGroup(&group), check.Equals, nil) ch.Assert(PostGroup(&group), check.Equals, nil)

View File

@ -1,6 +1,7 @@
package models package models
import ( import (
"fmt"
"net/mail" "net/mail"
"regexp" "regexp"
"time" "time"
@ -58,6 +59,25 @@ func (s *ModelsSuite) TestResultScheduledStatus(ch *check.C) {
} }
} }
func (s *ModelsSuite) TestResultVariableStatus(ch *check.C) {
c := s.createCampaignDependencies(ch)
c.LaunchDate = time.Now().UTC()
c.SendByDate = c.LaunchDate.Add(2 * time.Minute)
ch.Assert(PostCampaign(&c, c.UserId), check.Equals, nil)
// The campaign has a window smaller than our group size, so we expect some
// emails to be sent immediately, while others will be scheduled
for _, r := range c.Results {
if r.SendDate.Before(c.CreatedDate) || r.SendDate.Equal(c.CreatedDate) {
fmt.Println("SENDING")
ch.Assert(r.Status, check.Equals, STATUS_SENDING)
} else {
fmt.Println("SCHEDULED")
ch.Assert(r.Status, check.Equals, STATUS_SCHEDULED)
}
}
}
func (s *ModelsSuite) TestDuplicateResults(ch *check.C) { func (s *ModelsSuite) TestDuplicateResults(ch *check.C) {
group := Group{Name: "Test Group"} group := Group{Name: "Test Group"}
group.Targets = []Target{ group.Targets = []Target{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -357,14 +357,15 @@ var renderDevice = function (event_details) {
} }
function renderTimeline(data) { function renderTimeline(data) {
console.log(data)
record = { record = {
"first_name": data[2], "first_name": data[2],
"last_name": data[3], "last_name": data[3],
"email": data[4], "email": data[4],
"position": data[5], "position": data[5],
"status": data[6], "status": data[6],
"send_date": data[7], "reported": data[7],
"reported": data[8] "send_date": data[8]
} }
results = '<div class="timeline col-sm-12 well well-lg">' + results = '<div class="timeline col-sm-12 well well-lg">' +
'<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) + '<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +

View File

@ -33,6 +33,10 @@ function launch() {
}); });
}) })
// Validate our fields // Validate our fields
var send_by_date = $("#send_by_date").val()
if (send_by_date != "") {
send_by_date = moment(send_by_date, "MM/DD/YYYY hh:mm a").utc().format()
}
campaign = { campaign = {
name: $("#name").val(), name: $("#name").val(),
template: { template: {
@ -46,7 +50,8 @@ function launch() {
name: $("#profile").select2("data")[0].text name: $("#profile").select2("data")[0].text
}, },
launch_date: moment($("#launch_date").val(), "MM/DD/YYYY hh:mm a").utc().format(), launch_date: moment($("#launch_date").val(), "MM/DD/YYYY hh:mm a").utc().format(),
groups: groups send_by_date: send_by_date || null,
groups: groups,
} }
console.log("Launching campaign at time: " + campaign.launch_date) console.log("Launching campaign at time: " + campaign.launch_date)
// Submit the campaign // Submit the campaign
@ -267,6 +272,13 @@ $(document).ready(function () {
"showTodayButton": true, "showTodayButton": true,
"defaultDate": moment() "defaultDate": moment()
}) })
$("#send_by_date").datetimepicker({
"widgetPositioning": {
"vertical": "bottom"
},
"showTodayButton": true,
"useCurrent": false
})
// Setup multiple modals // Setup multiple modals
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html // Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
$('.modal').on('hidden.bs.modal', function (event) { $('.modal').on('hidden.bs.modal', function (event) {

View File

@ -3,24 +3,35 @@
<div class="row"> <div class="row">
<div class="col-sm-3 col-md-2 sidebar"> <div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar"> <ul class="nav nav-sidebar">
<li><a href="/">Dashboard</a> <li>
<a href="/">Dashboard</a>
</li> </li>
<li class="active"><a href="/campaigns">Campaigns</a> <li class="active">
<a href="/campaigns">Campaigns</a>
</li> </li>
<li><a href="/users">Users &amp; Groups</a> <li>
<a href="/users">Users &amp; Groups</a>
</li> </li>
<li><a href="/templates">Email Templates</a> <li>
<a href="/templates">Email Templates</a>
</li> </li>
<li><a href="/landing_pages">Landing Pages</a> <li>
<a href="/landing_pages">Landing Pages</a>
</li> </li>
<li><a href="/sending_profiles">Sending Profiles</a> <li>
<a href="/sending_profiles">Sending Profiles</a>
</li> </li>
<li><a href="/settings">Settings</a> <li>
<a href="/settings">Settings</a>
</li> </li>
<li><hr></li> <li>
<li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a> <hr>
</li> </li>
<li><a href="/api/">API Documentation</a> <li>
<a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
</li>
<li>
<a href="/api/">API Documentation</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -34,7 +45,8 @@
</div> </div>
<div id="flashes" class="row"></div> <div id="flashes" class="row"></div>
<div class="row"> <div class="row">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#modal" onclick="edit('new')"><i class="fa fa-plus"></i> New Campaign</button> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#modal" onclick="edit('new')">
<i class="fa fa-plus"></i> New Campaign</button>
</div> </div>
&nbsp; &nbsp;
<div id="loading"> <div id="loading">
@ -62,87 +74,105 @@
</div> </div>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"> <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="dismiss()"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="dismiss()">
<h4 class="modal-title" id="campaignModalLabel">New Campaign</h4> <span aria-hidden="true">&times;</span>
</div> </button>
<div class="modal-body" id="modal_body"> <h4 class="modal-title" id="campaignModalLabel">New Campaign</h4>
<div class="row" id="modal.flashes"></div> </div>
<div class="form-group"> <div class="modal-body" id="modal_body">
<label for="name">Name:</label> <div class="row" id="modal.flashes"></div>
<input type="text" class="form-control" id="name" placeholder="Campaign name" autofocus> <div class="form-group">
<label class="control-label" for="template">Email Template:</label> <label for="name">Name:</label>
<select class="form-control" placeholder="Template Name" id="template"/> <input type="text" class="form-control" id="name" placeholder="Campaign name" autofocus>
<option></option> <label class="control-label" for="template">Email Template:</label>
</select> <select class="form-control" placeholder="Template Name" id="template" />
<label class="control-label" for="page">Landing Page:</label> <option></option>
<select class="form-control" placeholder="Landing Page" id="page"/> </select>
<option></option> <label class="control-label" for="page">Landing Page:</label>
</select> <select class="form-control" placeholder="Landing Page" id="page" />
<label class="control-label" for="url">URL: <i class="fa fa-question-circle" data-toggle="tooltip" data-placement="right" title="Location of gophish listener (must be reachable by targets!)"></i></label> <option></option>
<input type="text" class="form-control" placeholder="http://192.168.1.1" id="url"/> </select>
<label class="control-label" for="url">Schedule: </label> <label class="control-label" for="url">URL:
<input type="text" class="form-control" id="launch_date"/> <i class="fa fa-question-circle" data-toggle="tooltip" data-placement="right" title="Location of Gophish listener (must be reachable by targets!)"></i>
<label class="control-label" for="profile">Sending Profile:</label> </label>
<div class="input-group"> <input type="text" class="form-control" placeholder="http://192.168.1.1" id="url" />
<select class="form-control" placeholder="Sending Profile" id="profile"/> <div class="row">
<option></option> <div class="col-md-6">
</select> <label class="control-label" for="url">Launch Date </label>
<span class="input-group-btn"> <input type="text" class="form-control" id="launch_date" />
<button type="button" data-toggle="modal" data-target="#sendTestEmailModal" class="btn btn-primary button"><i class="fa fa-envelope"></i> Send Test Email</button> </div>
</span> <div class="col-md-6">
</div> <label class="control-label" for="delay">Send Emails By (Optional)
<label class="control-label" for="users">Groups:</label> <i class="fa fa-question-circle" data-toggle="tooltip" data-placement="right" title="If specified, Gophish will send emails evenly between the campaign launch and this date."></i>
<select class="form-control" id="users" multiple="multiple"></select> </label>
</div> <input type="text" class="form-control" id="send_by_date" />
</div> </div>
<div class="modal-footer"> </div>
<button type="button" class="btn btn-default" data-dismiss="modal" onclick="dismiss()">Close</button> <label class="control-label" for="profile">Sending Profile:</label>
<button type="button" id="launchButton" class="btn btn-primary" onclick="launch()"><i class="fa fa-rocket"></i> Launch Campaign</button> <div class="input-group">
</div> <select class="form-control" placeholder="Sending Profile" id="profile" />
<option></option>
</select>
<span class="input-group-btn">
<button type="button" data-toggle="modal" data-target="#sendTestEmailModal" class="btn btn-primary button">
<i class="fa fa-envelope"></i> Send Test Email</button>
</span>
</div>
<label class="control-label" for="users">Groups:</label>
<select class="form-control" id="users" multiple="multiple"></select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" onclick="dismiss()">Close</button>
<button type="button" id="launchButton" class="btn btn-primary" onclick="launch()">
<i class="fa fa-rocket"></i> Launch Campaign</button>
</div>
</div>
</div> </div>
</div>
</div> </div>
<!-- Send Test Email Modal --> <!-- Send Test Email Modal -->
<div class="modal" id="sendTestEmailModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel"> <div class="modal" id="sendTestEmailModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<!-- New Email Modal --> <!-- New Email Modal -->
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<h4 class="modal-title" id="sendTestEmailModalTitle">Send Test Email</h4> <span aria-hidden="true">&times;</span>
</div> </button>
<div class="modal-body"> <h4 class="modal-title" id="sendTestEmailModalTitle">Send Test Email</h4>
<div class="row" id="sendTestEmailModal.flashes"></div> </div>
<div class="row"> <div class="modal-body">
<div class="col-sm-12"> <div class="row" id="sendTestEmailModal.flashes"></div>
<label class="control-label" for="to">Send Test Email to:</label> <div class="row">
</div> <div class="col-sm-12">
<br> <label class="control-label" for="to">Send Test Email to:</label>
<div class="col-sm-2"> </div>
<input type="text" class="form-control" placeholder="First Name" name="to_first_name"> <br>
</div> <div class="col-sm-2">
<div class="col-sm-2"> <input type="text" class="form-control" placeholder="First Name" name="to_first_name">
<input type="text" class="form-control" placeholder="Last Name" name="to_last_name"> </div>
</div> <div class="col-sm-2">
<div class="col-sm-4"> <input type="text" class="form-control" placeholder="Last Name" name="to_last_name">
<input type="email" class="form-control" placeholder="Email" name="to_email" required> </div>
</div> <div class="col-sm-4">
<div class="col-sm-4"> <input type="email" class="form-control" placeholder="Email" name="to_email" required>
<input type="text" class="form-control" placeholder="Position" name="to_position"> </div>
<div class="col-sm-4">
<input type="text" class="form-control" placeholder="Position" name="to_position">
</div>
</div> </div>
</div> </div>
</div> <div class="modal-footer">
<div class="modal-footer"> <button type="button" data-dismiss="modal" class="btn btn-default">Cancel</button>
<button type="button" data-dismiss="modal" class="btn btn-default">Cancel</button> <button type="button" class="btn btn-primary" id="sendTestModalSubmit" onclick="sendTestEmail()">
<button type="button" class="btn btn-primary" id="sendTestModalSubmit" onclick="sendTestEmail()"><i class="fa fa-envelope"></i> Send</button> <i class="fa fa-envelope"></i> Send</button>
</div>
</div> </div>
</div> </div>
</div>
</div> </div>
{{end}} {{end}} {{define "scripts"}}
{{define "scripts"}}
<script src="/js/dist/app/campaigns.min.js"></script> <script src="/js/dist/app/campaigns.min.js"></script>
{{end}} {{end}}

View File

@ -79,7 +79,14 @@ func (w *Worker) LaunchCampaign(c models.Campaign) {
// This is required since you cannot pass a slice of values // This is required since you cannot pass a slice of values
// that implements an interface as a slice of that interface. // that implements an interface as a slice of that interface.
mailEntries := []mailer.Mail{} mailEntries := []mailer.Mail{}
currentTime := time.Now().UTC()
for _, m := range ms { for _, m := range ms {
// Only send the emails scheduled to be sent for the past minute to
// respect the campaign scheduling options
if m.SendDate.After(currentTime) {
m.Unlock()
continue
}
mailEntries = append(mailEntries, m) mailEntries = append(mailEntries, m)
} }
mailer.Mailer.Queue <- mailEntries mailer.Mailer.Queue <- mailEntries
@ -88,7 +95,8 @@ func (w *Worker) LaunchCampaign(c models.Campaign) {
// SendTestEmail sends a test email // SendTestEmail sends a test email
func (w *Worker) SendTestEmail(s *models.EmailRequest) error { func (w *Worker) SendTestEmail(s *models.EmailRequest) error {
go func() { go func() {
mailer.Mailer.Queue <- []mailer.Mail{s} ms := []mailer.Mail{s}
mailer.Mailer.Queue <- ms
}() }()
return <-s.ErrorChan return <-s.ErrorChan
} }