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
|
sudo: false
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.8
|
|
||||||
- 1.9
|
- 1.9
|
||||||
- tip
|
- 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
|
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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
// 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
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
@ -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) +
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -366,4 +378,4 @@ $(document).ready(function () {
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -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 & Groups</a>
|
<li>
|
||||||
|
<a href="/users">Users & 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>
|
||||||
|
|
||||||
<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">×</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">×</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">×</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">×</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}}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue