mirror of https://github.com/gophish/gophish
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
parent
9f334281ab
commit
7dcf30f277
|
@ -2,7 +2,6 @@ language: go
|
|||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.8
|
||||
- 1.9
|
||||
- tip
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -83,7 +83,6 @@ func (mw *MailWorker) Start(ctx context.Context) {
|
|||
return
|
||||
case ms := <-mw.Queue:
|
||||
go func(ctx context.Context, ms []Mail) {
|
||||
log.Infof("Mailer got %d mail to send", len(ms))
|
||||
dialer, err := ms[0].GetDialer()
|
||||
if err != nil {
|
||||
errorMail(err, ms)
|
||||
|
|
|
@ -17,6 +17,7 @@ type Campaign struct {
|
|||
Name string `json:"name" sql:"not null"`
|
||||
CreatedDate time.Time `json:"created_date"`
|
||||
LaunchDate time.Time `json:"launch_date"`
|
||||
SendByDate time.Time `json:"send_by_date"`
|
||||
CompletedDate time.Time `json:"completed_date"`
|
||||
TemplateId int64 `json:"-"`
|
||||
Template Template `json:"template"`
|
||||
|
@ -52,6 +53,7 @@ type CampaignSummary struct {
|
|||
Id int64 `json:"id"`
|
||||
CreatedDate time.Time `json:"created_date"`
|
||||
LaunchDate time.Time `json:"launch_date"`
|
||||
SendByDate time.Time `json:"send_by_date"`
|
||||
CompletedDate time.Time `json:"completed_date"`
|
||||
Status string `json:"status"`
|
||||
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
|
||||
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.
|
||||
const RecipientParameter = "rid"
|
||||
|
||||
|
@ -136,6 +142,8 @@ func (c *Campaign) Validate() error {
|
|||
return ErrPageNotSpecified
|
||||
case c.SMTP.Name == "":
|
||||
return ErrSMTPNotSpecified
|
||||
case !c.SendByDate.IsZero() && !c.LaunchDate.IsZero() && c.SendByDate.Before(c.LaunchDate):
|
||||
return ErrInvalidSendByDate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -218,6 +226,27 @@ func (c *Campaign) getFromAddress() string {
|
|||
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.
|
||||
// It also backfills numbers as appropriate with a running total, so that the values are aggregated.
|
||||
func getCampaignStats(cid int64) (CampaignStats, error) {
|
||||
|
@ -387,10 +416,16 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
} else {
|
||||
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) {
|
||||
c.Status = CAMPAIGN_IN_PROGRESS
|
||||
}
|
||||
// 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 {
|
||||
c.Groups[i], err = GetGroupByName(g.Name, uid)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
|
@ -402,6 +437,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
totalRecipients += len(c.Groups[i].Targets)
|
||||
}
|
||||
// Check to make sure the template exists
|
||||
t, err := GetTemplateByName(c.Template.Name, uid)
|
||||
|
@ -454,6 +490,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
}
|
||||
// Insert all the results
|
||||
resultMap := make(map[string]bool)
|
||||
recipientIndex := 0
|
||||
for _, g := range c.Groups {
|
||||
// Insert a result for each target in the group
|
||||
for _, t := range g.Targets {
|
||||
|
@ -463,6 +500,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
continue
|
||||
}
|
||||
resultMap[t.Email] = true
|
||||
sendDate := c.generateSendDate(recipientIndex, totalRecipients)
|
||||
r := &Result{
|
||||
BaseRecipient: BaseRecipient{
|
||||
Email: t.Email,
|
||||
|
@ -473,11 +511,11 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
Status: STATUS_SCHEDULED,
|
||||
CampaignId: c.Id,
|
||||
UserId: c.UserId,
|
||||
SendDate: c.LaunchDate,
|
||||
SendDate: sendDate,
|
||||
Reported: false,
|
||||
ModifiedDate: c.CreatedDate,
|
||||
}
|
||||
if c.Status == CAMPAIGN_IN_PROGRESS {
|
||||
if r.SendDate.Before(c.CreatedDate) || r.SendDate.Equal(c.CreatedDate) {
|
||||
r.Status = STATUS_SENDING
|
||||
}
|
||||
err = r.GenerateId()
|
||||
|
@ -492,11 +530,13 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
}).Error(err)
|
||||
}
|
||||
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 {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
recipientIndex++
|
||||
}
|
||||
}
|
||||
err = db.Save(c).Error
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -38,12 +38,12 @@ type MailLog struct {
|
|||
|
||||
// GenerateMailLog creates a new maillog for the given campaign and
|
||||
// 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{
|
||||
UserId: c.UserId,
|
||||
CampaignId: c.Id,
|
||||
RId: r.RId,
|
||||
SendDate: c.LaunchDate,
|
||||
SendDate: sendDate,
|
||||
}
|
||||
err = db.Save(m).Error
|
||||
return err
|
||||
|
|
|
@ -193,7 +193,7 @@ func (s *ModelsSuite) TestGenerateMailLog(ch *check.C) {
|
|||
result := Result{
|
||||
RId: "abc1234",
|
||||
}
|
||||
err := GenerateMailLog(&campaign, &result)
|
||||
err := GenerateMailLog(&campaign, &result, campaign.LaunchDate)
|
||||
ch.Assert(err, check.Equals, nil)
|
||||
|
||||
m := MailLog{}
|
||||
|
|
|
@ -47,6 +47,8 @@ func (s *ModelsSuite) createCampaignDependencies(ch *check.C, optional ...string
|
|||
group.Targets = []Target{
|
||||
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: "test3@example.com", FirstName: "Second", LastName: "Example"}},
|
||||
Target{BaseRecipient: BaseRecipient{Email: "test4@example.com", FirstName: "Second", LastName: "Example"}},
|
||||
}
|
||||
group.UserId = 1
|
||||
ch.Assert(PostGroup(&group), check.Equals, nil)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"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) {
|
||||
group := Group{Name: "Test Group"}
|
||||
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
|
@ -357,14 +357,15 @@ var renderDevice = function (event_details) {
|
|||
}
|
||||
|
||||
function renderTimeline(data) {
|
||||
console.log(data)
|
||||
record = {
|
||||
"first_name": data[2],
|
||||
"last_name": data[3],
|
||||
"email": data[4],
|
||||
"position": data[5],
|
||||
"status": data[6],
|
||||
"send_date": data[7],
|
||||
"reported": data[8]
|
||||
"reported": data[7],
|
||||
"send_date": data[8]
|
||||
}
|
||||
results = '<div class="timeline col-sm-12 well well-lg">' +
|
||||
'<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
|
||||
|
|
|
@ -33,6 +33,10 @@ function launch() {
|
|||
});
|
||||
})
|
||||
// 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 = {
|
||||
name: $("#name").val(),
|
||||
template: {
|
||||
|
@ -46,7 +50,8 @@ function launch() {
|
|||
name: $("#profile").select2("data")[0].text
|
||||
},
|
||||
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)
|
||||
// Submit the campaign
|
||||
|
@ -267,6 +272,13 @@ $(document).ready(function () {
|
|||
"showTodayButton": true,
|
||||
"defaultDate": moment()
|
||||
})
|
||||
$("#send_by_date").datetimepicker({
|
||||
"widgetPositioning": {
|
||||
"vertical": "bottom"
|
||||
},
|
||||
"showTodayButton": true,
|
||||
"useCurrent": false
|
||||
})
|
||||
// Setup multiple modals
|
||||
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
|
||||
$('.modal').on('hidden.bs.modal', function (event) {
|
||||
|
|
|
@ -3,24 +3,35 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-3 col-md-2 sidebar">
|
||||
<ul class="nav nav-sidebar">
|
||||
<li><a href="/">Dashboard</a>
|
||||
<li>
|
||||
<a href="/">Dashboard</a>
|
||||
</li>
|
||||
<li class="active"><a href="/campaigns">Campaigns</a>
|
||||
<li class="active">
|
||||
<a href="/campaigns">Campaigns</a>
|
||||
</li>
|
||||
<li><a href="/users">Users & Groups</a>
|
||||
<li>
|
||||
<a href="/users">Users & Groups</a>
|
||||
</li>
|
||||
<li><a href="/templates">Email Templates</a>
|
||||
<li>
|
||||
<a href="/templates">Email Templates</a>
|
||||
</li>
|
||||
<li><a href="/landing_pages">Landing Pages</a>
|
||||
<li>
|
||||
<a href="/landing_pages">Landing Pages</a>
|
||||
</li>
|
||||
<li><a href="/sending_profiles">Sending Profiles</a>
|
||||
<li>
|
||||
<a href="/sending_profiles">Sending Profiles</a>
|
||||
</li>
|
||||
<li><a href="/settings">Settings</a>
|
||||
<li>
|
||||
<a href="/settings">Settings</a>
|
||||
</li>
|
||||
<li><hr></li>
|
||||
<li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
|
||||
<li>
|
||||
<hr>
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -34,7 +45,8 @@
|
|||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
<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 id="loading">
|
||||
|
@ -65,7 +77,9 @@
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="dismiss()"><span aria-hidden="true">×</span></button>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="dismiss()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="campaignModalLabel">New Campaign</h4>
|
||||
</div>
|
||||
<div class="modal-body" id="modal_body">
|
||||
|
@ -81,17 +95,30 @@
|
|||
<select class="form-control" placeholder="Landing Page" id="page" />
|
||||
<option></option>
|
||||
</select>
|
||||
<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>
|
||||
<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>
|
||||
<input type="text" class="form-control" placeholder="http://192.168.1.1" id="url" />
|
||||
<label class="control-label" for="url">Schedule: </label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="control-label" for="url">Launch Date </label>
|
||||
<input type="text" class="form-control" id="launch_date" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="control-label" for="delay">Send Emails By (Optional)
|
||||
<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>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="send_by_date" />
|
||||
</div>
|
||||
</div>
|
||||
<label class="control-label" for="profile">Sending Profile:</label>
|
||||
<div class="input-group">
|
||||
<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>
|
||||
<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>
|
||||
|
@ -100,7 +127,8 @@
|
|||
</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>
|
||||
<button type="button" id="launchButton" class="btn btn-primary" onclick="launch()">
|
||||
<i class="fa fa-rocket"></i> Launch Campaign</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -111,7 +139,9 @@
|
|||
<div class="modal-content">
|
||||
<!-- New Email Modal -->
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="sendTestEmailModalTitle">Send Test Email</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
@ -137,12 +167,12 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" data-dismiss="modal" class="btn btn-default">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="sendTestModalSubmit" onclick="sendTestEmail()"><i class="fa fa-envelope"></i> Send</button>
|
||||
<button type="button" class="btn btn-primary" id="sendTestModalSubmit" onclick="sendTestEmail()">
|
||||
<i class="fa fa-envelope"></i> Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "scripts"}}
|
||||
{{end}} {{define "scripts"}}
|
||||
<script src="/js/dist/app/campaigns.min.js"></script>
|
||||
{{end}}
|
|
@ -79,7 +79,14 @@ func (w *Worker) LaunchCampaign(c models.Campaign) {
|
|||
// This is required since you cannot pass a slice of values
|
||||
// that implements an interface as a slice of that interface.
|
||||
mailEntries := []mailer.Mail{}
|
||||
currentTime := time.Now().UTC()
|
||||
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)
|
||||
}
|
||||
mailer.Mailer.Queue <- mailEntries
|
||||
|
@ -88,7 +95,8 @@ func (w *Worker) LaunchCampaign(c models.Campaign) {
|
|||
// SendTestEmail sends a test email
|
||||
func (w *Worker) SendTestEmail(s *models.EmailRequest) error {
|
||||
go func() {
|
||||
mailer.Mailer.Queue <- []mailer.Mail{s}
|
||||
ms := []mailer.Mail{s}
|
||||
mailer.Mailer.Queue <- ms
|
||||
}()
|
||||
return <-s.ErrorChan
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue