Implemented the ability to preview landing pages when sending a test email.

pull/1069/merge
Jordan Wright 2018-06-08 21:20:52 -05:00
parent a04f6d031b
commit ebb6cd61b2
21 changed files with 535 additions and 244 deletions

View File

@ -652,8 +652,9 @@ func API_Import_Site(w http.ResponseWriter, r *http.Request) {
// API_Send_Test_Email sends a test email using the template name // API_Send_Test_Email sends a test email using the template name
// and Target given. // and Target given.
func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) { func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
s := &models.SendTestEmailRequest{ s := &models.EmailRequest{
ErrorChan: make(chan error), ErrorChan: make(chan error),
UserId: ctx.Get(r, "user_id").(int64),
} }
if r.Method != "POST" { if r.Method != "POST" {
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest) JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
@ -664,11 +665,8 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest) JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
return return
} }
// Validate the given request
if err = s.Validate(); err != nil { storeRequest := false
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
// If a Template is not specified use a default // If a Template is not specified use a default
if s.Template.Name == "" { if s.Template.Name == "" {
@ -678,17 +676,15 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
"{{if .FirstName}} First Name: {{.FirstName}}\n{{end}}" + "{{if .FirstName}} First Name: {{.FirstName}}\n{{end}}" +
"{{if .LastName}} Last Name: {{.LastName}}\n{{end}}" + "{{if .LastName}} Last Name: {{.LastName}}\n{{end}}" +
"{{if .Position}} Position: {{.Position}}\n{{end}}" + "{{if .Position}} Position: {{.Position}}\n{{end}}" +
"{{if .TrackingURL}} Tracking URL: {{.TrackingURL}}\n{{end}}" +
"\nNow go send some phish!" "\nNow go send some phish!"
t := models.Template{ t := models.Template{
Subject: "Default Email from Gophish", Subject: "Default Email from Gophish",
Text: text, Text: text,
} }
s.Template = t s.Template = t
// Try to lookup the Template by name
} else { } else {
// Get the Template requested by name // Get the Template requested by name
s.Template, err = models.GetTemplateByName(s.Template.Name, ctx.Get(r, "user_id").(int64)) s.Template, err = models.GetTemplateByName(s.Template.Name, s.UserId)
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"template": s.Template.Name, "template": s.Template.Name,
@ -700,12 +696,32 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return return
} }
s.TemplateId = s.Template.Id
// We'll only save the test request to the database if there is a
// user-specified template to use.
storeRequest = true
}
if s.Page.Name != "" {
s.Page, err = models.GetPageByName(s.Page.Name, s.UserId)
if err == gorm.ErrRecordNotFound {
log.WithFields(logrus.Fields{
"page": s.Page.Name,
}).Error("Page does not exist")
JSONResponse(w, models.Response{Success: false, Message: models.ErrPageNotFound.Error()}, http.StatusBadRequest)
return
} else if err != nil {
log.Error(err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
s.PageId = s.Page.Id
} }
// If a complete sending profile is provided use it // If a complete sending profile is provided use it
if err := s.SMTP.Validate(); err != nil { if err := s.SMTP.Validate(); err != nil {
// Otherwise get the SMTP requested by name // Otherwise get the SMTP requested by name
smtp, lookupErr := models.GetSMTPByName(s.SMTP.Name, ctx.Get(r, "user_id").(int64)) smtp, lookupErr := models.GetSMTPByName(s.SMTP.Name, s.UserId)
// If the Sending Profile doesn't exist, let's err on the side // If the Sending Profile doesn't exist, let's err on the side
// of caution and assume that the validation failure was more important. // of caution and assume that the validation failure was more important.
if lookupErr != nil { if lookupErr != nil {
@ -716,9 +732,25 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
s.SMTP = smtp s.SMTP = smtp
} }
// Validate the given request
if err = s.Validate(); err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
// Store the request if this wasn't the default template
if storeRequest {
err = models.PostEmailRequest(s)
if err != nil {
log.Error(err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
}
// Send the test email // Send the test email
err = Worker.SendTestEmail(s) err = Worker.SendTestEmail(s)
if err != nil { if err != nil {
log.Error(err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return return
} }

View File

@ -63,8 +63,8 @@ func (s *ControllersSuite) SetupTest() {
// Add a group // Add a group
group := models.Group{Name: "Test Group"} group := models.Group{Name: "Test Group"}
group.Targets = []models.Target{ group.Targets = []models.Target{
models.Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, models.Target{BaseRecipient: models.BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
models.Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, models.Target{BaseRecipient: models.BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
} }
group.UserId = 1 group.UserId = 1
models.PostGroup(&group) models.PostGroup(&group)

View File

@ -1,14 +1,10 @@
package controllers package controllers
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"html/template"
"net" "net"
"net/http" "net/http"
"net/mail"
"net/url"
"strings" "strings"
ctx "github.com/gophish/gophish/context" ctx "github.com/gophish/gophish/context"
@ -50,6 +46,11 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
// Check for a preview
if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
http.ServeFile(w, r, "static/images/pixel.png")
return
}
rs := ctx.Get(r, "result").(models.Result) rs := ctx.Get(r, "result").(models.Result)
d := ctx.Get(r, "details").(models.EventDetails) d := ctx.Get(r, "details").(models.EventDetails)
err = rs.HandleEmailOpened(d) err = rs.HandleEmailOpened(d)
@ -70,6 +71,11 @@ func PhishReporter(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
// Check for a preview
if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
w.WriteHeader(http.StatusNoContent)
return
}
rs := ctx.Get(r, "result").(models.Result) rs := ctx.Get(r, "result").(models.Result)
d := ctx.Get(r, "details").(models.EventDetails) d := ctx.Get(r, "details").(models.EventDetails)
@ -92,6 +98,24 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
var ptx models.PhishingTemplateContext
// Check for a preview
if preview, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
ptx, err = models.NewPhishingTemplateContext(&preview, preview.BaseRecipient, preview.RId)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
p, err := models.GetPage(preview.PageId, preview.UserId)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
renderPhishResponse(w, r, ptx, p)
return
}
rs := ctx.Get(r, "result").(models.Result) rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign) c := ctx.Get(r, "campaign").(models.Campaign)
d := ctx.Get(r, "details").(models.EventDetails) d := ctx.Get(r, "details").(models.EventDetails)
@ -112,49 +136,35 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
// Redirect to the desired page }
ptx, err = models.NewPhishingTemplateContext(&c, rs.BaseRecipient, rs.RId)
if err != nil {
log.Error(err)
http.NotFound(w, r)
}
renderPhishResponse(w, r, ptx, p)
}
// renderPhishResponse handles rendering the correct response to the phishing
// connection. This usually involves writing out the page HTML or redirecting
// the user to the correct URL.
func renderPhishResponse(w http.ResponseWriter, r *http.Request, ptx models.PhishingTemplateContext, p models.Page) {
// If the request was a form submit and a redirect URL was specified, we
// should send the user to that URL
if r.Method == "POST" {
if p.RedirectURL != "" { if p.RedirectURL != "" {
http.Redirect(w, r, p.RedirectURL, 302) http.Redirect(w, r, p.RedirectURL, 302)
return return
} }
} }
var htmlBuff bytes.Buffer // Otherwise, we just need to write out the templated HTML
tmpl, err := template.New("html_template").Parse(p.HTML) html, err := models.ExecuteTemplate(p.HTML, ptx)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
f, err := mail.ParseAddress(c.SMTP.FromAddress) w.Write([]byte(html))
if err != nil {
log.Error(err)
}
fn := f.Name
if fn == "" {
fn = f.Address
}
phishURL, _ := url.Parse(c.URL)
q := phishURL.Query()
q.Set(models.RecipientParameter, rs.RId)
phishURL.RawQuery = q.Encode()
rsf := struct {
models.Result
URL string
From string
}{
rs,
phishURL.String(),
fn,
}
err = tmpl.Execute(&htmlBuff, rsf)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
w.Write(htmlBuff.Bytes())
} }
// RobotsHandler prevents search engines, etc. from indexing phishing materials // RobotsHandler prevents search engines, etc. from indexing phishing materials
@ -173,6 +183,15 @@ func setupContext(r *http.Request) (error, *http.Request) {
if id == "" { if id == "" {
return ErrInvalidRequest, r return ErrInvalidRequest, r
} }
// Check to see if this is a preview or a real result
if strings.HasPrefix(id, models.PreviewPrefix) {
rs, err := models.GetEmailRequestByResultId(id)
if err != nil {
return err, r
}
r = ctx.Set(r, "result", rs)
return nil, r
}
rs, err := models.GetResult(id) rs, err := models.GetResult(id)
if err != nil { if err != nil {
return err, r return err, r

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
@ -15,6 +16,23 @@ func (s *ControllersSuite) getFirstCampaign() models.Campaign {
return campaigns[0] return campaigns[0]
} }
func (s *ControllersSuite) getFirstEmailRequest() models.EmailRequest {
campaign := s.getFirstCampaign()
req := models.EmailRequest{
TemplateId: campaign.TemplateId,
Template: campaign.Template,
PageId: campaign.PageId,
Page: campaign.Page,
URL: "http://localhost.localdomain",
UserId: 1,
BaseRecipient: campaign.Results[0].BaseRecipient,
SMTP: campaign.SMTP,
}
err := models.PostEmailRequest(&req)
s.Nil(err)
return req
}
func (s *ControllersSuite) openEmail(rid string) { func (s *ControllersSuite) openEmail(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid)) resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid))
s.Nil(err) s.Nil(err)
@ -40,13 +58,14 @@ func (s *ControllersSuite) openEmail404(rid string) {
s.Equal(resp.StatusCode, http.StatusNotFound) s.Equal(resp.StatusCode, http.StatusNotFound)
} }
func (s *ControllersSuite) clickLink(rid string, campaign models.Campaign) { func (s *ControllersSuite) clickLink(rid string, expectedHTML string) {
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", ps.URL, models.RecipientParameter, rid)) resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", ps.URL, models.RecipientParameter, rid))
s.Nil(err) s.Nil(err)
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
s.Nil(err) s.Nil(err)
s.Equal(bytes.Compare(body, []byte(campaign.Page.HTML)), 0) log.Printf("%s\n\n\n", body)
s.Equal(bytes.Compare(body, []byte(expectedHTML)), 0)
} }
func (s *ControllersSuite) clickLink404(rid string) { func (s *ControllersSuite) clickLink404(rid string) {
@ -93,7 +112,7 @@ func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
s.Equal(result.Status, models.STATUS_SENDING) s.Equal(result.Status, models.STATUS_SENDING)
s.openEmail(result.RId) s.openEmail(result.RId)
s.clickLink(result.RId, campaign) s.clickLink(result.RId, campaign.Page.HTML)
campaign = s.getFirstCampaign() campaign = s.getFirstCampaign()
result = campaign.Results[0] result = campaign.Results[0]
@ -153,3 +172,19 @@ func (s *ControllersSuite) TestRobotsHandler() {
s.Nil(err) s.Nil(err)
s.Equal(bytes.Compare(body, expected), 0) s.Equal(bytes.Compare(body, expected), 0)
} }
func (s *ControllersSuite) TestInvalidPreviewID() {
bogusRId := fmt.Sprintf("%sbogus", models.PreviewPrefix)
s.openEmail404(bogusRId)
s.clickLink404(bogusRId)
}
func (s *ControllersSuite) TestPreviewTrack() {
req := s.getFirstEmailRequest()
s.openEmail(req.RId)
}
func (s *ControllersSuite) TestPreviewClick() {
req := s.getFirstEmailRequest()
s.clickLink(req.RId, req.Page.HTML)
}

View File

@ -0,0 +1,21 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE IF NOT EXISTS email_requests (
`id` integer primary key auto_increment,
`user_id` integer,
`template_id` integer,
`page_id` integer,
`first_name` varchar(255),
`last_name` varchar(255),
`email` varchar(255),
`position` varchar(255),
`url` varchar(255),
`r_id` varchar(255),
`from_address` varchar(255)
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE email_requests

View File

@ -0,0 +1,21 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE IF NOT EXISTS "email_requests" (
"id" integer primary key autoincrement,
"user_id" integer,
"template_id" integer,
"page_id" integer,
"first_name" varchar(255),
"last_name" varchar(255),
"email" varchar(255),
"position" varchar(255),
"url" varchar(255),
"r_id" varchar(255),
"from_address" varchar(255)
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View File

@ -206,6 +206,18 @@ func (c *Campaign) getDetails() error {
return nil return nil
} }
// getBaseURL returns the Campaign's configured URL.
// This is used to implement the TemplateContext interface.
func (c *Campaign) getBaseURL() string {
return c.URL
}
// getFromAddress returns the Campaign's configured SMTP "From" address.
// This is used to implement the TemplateContext interface.
func (c *Campaign) getFromAddress() string {
return c.SMTP.FromAddress
}
// 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) {
@ -452,13 +464,15 @@ func PostCampaign(c *Campaign, uid int64) error {
} }
resultMap[t.Email] = true resultMap[t.Email] = true
r := &Result{ r := &Result{
Email: t.Email, BaseRecipient: BaseRecipient{
Position: t.Position, Email: t.Email,
Position: t.Position,
FirstName: t.FirstName,
LastName: t.LastName,
},
Status: STATUS_SCHEDULED, Status: STATUS_SCHEDULED,
CampaignId: c.Id, CampaignId: c.Id,
UserId: c.UserId, UserId: c.UserId,
FirstName: t.FirstName,
LastName: t.LastName,
SendDate: c.LaunchDate, SendDate: c.LaunchDate,
Reported: false, Reported: false,
ModifiedDate: c.CreatedDate, ModifiedDate: c.CreatedDate,

View File

@ -12,55 +12,94 @@ import (
"github.com/gophish/gophish/mailer" "github.com/gophish/gophish/mailer"
) )
// SendTestEmailRequest is the structure of a request // PreviewPrefix is the standard prefix added to the rid parameter when sending
// test emails.
const PreviewPrefix = "preview-"
// EmailRequest is the structure of a request
// to send a test email to test an SMTP connection. // to send a test email to test an SMTP connection.
// This type implements the mailer.Mail interface. // This type implements the mailer.Mail interface.
type SendTestEmailRequest struct { type EmailRequest struct {
Template Template `json:"template"` Id int64 `json:"-"`
Page Page `json:"page"` Template Template `json:"template"`
SMTP SMTP `json:"smtp"` TemplateId int64 `json:"-"`
URL string `json:"url"` Page Page `json:"page"`
Tracker string `json:"tracker"` PageId int64 `json:"-"`
TrackingURL string `json:"tracking_url"` SMTP SMTP `json:"smtp"`
From string `json:"from"` URL string `json:"url"`
Target Tracker string `json:"tracker" gorm:"-"`
ErrorChan chan (error) `json:"-"` TrackingURL string `json:"tracking_url" gorm:"-"`
UserId int64 `json:"-"`
ErrorChan chan (error) `json:"-" gorm:"-"`
RId string `json:"id"`
FromAddress string `json:"-"`
BaseRecipient
}
func (s *EmailRequest) getBaseURL() string {
return s.URL
}
func (s *EmailRequest) getFromAddress() string {
return s.FromAddress
} }
// Validate ensures the SendTestEmailRequest structure // Validate ensures the SendTestEmailRequest structure
// is valid. // is valid.
func (s *SendTestEmailRequest) Validate() error { func (s *EmailRequest) Validate() error {
switch { switch {
case s.Email == "": case s.Email == "":
return ErrEmailNotSpecified return ErrEmailNotSpecified
case s.FromAddress == "" && s.SMTP.FromAddress == "":
return ErrFromAddressNotSpecified
} }
return nil return nil
} }
// Backoff treats temporary errors as permanent since this is expected to be a // Backoff treats temporary errors as permanent since this is expected to be a
// synchronous operation. It returns any errors given back to the ErrorChan // synchronous operation. It returns any errors given back to the ErrorChan
func (s *SendTestEmailRequest) Backoff(reason error) error { func (s *EmailRequest) Backoff(reason error) error {
s.ErrorChan <- reason s.ErrorChan <- reason
return nil return nil
} }
// Error returns an error on the ErrorChan. // Error returns an error on the ErrorChan.
func (s *SendTestEmailRequest) Error(err error) error { func (s *EmailRequest) Error(err error) error {
s.ErrorChan <- err s.ErrorChan <- err
return nil return nil
} }
// Success returns nil on the ErrorChan to indicate that the email was sent // Success returns nil on the ErrorChan to indicate that the email was sent
// successfully. // successfully.
func (s *SendTestEmailRequest) Success() error { func (s *EmailRequest) Success() error {
s.ErrorChan <- nil s.ErrorChan <- nil
return nil return nil
} }
// PostEmailRequest stores a SendTestEmailRequest in the database.
func PostEmailRequest(s *EmailRequest) error {
// Generate an ID to be used in the underlying Result object
rid, err := generateResultId()
if err != nil {
return err
}
s.RId = fmt.Sprintf("%s%s", PreviewPrefix, rid)
s.FromAddress = s.SMTP.FromAddress
return db.Save(&s).Error
}
// GetEmailRequestByResultId retrieves the EmailRequest by the underlying rid
// parameter.
func GetEmailRequestByResultId(id string) (EmailRequest, error) {
s := EmailRequest{}
err := db.Table("email_requests").Where("r_id=?", id).First(&s).Error
return s, err
}
// Generate fills in the details of a gomail.Message with the contents // Generate fills in the details of a gomail.Message with the contents
// from the SendTestEmailRequest. // from the SendTestEmailRequest.
func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error { func (s *EmailRequest) Generate(msg *gomail.Message) error {
f, err := mail.ParseAddress(s.SMTP.FromAddress) f, err := mail.ParseAddress(s.FromAddress)
if err != nil { if err != nil {
return err return err
} }
@ -70,7 +109,12 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
} }
msg.SetAddressHeader("From", f.Address, f.Name) msg.SetAddressHeader("From", f.Address, f.Name)
url, err := buildTemplate(s.URL, s) ptx, err := NewPhishingTemplateContext(s, s.BaseRecipient, s.RId)
if err != nil {
return err
}
url, err := ExecuteTemplate(s.URL, ptx)
if err != nil { if err != nil {
return err return err
} }
@ -78,12 +122,12 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
// Parse the customHeader templates // Parse the customHeader templates
for _, header := range s.SMTP.Headers { for _, header := range s.SMTP.Headers {
key, err := buildTemplate(header.Key, s) key, err := ExecuteTemplate(header.Key, ptx)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
value, err := buildTemplate(header.Value, s) value, err := ExecuteTemplate(header.Value, ptx)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
@ -93,7 +137,7 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
} }
// Parse remaining templates // Parse remaining templates
subject, err := buildTemplate(s.Template.Subject, s) subject, err := ExecuteTemplate(s.Template.Subject, ptx)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
@ -104,14 +148,14 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
msg.SetHeader("To", s.FormatAddress()) msg.SetHeader("To", s.FormatAddress())
if s.Template.Text != "" { if s.Template.Text != "" {
text, err := buildTemplate(s.Template.Text, s) text, err := ExecuteTemplate(s.Template.Text, ptx)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
msg.SetBody("text/plain", text) msg.SetBody("text/plain", text)
} }
if s.Template.HTML != "" { if s.Template.HTML != "" {
html, err := buildTemplate(s.Template.HTML, s) html, err := ExecuteTemplate(s.Template.HTML, ptx)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
@ -137,6 +181,6 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
} }
// GetDialer returns the mailer.Dialer for the underlying SMTP object // GetDialer returns the mailer.Dialer for the underlying SMTP object
func (s *SendTestEmailRequest) GetDialer() (mailer.Dialer, error) { func (s *EmailRequest) GetDialer() (mailer.Dialer, error) {
return s.SMTP.GetDialer() return s.SMTP.GetDialer()
} }

View File

@ -11,14 +11,16 @@ import (
) )
func (s *ModelsSuite) TestEmailNotPresent(ch *check.C) { func (s *ModelsSuite) TestEmailNotPresent(ch *check.C) {
req := &SendTestEmailRequest{} req := &EmailRequest{}
ch.Assert(req.Validate(), check.Equals, ErrEmailNotSpecified) ch.Assert(req.Validate(), check.Equals, ErrEmailNotSpecified)
req.Email = "test@example.com" req.Email = "test@example.com"
ch.Assert(req.Validate(), check.Equals, ErrFromAddressNotSpecified)
req.FromAddress = "from@example.com"
ch.Assert(req.Validate(), check.Equals, nil) ch.Assert(req.Validate(), check.Equals, nil)
} }
func (s *ModelsSuite) TestEmailRequestBackoff(ch *check.C) { func (s *ModelsSuite) TestEmailRequestBackoff(ch *check.C) {
req := &SendTestEmailRequest{ req := &EmailRequest{
ErrorChan: make(chan error), ErrorChan: make(chan error),
} }
expected := errors.New("Temporary Error") expected := errors.New("Temporary Error")
@ -30,7 +32,7 @@ func (s *ModelsSuite) TestEmailRequestBackoff(ch *check.C) {
} }
func (s *ModelsSuite) TestEmailRequestError(ch *check.C) { func (s *ModelsSuite) TestEmailRequestError(ch *check.C) {
req := &SendTestEmailRequest{ req := &EmailRequest{
ErrorChan: make(chan error), ErrorChan: make(chan error),
} }
expected := errors.New("Temporary Error") expected := errors.New("Temporary Error")
@ -42,7 +44,7 @@ func (s *ModelsSuite) TestEmailRequestError(ch *check.C) {
} }
func (s *ModelsSuite) TestEmailRequestSuccess(ch *check.C) { func (s *ModelsSuite) TestEmailRequestSuccess(ch *check.C) {
req := &SendTestEmailRequest{ req := &EmailRequest{
ErrorChan: make(chan error), ErrorChan: make(chan error),
} }
go func() { go func() {
@ -62,15 +64,15 @@ func (s *ModelsSuite) TestEmailRequestGenerate(ch *check.C) {
Text: "{{.Email}} - Text", Text: "{{.Email}} - Text",
HTML: "{{.Email}} - HTML", HTML: "{{.Email}} - HTML",
} }
target := Target{ req := &EmailRequest{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
}
req := &SendTestEmailRequest{
SMTP: smtp, SMTP: smtp,
Template: template, Template: template,
Target: target, BaseRecipient: BaseRecipient{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
},
FromAddress: smtp.FromAddress,
} }
msg := gomail.NewMessage() msg := gomail.NewMessage()
@ -104,23 +106,24 @@ func (s *ModelsSuite) TestEmailRequestURLTemplating(ch *check.C) {
Text: "{{.URL}}", Text: "{{.URL}}",
HTML: "{{.URL}}", HTML: "{{.URL}}",
} }
target := Target{ req := &EmailRequest{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
}
req := &SendTestEmailRequest{
SMTP: smtp, SMTP: smtp,
Template: template, Template: template,
Target: target, URL: "http://127.0.0.1/{{.Email}}",
URL: "http://127.0.0.1/{{.Email}}", BaseRecipient: BaseRecipient{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
},
FromAddress: smtp.FromAddress,
RId: fmt.Sprintf("%s-foobar", PreviewPrefix),
} }
msg := gomail.NewMessage() msg := gomail.NewMessage()
err = req.Generate(msg) err = req.Generate(msg)
ch.Assert(err, check.Equals, nil) ch.Assert(err, check.Equals, nil)
expectedURL := fmt.Sprintf("http://127.0.0.1/%s", target.Email) expectedURL := fmt.Sprintf("http://127.0.0.1/%s?%s=%s", req.Email, RecipientParameter, req.RId)
msgBuff := &bytes.Buffer{} msgBuff := &bytes.Buffer{}
_, err = msg.WriteTo(msgBuff) _, err = msg.WriteTo(msgBuff)
@ -142,15 +145,15 @@ func (s *ModelsSuite) TestEmailRequestGenerateEmptySubject(ch *check.C) {
Text: "{{.Email}} - Text", Text: "{{.Email}} - Text",
HTML: "{{.Email}} - HTML", HTML: "{{.Email}} - HTML",
} }
target := Target{ req := &EmailRequest{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
}
req := &SendTestEmailRequest{
SMTP: smtp, SMTP: smtp,
Template: template, Template: template,
Target: target, BaseRecipient: BaseRecipient{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
},
FromAddress: smtp.FromAddress,
} }
msg := gomail.NewMessage() msg := gomail.NewMessage()
@ -171,3 +174,44 @@ func (s *ModelsSuite) TestEmailRequestGenerateEmptySubject(ch *check.C) {
ch.Assert(err, check.Equals, nil) ch.Assert(err, check.Equals, nil)
ch.Assert(got.Subject, check.Equals, expected.Subject) ch.Assert(got.Subject, check.Equals, expected.Subject)
} }
func (s *ModelsSuite) TestPostSendTestEmailRequest(ch *check.C) {
smtp := SMTP{
FromAddress: "from@example.com",
}
template := Template{
Name: "Test Template",
Subject: "",
Text: "{{.Email}} - Text",
HTML: "{{.Email}} - HTML",
UserId: 1,
}
err := PostTemplate(&template)
ch.Assert(err, check.Equals, nil)
page := Page{
Name: "Test Page",
HTML: "test",
UserId: 1,
}
err = PostPage(&page)
ch.Assert(err, check.Equals, nil)
req := &EmailRequest{
SMTP: smtp,
TemplateId: template.Id,
PageId: page.Id,
BaseRecipient: BaseRecipient{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
},
}
err = PostEmailRequest(req)
ch.Assert(err, check.Equals, nil)
got, err := GetEmailRequestByResultId(req.RId)
ch.Assert(err, check.Equals, nil)
ch.Assert(got.RId, check.Equals, req.RId)
ch.Assert(got.Email, check.Equals, req.Email)
}

View File

@ -46,14 +46,33 @@ type GroupTarget struct {
// Target contains the fields needed for individual targets specified by the user // Target contains the fields needed for individual targets specified by the user
// Groups contain 1..* Targets, but 1 Target may belong to 1..* Groups // Groups contain 1..* Targets, but 1 Target may belong to 1..* Groups
type Target struct { type Target struct {
Id int64 `json:"-"` Id int64 `json:"-"`
BaseRecipient
}
// BaseRecipient contains the fields for a single recipient. This is the base
// struct used in members of groups and campaign results.
type BaseRecipient struct {
Email string `json:"email"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Email string `json:"email"`
Position string `json:"position"` Position string `json:"position"`
} }
// Returns the email address to use in the "To" header of the email // FormatAddress returns the email address to use in the "To" header of the email
func (r *BaseRecipient) FormatAddress() string {
addr := r.Email
if r.FirstName != "" && r.LastName != "" {
a := &mail.Address{
Name: fmt.Sprintf("%s %s", r.FirstName, r.LastName),
Address: r.Email,
}
addr = a.String()
}
return addr
}
// FormatAddress returns the email address to use in the "To" header of the email
func (t *Target) FormatAddress() string { func (t *Target) FormatAddress() string {
addr := t.Email addr := t.Email
if t.FirstName != "" && t.LastName != "" { if t.FirstName != "" && t.LastName != "" {
@ -66,7 +85,7 @@ func (t *Target) FormatAddress() string {
return addr return addr
} }
// ErrNoEmailSpecified is thrown when no email is specified for the Target // ErrEmailNotSpecified is thrown when no email is specified for the Target
var ErrEmailNotSpecified = errors.New("No email address specified") var ErrEmailNotSpecified = errors.New("No email address specified")
// ErrGroupNameNotSpecified is thrown when a group name is not specified // ErrGroupNameNotSpecified is thrown when a group name is not specified
@ -274,11 +293,12 @@ func insertTargetIntoGroup(t Target, gid int64) error {
return err return err
} }
trans := db.Begin() trans := db.Begin()
trans.Where(t).FirstOrCreate(&t) err = trans.Where(t).FirstOrCreate(&t).Error
if err != nil { if err != nil {
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"email": t.Email, "email": t.Email,
}).Error("Error adding target") }).Error(err)
trans.Rollback()
return err return err
} }
err = trans.Where("group_id=? and target_id=?", gid, t.Id).Find(&GroupTarget{}).Error err = trans.Where("group_id=? and target_id=?", gid, t.Id).Find(&GroupTarget{}).Error
@ -286,6 +306,7 @@ func insertTargetIntoGroup(t Target, gid int64) error {
err = trans.Save(&GroupTarget{GroupId: gid, TargetId: t.Id}).Error err = trans.Save(&GroupTarget{GroupId: gid, TargetId: t.Id}).Error
if err != nil { if err != nil {
log.Error(err) log.Error(err)
trans.Rollback()
return err return err
} }
} }
@ -293,10 +314,12 @@ func insertTargetIntoGroup(t Target, gid int64) error {
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"email": t.Email, "email": t.Email,
}).Error("Error adding many-many mapping") }).Error("Error adding many-many mapping")
trans.Rollback()
return err return err
} }
err = trans.Commit().Error err = trans.Commit().Error
if err != nil { if err != nil {
trans.Rollback()
log.Error("Error committing db changes") log.Error("Error committing db changes")
return err return err
} }

View File

@ -7,7 +7,7 @@ import (
func (s *ModelsSuite) TestPostGroup(c *check.C) { func (s *ModelsSuite) TestPostGroup(c *check.C) {
g := Group{Name: "Test Group"} g := Group{Name: "Test Group"}
g.Targets = []Target{Target{Email: "test@example.com"}} g.Targets = []Target{Target{BaseRecipient: BaseRecipient{Email: "test@example.com"}}}
g.UserId = 1 g.UserId = 1
err := PostGroup(&g) err := PostGroup(&g)
c.Assert(err, check.Equals, nil) c.Assert(err, check.Equals, nil)
@ -17,7 +17,7 @@ func (s *ModelsSuite) TestPostGroup(c *check.C) {
func (s *ModelsSuite) TestPostGroupNoName(c *check.C) { func (s *ModelsSuite) TestPostGroupNoName(c *check.C) {
g := Group{Name: ""} g := Group{Name: ""}
g.Targets = []Target{Target{Email: "test@example.com"}} g.Targets = []Target{Target{BaseRecipient: BaseRecipient{Email: "test@example.com"}}}
g.UserId = 1 g.UserId = 1
err := PostGroup(&g) err := PostGroup(&g)
c.Assert(err, check.Equals, ErrGroupNameNotSpecified) c.Assert(err, check.Equals, ErrGroupNameNotSpecified)
@ -34,14 +34,22 @@ func (s *ModelsSuite) TestPostGroupNoTargets(c *check.C) {
func (s *ModelsSuite) TestGetGroups(c *check.C) { func (s *ModelsSuite) TestGetGroups(c *check.C) {
// Add groups. // Add groups.
PostGroup(&Group{ PostGroup(&Group{
Name: "Test Group 1", Name: "Test Group 1",
Targets: []Target{Target{Email: "test1@example.com"}}, Targets: []Target{
UserId: 1, Target{
BaseRecipient: BaseRecipient{Email: "test1@example.com"},
},
},
UserId: 1,
}) })
PostGroup(&Group{ PostGroup(&Group{
Name: "Test Group 2", Name: "Test Group 2",
Targets: []Target{Target{Email: "test2@example.com"}}, Targets: []Target{
UserId: 1, Target{
BaseRecipient: BaseRecipient{Email: "test2@example.com"},
},
},
UserId: 1,
}) })
// Get groups and test result. // Get groups and test result.
@ -65,9 +73,13 @@ func (s *ModelsSuite) TestGetGroupsNoGroups(c *check.C) {
func (s *ModelsSuite) TestGetGroup(c *check.C) { func (s *ModelsSuite) TestGetGroup(c *check.C) {
// Add group. // Add group.
originalGroup := &Group{ originalGroup := &Group{
Name: "Test Group", Name: "Test Group",
Targets: []Target{Target{Email: "test@example.com"}}, Targets: []Target{
UserId: 1, Target{
BaseRecipient: BaseRecipient{Email: "test@example.com"},
},
},
UserId: 1,
} }
c.Assert(PostGroup(originalGroup), check.Equals, nil) c.Assert(PostGroup(originalGroup), check.Equals, nil)
@ -87,9 +99,13 @@ func (s *ModelsSuite) TestGetGroupNoGroups(c *check.C) {
func (s *ModelsSuite) TestGetGroupByName(c *check.C) { func (s *ModelsSuite) TestGetGroupByName(c *check.C) {
// Add group. // Add group.
PostGroup(&Group{ PostGroup(&Group{
Name: "Test Group", Name: "Test Group",
Targets: []Target{Target{Email: "test@example.com"}}, Targets: []Target{
UserId: 1, Target{
BaseRecipient: BaseRecipient{Email: "test@example.com"},
},
},
UserId: 1,
}) })
// Get group and test result. // Get group and test result.
@ -109,8 +125,8 @@ func (s *ModelsSuite) TestPutGroup(c *check.C) {
// Add test group. // Add test group.
group := Group{Name: "Test Group"} group := Group{Name: "Test Group"}
group.Targets = []Target{ group.Targets = []Target{
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
} }
group.UserId = 1 group.UserId = 1
PostGroup(&group) PostGroup(&group)
@ -134,8 +150,8 @@ func (s *ModelsSuite) TestPutGroupEmptyAttribute(c *check.C) {
// Add test group. // Add test group.
group := Group{Name: "Test Group"} group := Group{Name: "Test Group"}
group.Targets = []Target{ group.Targets = []Target{
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
} }
group.UserId = 1 group.UserId = 1
PostGroup(&group) PostGroup(&group)

View File

@ -1,17 +1,13 @@
package models package models
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math" "math"
"net/mail" "net/mail"
"net/url"
"path"
"strings" "strings"
"text/template"
"time" "time"
"github.com/gophish/gomail" "github.com/gophish/gomail"
@ -136,18 +132,6 @@ func (m *MailLog) GetDialer() (mailer.Dialer, error) {
return c.SMTP.GetDialer() return c.SMTP.GetDialer()
} }
// buildTemplate creates a templated string based on the provided
// template body and data.
func buildTemplate(text string, data interface{}) (string, error) {
buff := bytes.Buffer{}
tmpl, err := template.New("template").Parse(text)
if err != nil {
return buff.String(), err
}
err = tmpl.Execute(&buff, data)
return buff.String(), err
}
// Generate fills in the details of a gomail.Message instance with // Generate fills in the details of a gomail.Message instance with
// the correct headers and body from the campaign and recipient listed in // the correct headers and body from the campaign and recipient listed in
// the maillog. We accept the gomail.Message as an argument so that the caller // the maillog. We accept the gomail.Message as an argument so that the caller
@ -161,51 +145,26 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
if err != nil { if err != nil {
return err return err
} }
f, err := mail.ParseAddress(c.SMTP.FromAddress) f, err := mail.ParseAddress(c.SMTP.FromAddress)
if err != nil { if err != nil {
return err return err
} }
fn := f.Name
if fn == "" {
fn = f.Address
}
msg.SetAddressHeader("From", f.Address, f.Name) msg.SetAddressHeader("From", f.Address, f.Name)
campaignURL, err := buildTemplate(c.URL, r)
ptx, err := NewPhishingTemplateContext(&c, r.BaseRecipient, r.RId)
if err != nil { if err != nil {
return err return err
} }
phishURL, _ := url.Parse(campaignURL)
q := phishURL.Query()
q.Set("rid", r.RId)
phishURL.RawQuery = q.Encode()
trackingURL, _ := url.Parse(campaignURL)
trackingURL.Path = path.Join(trackingURL.Path, "/track")
trackingURL.RawQuery = q.Encode()
td := struct {
Result
URL string
TrackingURL string
Tracker string
From string
}{
r,
phishURL.String(),
trackingURL.String(),
"<img alt='' style='display: none' src='" + trackingURL.String() + "'/>",
fn,
}
// Parse the customHeader templates // Parse the customHeader templates
for _, header := range c.SMTP.Headers { for _, header := range c.SMTP.Headers {
key, err := buildTemplate(header.Key, td) key, err := ExecuteTemplate(header.Key, ptx)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
} }
value, err := buildTemplate(header.Value, td) value, err := ExecuteTemplate(header.Value, ptx)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
} }
@ -215,7 +174,7 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
} }
// Parse remaining templates // Parse remaining templates
subject, err := buildTemplate(c.Template.Subject, td) subject, err := ExecuteTemplate(c.Template.Subject, ptx)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
} }
@ -226,14 +185,14 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
msg.SetHeader("To", r.FormatAddress()) msg.SetHeader("To", r.FormatAddress())
if c.Template.Text != "" { if c.Template.Text != "" {
text, err := buildTemplate(c.Template.Text, td) text, err := ExecuteTemplate(c.Template.Text, ptx)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
} }
msg.SetBody("text/plain", text) msg.SetBody("text/plain", text)
} }
if c.Template.HTML != "" { if c.Template.HTML != "" {
html, err := buildTemplate(c.Template.HTML, td) html, err := ExecuteTemplate(c.Template.HTML, ptx)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
} }

View File

@ -45,8 +45,8 @@ func (s *ModelsSuite) createCampaignDependencies(ch *check.C, optional ...string
// we use the optional parameter to pass an alternative subject // we use the optional parameter to pass an alternative subject
group := Group{Name: "Test Group"} group := Group{Name: "Test Group"}
group.Targets = []Target{ group.Targets = []Target{
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, Target{BaseRecipient: BaseRecipient{Email: "test2@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

@ -3,10 +3,8 @@ package models
import ( import (
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"fmt"
"math/big" "math/big"
"net" "net"
"net/mail"
"time" "time"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
@ -30,10 +28,6 @@ type Result struct {
CampaignId int64 `json:"-"` CampaignId int64 `json:"-"`
UserId int64 `json:"-"` UserId int64 `json:"-"`
RId string `json:"id"` RId string `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Position string `json:"position"`
Status string `json:"status" sql:"not null"` Status string `json:"status" sql:"not null"`
IP string `json:"ip"` IP string `json:"ip"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
@ -41,6 +35,7 @@ type Result struct {
SendDate time.Time `json:"send_date"` SendDate time.Time `json:"send_date"`
Reported bool `json:"reported" sql:"not null"` Reported bool `json:"reported" sql:"not null"`
ModifiedDate time.Time `json:"modified_date"` ModifiedDate time.Time `json:"modified_date"`
BaseRecipient
} }
func (r *Result) createEvent(status string, details interface{}) (*Event, error) { func (r *Result) createEvent(status string, details interface{}) (*Event, error) {
@ -178,22 +173,30 @@ func (r *Result) UpdateGeo(addr string) error {
return db.Save(r).Error return db.Save(r).Error
} }
func generateResultId() (string, error) {
const alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
k := make([]byte, 7)
for i := range k {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphaNum))))
if err != nil {
return "", err
}
k[i] = alphaNum[idx.Int64()]
}
return string(k), nil
}
// GenerateId generates a unique key to represent the result // GenerateId generates a unique key to represent the result
// in the database // in the database
func (r *Result) GenerateId() error { func (r *Result) GenerateId() error {
// Keep trying until we generate a unique key (shouldn't take more than one or two iterations) // Keep trying until we generate a unique key (shouldn't take more than one or two iterations)
const alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
k := make([]byte, 7)
for { for {
for i := range k { rid, err := generateResultId()
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphaNum)))) if err != nil {
if err != nil { return err
return err
}
k[i] = alphaNum[idx.Int64()]
} }
r.RId = string(k) r.RId = rid
err := db.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error err = db.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
break break
} }
@ -201,19 +204,6 @@ func (r *Result) GenerateId() error {
return nil return nil
} }
// FormatAddress returns the email address to use in the "To" header of the email
func (r *Result) FormatAddress() string {
addr := r.Email
if r.FirstName != "" && r.LastName != "" {
a := &mail.Address{
Name: fmt.Sprintf("%s %s", r.FirstName, r.LastName),
Address: r.Email,
}
addr = a.String()
}
return addr
}
// GetResult returns the Result object from the database // GetResult returns the Result object from the database
// given the ResultId // given the ResultId
func GetResult(rid string) (Result, error) { func GetResult(rid string) (Result, error) {

View File

@ -18,9 +18,11 @@ func (s *ModelsSuite) TestGenerateResultId(c *check.C) {
func (s *ModelsSuite) TestFormatAddress(c *check.C) { func (s *ModelsSuite) TestFormatAddress(c *check.C) {
r := Result{ r := Result{
FirstName: "John", BaseRecipient: BaseRecipient{
LastName: "Doe", FirstName: "John",
Email: "johndoe@example.com", LastName: "Doe",
Email: "johndoe@example.com",
},
} }
expected := &mail.Address{ expected := &mail.Address{
Name: "John Doe", Name: "John Doe",
@ -29,7 +31,7 @@ func (s *ModelsSuite) TestFormatAddress(c *check.C) {
c.Assert(r.FormatAddress(), check.Equals, expected.String()) c.Assert(r.FormatAddress(), check.Equals, expected.String())
r = Result{ r = Result{
Email: "johndoe@example.com", BaseRecipient: BaseRecipient{Email: "johndoe@example.com"},
} }
c.Assert(r.FormatAddress(), check.Equals, r.Email) c.Assert(r.FormatAddress(), check.Equals, r.Email)
} }
@ -59,9 +61,9 @@ func (s *ModelsSuite) TestResultScheduledStatus(ch *check.C) {
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{
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{Email: "test1@example.com", FirstName: "Duplicate", LastName: "Duplicate"}, Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "Duplicate", LastName: "Duplicate"}},
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, Target{BaseRecipient: BaseRecipient{Email: "test2@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,9 +1,7 @@
package models package models
import ( import (
"bytes"
"errors" "errors"
"html/template"
"time" "time"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
@ -36,7 +34,6 @@ func (t *Template) Validate() error {
case t.Text == "" && t.HTML == "": case t.Text == "" && t.HTML == "":
return ErrTemplateMissingParameter return ErrTemplateMissingParameter
} }
var buff bytes.Buffer
// Test that the variables used in the template // Test that the variables used in the template
// validate with no issues // validate with no issues
td := struct { td := struct {
@ -47,31 +44,27 @@ func (t *Template) Validate() error {
From string From string
}{ }{
Result{ Result{
Email: "foo@bar.com", BaseRecipient: BaseRecipient{
FirstName: "Foo", Email: "foo@bar.com",
LastName: "Bar", FirstName: "Foo",
Position: "Test", LastName: "Bar",
Position: "Test",
},
}, },
"http://foo.bar", "http://foo.bar",
"http://foo.bar/track", "http://foo.bar/track",
"<img src='http://foo.bar/track", "<img src='http://foo.bar/track",
"John Doe <foo@bar.com>", "John Doe <foo@bar.com>",
} }
tmpl, err := template.New("html_template").Parse(t.HTML) _, err := ExecuteTemplate(t.HTML, td)
if err != nil { if err != nil {
return err return err
} }
err = tmpl.Execute(&buff, td) _, err = ExecuteTemplate(t.Text, td)
if err != nil { if err != nil {
return err return err
} }
return nil
tmpl, err = template.New("text_template").Parse(t.Text)
if err != nil {
return err
}
err = tmpl.Execute(&buff, td)
return err
} }
// GetTemplates returns the templates owned by the given user. // GetTemplates returns the templates owned by the given user.

View File

@ -0,0 +1,74 @@
package models
import (
"bytes"
"net/mail"
"net/url"
"path"
"text/template"
)
// TemplateContext is an interface that allows both campaigns and email
// requests to have a PhishingTemplateContext generated for them.
type TemplateContext interface {
getFromAddress() string
getBaseURL() string
}
// PhishingTemplateContext is the context that is sent to any template, such
// as the email or landing page content.
type PhishingTemplateContext struct {
From string
URL string
Tracker string
TrackingURL string
RId string
BaseRecipient
}
// NewPhishingTemplateContext returns a populated PhishingTemplateContext,
// parsing the correct fields from the provided TemplateContext and recipient.
func NewPhishingTemplateContext(ctx TemplateContext, r BaseRecipient, rid string) (PhishingTemplateContext, error) {
f, err := mail.ParseAddress(ctx.getFromAddress())
if err != nil {
return PhishingTemplateContext{}, err
}
fn := f.Name
if fn == "" {
fn = f.Address
}
baseURL, err := ExecuteTemplate(ctx.getBaseURL(), r)
if err != nil {
return PhishingTemplateContext{}, err
}
phishURL, _ := url.Parse(baseURL)
q := phishURL.Query()
q.Set(RecipientParameter, rid)
phishURL.RawQuery = q.Encode()
trackingURL, _ := url.Parse(baseURL)
trackingURL.Path = path.Join(trackingURL.Path, "/track")
trackingURL.RawQuery = q.Encode()
return PhishingTemplateContext{
BaseRecipient: r,
URL: phishURL.String(),
TrackingURL: trackingURL.String(),
Tracker: "<img alt='' style='display: none' src='" + trackingURL.String() + "'/>",
From: fn,
RId: rid,
}, nil
}
// ExecuteTemplate creates a templated string based on the provided
// template body and data.
func ExecuteTemplate(text string, data interface{}) (string, error) {
buff := bytes.Buffer{}
tmpl, err := template.New("template").Parse(text)
if err != nil {
return buff.String(), err
}
err = tmpl.Execute(&buff, data)
return buff.String(), err
}

View File

@ -100,10 +100,12 @@ func ParseCSV(r *http.Request) ([]models.Target, error) {
ps = record[pi] ps = record[pi]
} }
t := models.Target{ t := models.Target{
FirstName: fn, BaseRecipient: models.BaseRecipient{
LastName: ln, FirstName: fn,
Email: ea, LastName: ln,
Position: ps, Email: ea,
Position: ps,
},
} }
ts = append(ts, t) ts = append(ts, t)
} }

View File

@ -52,9 +52,11 @@ func buildCSVRequest(csvPayload string) (*http.Request, error) {
func (s *UtilSuite) TestParseCSVEmail() { func (s *UtilSuite) TestParseCSVEmail() {
expected := models.Target{ expected := models.Target{
FirstName: "John", BaseRecipient: models.BaseRecipient{
LastName: "Doe", FirstName: "John",
Email: "johndoe@example.com", LastName: "Doe",
Email: "johndoe@example.com",
},
} }
csvPayload := fmt.Sprintf("%s,%s,<%s>", expected.FirstName, expected.LastName, expected.Email) csvPayload := fmt.Sprintf("%s,%s,<%s>", expected.FirstName, expected.LastName, expected.Email)

View File

@ -86,7 +86,7 @@ func (w *Worker) LaunchCampaign(c models.Campaign) {
} }
// SendTestEmail sends a test email // SendTestEmail sends a test email
func (w *Worker) SendTestEmail(s *models.SendTestEmailRequest) error { func (w *Worker) SendTestEmail(s *models.EmailRequest) error {
go func() { go func() {
mailer.Mailer.Queue <- []mailer.Mail{s} mailer.Mailer.Queue <- []mailer.Mail{s}
}() }()

View File

@ -35,8 +35,8 @@ func (s *WorkerSuite) SetupTest() {
// Add a group // Add a group
group := models.Group{Name: "Test Group"} group := models.Group{Name: "Test Group"}
group.Targets = []models.Target{ group.Targets = []models.Target{
models.Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, models.Target{BaseRecipient: models.BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
models.Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, models.Target{BaseRecipient: models.BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
} }
group.UserId = 1 group.UserId = 1
models.PostGroup(&group) models.PostGroup(&group)