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
// and Target given.
func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
s := &models.SendTestEmailRequest{
s := &models.EmailRequest{
ErrorChan: make(chan error),
UserId: ctx.Get(r, "user_id").(int64),
}
if r.Method != "POST" {
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)
return
}
// Validate the given request
if err = s.Validate(); err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
storeRequest := false
// If a Template is not specified use a default
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 .LastName}} Last Name: {{.LastName}}\n{{end}}" +
"{{if .Position}} Position: {{.Position}}\n{{end}}" +
"{{if .TrackingURL}} Tracking URL: {{.TrackingURL}}\n{{end}}" +
"\nNow go send some phish!"
t := models.Template{
Subject: "Default Email from Gophish",
Text: text,
}
s.Template = t
// Try to lookup the Template by name
} else {
// 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 {
log.WithFields(logrus.Fields{
"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)
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 err := s.SMTP.Validate(); err != nil {
// 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
// of caution and assume that the validation failure was more important.
if lookupErr != nil {
@ -716,9 +732,25 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
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
err = Worker.SendTestEmail(s)
if err != nil {
log.Error(err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}

View File

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

View File

@ -1,14 +1,10 @@
package controllers
import (
"bytes"
"errors"
"fmt"
"html/template"
"net"
"net/http"
"net/mail"
"net/url"
"strings"
ctx "github.com/gophish/gophish/context"
@ -50,6 +46,11 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
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)
d := ctx.Get(r, "details").(models.EventDetails)
err = rs.HandleEmailOpened(d)
@ -70,6 +71,11 @@ func PhishReporter(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
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)
d := ctx.Get(r, "details").(models.EventDetails)
@ -92,6 +98,24 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
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)
c := ctx.Get(r, "campaign").(models.Campaign)
d := ctx.Get(r, "details").(models.EventDetails)
@ -112,49 +136,35 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
if err != nil {
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 != "" {
http.Redirect(w, r, p.RedirectURL, 302)
return
}
}
var htmlBuff bytes.Buffer
tmpl, err := template.New("html_template").Parse(p.HTML)
// Otherwise, we just need to write out the templated HTML
html, err := models.ExecuteTemplate(p.HTML, ptx)
if err != nil {
log.Error(err)
http.NotFound(w, r)
return
}
f, err := mail.ParseAddress(c.SMTP.FromAddress)
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())
w.Write([]byte(html))
}
// RobotsHandler prevents search engines, etc. from indexing phishing materials
@ -173,6 +183,15 @@ func setupContext(r *http.Request) (error, *http.Request) {
if id == "" {
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)
if err != nil {
return err, r

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gophish/gophish/models"
@ -15,6 +16,23 @@ func (s *ControllersSuite) getFirstCampaign() models.Campaign {
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) {
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid))
s.Nil(err)
@ -40,13 +58,14 @@ func (s *ControllersSuite) openEmail404(rid string) {
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))
s.Nil(err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
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) {
@ -93,7 +112,7 @@ func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
s.Equal(result.Status, models.STATUS_SENDING)
s.openEmail(result.RId)
s.clickLink(result.RId, campaign)
s.clickLink(result.RId, campaign.Page.HTML)
campaign = s.getFirstCampaign()
result = campaign.Results[0]
@ -153,3 +172,19 @@ func (s *ControllersSuite) TestRobotsHandler() {
s.Nil(err)
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
}
// 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.
// It also backfills numbers as appropriate with a running total, so that the values are aggregated.
func getCampaignStats(cid int64) (CampaignStats, error) {
@ -452,13 +464,15 @@ func PostCampaign(c *Campaign, uid int64) error {
}
resultMap[t.Email] = true
r := &Result{
BaseRecipient: BaseRecipient{
Email: t.Email,
Position: t.Position,
FirstName: t.FirstName,
LastName: t.LastName,
},
Status: STATUS_SCHEDULED,
CampaignId: c.Id,
UserId: c.UserId,
FirstName: t.FirstName,
LastName: t.LastName,
SendDate: c.LaunchDate,
Reported: false,
ModifiedDate: c.CreatedDate,

View File

@ -12,55 +12,94 @@ import (
"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.
// This type implements the mailer.Mail interface.
type SendTestEmailRequest struct {
type EmailRequest struct {
Id int64 `json:"-"`
Template Template `json:"template"`
TemplateId int64 `json:"-"`
Page Page `json:"page"`
PageId int64 `json:"-"`
SMTP SMTP `json:"smtp"`
URL string `json:"url"`
Tracker string `json:"tracker"`
TrackingURL string `json:"tracking_url"`
From string `json:"from"`
Target
ErrorChan chan (error) `json:"-"`
Tracker string `json:"tracker" gorm:"-"`
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
// is valid.
func (s *SendTestEmailRequest) Validate() error {
func (s *EmailRequest) Validate() error {
switch {
case s.Email == "":
return ErrEmailNotSpecified
case s.FromAddress == "" && s.SMTP.FromAddress == "":
return ErrFromAddressNotSpecified
}
return nil
}
// Backoff treats temporary errors as permanent since this is expected to be a
// 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
return nil
}
// Error returns an error on the ErrorChan.
func (s *SendTestEmailRequest) Error(err error) error {
func (s *EmailRequest) Error(err error) error {
s.ErrorChan <- err
return nil
}
// Success returns nil on the ErrorChan to indicate that the email was sent
// successfully.
func (s *SendTestEmailRequest) Success() error {
func (s *EmailRequest) Success() error {
s.ErrorChan <- 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
// from the SendTestEmailRequest.
func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
f, err := mail.ParseAddress(s.SMTP.FromAddress)
func (s *EmailRequest) Generate(msg *gomail.Message) error {
f, err := mail.ParseAddress(s.FromAddress)
if err != nil {
return err
}
@ -70,7 +109,12 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
}
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 {
return err
}
@ -78,12 +122,12 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
// Parse the customHeader templates
for _, header := range s.SMTP.Headers {
key, err := buildTemplate(header.Key, s)
key, err := ExecuteTemplate(header.Key, ptx)
if err != nil {
log.Error(err)
}
value, err := buildTemplate(header.Value, s)
value, err := ExecuteTemplate(header.Value, ptx)
if err != nil {
log.Error(err)
}
@ -93,7 +137,7 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
}
// Parse remaining templates
subject, err := buildTemplate(s.Template.Subject, s)
subject, err := ExecuteTemplate(s.Template.Subject, ptx)
if err != nil {
log.Error(err)
}
@ -104,14 +148,14 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
msg.SetHeader("To", s.FormatAddress())
if s.Template.Text != "" {
text, err := buildTemplate(s.Template.Text, s)
text, err := ExecuteTemplate(s.Template.Text, ptx)
if err != nil {
log.Error(err)
}
msg.SetBody("text/plain", text)
}
if s.Template.HTML != "" {
html, err := buildTemplate(s.Template.HTML, s)
html, err := ExecuteTemplate(s.Template.HTML, ptx)
if err != nil {
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
func (s *SendTestEmailRequest) GetDialer() (mailer.Dialer, error) {
func (s *EmailRequest) GetDialer() (mailer.Dialer, error) {
return s.SMTP.GetDialer()
}

View File

@ -11,14 +11,16 @@ import (
)
func (s *ModelsSuite) TestEmailNotPresent(ch *check.C) {
req := &SendTestEmailRequest{}
req := &EmailRequest{}
ch.Assert(req.Validate(), check.Equals, ErrEmailNotSpecified)
req.Email = "test@example.com"
ch.Assert(req.Validate(), check.Equals, ErrFromAddressNotSpecified)
req.FromAddress = "from@example.com"
ch.Assert(req.Validate(), check.Equals, nil)
}
func (s *ModelsSuite) TestEmailRequestBackoff(ch *check.C) {
req := &SendTestEmailRequest{
req := &EmailRequest{
ErrorChan: make(chan error),
}
expected := errors.New("Temporary Error")
@ -30,7 +32,7 @@ func (s *ModelsSuite) TestEmailRequestBackoff(ch *check.C) {
}
func (s *ModelsSuite) TestEmailRequestError(ch *check.C) {
req := &SendTestEmailRequest{
req := &EmailRequest{
ErrorChan: make(chan error),
}
expected := errors.New("Temporary Error")
@ -42,7 +44,7 @@ func (s *ModelsSuite) TestEmailRequestError(ch *check.C) {
}
func (s *ModelsSuite) TestEmailRequestSuccess(ch *check.C) {
req := &SendTestEmailRequest{
req := &EmailRequest{
ErrorChan: make(chan error),
}
go func() {
@ -62,15 +64,15 @@ func (s *ModelsSuite) TestEmailRequestGenerate(ch *check.C) {
Text: "{{.Email}} - Text",
HTML: "{{.Email}} - HTML",
}
target := Target{
req := &EmailRequest{
SMTP: smtp,
Template: template,
BaseRecipient: BaseRecipient{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
}
req := &SendTestEmailRequest{
SMTP: smtp,
Template: template,
Target: target,
},
FromAddress: smtp.FromAddress,
}
msg := gomail.NewMessage()
@ -104,23 +106,24 @@ func (s *ModelsSuite) TestEmailRequestURLTemplating(ch *check.C) {
Text: "{{.URL}}",
HTML: "{{.URL}}",
}
target := Target{
req := &EmailRequest{
SMTP: smtp,
Template: template,
URL: "http://127.0.0.1/{{.Email}}",
BaseRecipient: BaseRecipient{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
}
req := &SendTestEmailRequest{
SMTP: smtp,
Template: template,
Target: target,
URL: "http://127.0.0.1/{{.Email}}",
},
FromAddress: smtp.FromAddress,
RId: fmt.Sprintf("%s-foobar", PreviewPrefix),
}
msg := gomail.NewMessage()
err = req.Generate(msg)
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{}
_, err = msg.WriteTo(msgBuff)
@ -142,15 +145,15 @@ func (s *ModelsSuite) TestEmailRequestGenerateEmptySubject(ch *check.C) {
Text: "{{.Email}} - Text",
HTML: "{{.Email}} - HTML",
}
target := Target{
req := &EmailRequest{
SMTP: smtp,
Template: template,
BaseRecipient: BaseRecipient{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
}
req := &SendTestEmailRequest{
SMTP: smtp,
Template: template,
Target: target,
},
FromAddress: smtp.FromAddress,
}
msg := gomail.NewMessage()
@ -171,3 +174,44 @@ func (s *ModelsSuite) TestEmailRequestGenerateEmptySubject(ch *check.C) {
ch.Assert(err, check.Equals, nil)
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

@ -47,13 +47,32 @@ type GroupTarget struct {
// Groups contain 1..* Targets, but 1 Target may belong to 1..* Groups
type Target struct {
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"`
LastName string `json:"last_name"`
Email string `json:"email"`
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 {
addr := t.Email
if t.FirstName != "" && t.LastName != "" {
@ -66,7 +85,7 @@ func (t *Target) FormatAddress() string {
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")
// ErrGroupNameNotSpecified is thrown when a group name is not specified
@ -274,11 +293,12 @@ func insertTargetIntoGroup(t Target, gid int64) error {
return err
}
trans := db.Begin()
trans.Where(t).FirstOrCreate(&t)
err = trans.Where(t).FirstOrCreate(&t).Error
if err != nil {
log.WithFields(logrus.Fields{
"email": t.Email,
}).Error("Error adding target")
}).Error(err)
trans.Rollback()
return err
}
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
if err != nil {
log.Error(err)
trans.Rollback()
return err
}
}
@ -293,10 +314,12 @@ func insertTargetIntoGroup(t Target, gid int64) error {
log.WithFields(logrus.Fields{
"email": t.Email,
}).Error("Error adding many-many mapping")
trans.Rollback()
return err
}
err = trans.Commit().Error
if err != nil {
trans.Rollback()
log.Error("Error committing db changes")
return err
}

View File

@ -7,7 +7,7 @@ import (
func (s *ModelsSuite) TestPostGroup(c *check.C) {
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
err := PostGroup(&g)
c.Assert(err, check.Equals, nil)
@ -17,7 +17,7 @@ func (s *ModelsSuite) TestPostGroup(c *check.C) {
func (s *ModelsSuite) TestPostGroupNoName(c *check.C) {
g := Group{Name: ""}
g.Targets = []Target{Target{Email: "test@example.com"}}
g.Targets = []Target{Target{BaseRecipient: BaseRecipient{Email: "test@example.com"}}}
g.UserId = 1
err := PostGroup(&g)
c.Assert(err, check.Equals, ErrGroupNameNotSpecified)
@ -35,12 +35,20 @@ func (s *ModelsSuite) TestGetGroups(c *check.C) {
// Add groups.
PostGroup(&Group{
Name: "Test Group 1",
Targets: []Target{Target{Email: "test1@example.com"}},
Targets: []Target{
Target{
BaseRecipient: BaseRecipient{Email: "test1@example.com"},
},
},
UserId: 1,
})
PostGroup(&Group{
Name: "Test Group 2",
Targets: []Target{Target{Email: "test2@example.com"}},
Targets: []Target{
Target{
BaseRecipient: BaseRecipient{Email: "test2@example.com"},
},
},
UserId: 1,
})
@ -66,7 +74,11 @@ func (s *ModelsSuite) TestGetGroup(c *check.C) {
// Add group.
originalGroup := &Group{
Name: "Test Group",
Targets: []Target{Target{Email: "test@example.com"}},
Targets: []Target{
Target{
BaseRecipient: BaseRecipient{Email: "test@example.com"},
},
},
UserId: 1,
}
c.Assert(PostGroup(originalGroup), check.Equals, nil)
@ -88,7 +100,11 @@ func (s *ModelsSuite) TestGetGroupByName(c *check.C) {
// Add group.
PostGroup(&Group{
Name: "Test Group",
Targets: []Target{Target{Email: "test@example.com"}},
Targets: []Target{
Target{
BaseRecipient: BaseRecipient{Email: "test@example.com"},
},
},
UserId: 1,
})
@ -109,8 +125,8 @@ func (s *ModelsSuite) TestPutGroup(c *check.C) {
// Add test group.
group := Group{Name: "Test Group"}
group.Targets = []Target{
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"},
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"},
Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
}
group.UserId = 1
PostGroup(&group)
@ -134,8 +150,8 @@ func (s *ModelsSuite) TestPutGroupEmptyAttribute(c *check.C) {
// Add test group.
group := Group{Name: "Test Group"}
group.Targets = []Target{
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"},
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"},
Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
}
group.UserId = 1
PostGroup(&group)

View File

@ -1,17 +1,13 @@
package models
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"math"
"net/mail"
"net/url"
"path"
"strings"
"text/template"
"time"
"github.com/gophish/gomail"
@ -136,18 +132,6 @@ func (m *MailLog) GetDialer() (mailer.Dialer, error) {
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
// 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
@ -161,51 +145,26 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
if err != nil {
return err
}
f, err := mail.ParseAddress(c.SMTP.FromAddress)
if err != nil {
return err
}
fn := f.Name
if fn == "" {
fn = f.Address
}
msg.SetAddressHeader("From", f.Address, f.Name)
campaignURL, err := buildTemplate(c.URL, r)
ptx, err := NewPhishingTemplateContext(&c, r.BaseRecipient, r.RId)
if err != nil {
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
for _, header := range c.SMTP.Headers {
key, err := buildTemplate(header.Key, td)
key, err := ExecuteTemplate(header.Key, ptx)
if err != nil {
log.Warn(err)
}
value, err := buildTemplate(header.Value, td)
value, err := ExecuteTemplate(header.Value, ptx)
if err != nil {
log.Warn(err)
}
@ -215,7 +174,7 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
}
// Parse remaining templates
subject, err := buildTemplate(c.Template.Subject, td)
subject, err := ExecuteTemplate(c.Template.Subject, ptx)
if err != nil {
log.Warn(err)
}
@ -226,14 +185,14 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
msg.SetHeader("To", r.FormatAddress())
if c.Template.Text != "" {
text, err := buildTemplate(c.Template.Text, td)
text, err := ExecuteTemplate(c.Template.Text, ptx)
if err != nil {
log.Warn(err)
}
msg.SetBody("text/plain", text)
}
if c.Template.HTML != "" {
html, err := buildTemplate(c.Template.HTML, td)
html, err := ExecuteTemplate(c.Template.HTML, ptx)
if err != nil {
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
group := Group{Name: "Test Group"}
group.Targets = []Target{
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"},
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"},
Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
}
group.UserId = 1
ch.Assert(PostGroup(&group), check.Equals, nil)

View File

@ -3,10 +3,8 @@ package models
import (
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
"net"
"net/mail"
"time"
log "github.com/gophish/gophish/logger"
@ -30,10 +28,6 @@ type Result struct {
CampaignId int64 `json:"-"`
UserId int64 `json:"-"`
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"`
IP string `json:"ip"`
Latitude float64 `json:"latitude"`
@ -41,6 +35,7 @@ type Result struct {
SendDate time.Time `json:"send_date"`
Reported bool `json:"reported" sql:"not null"`
ModifiedDate time.Time `json:"modified_date"`
BaseRecipient
}
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
}
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
// in the database
func (r *Result) GenerateId() error {
// 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 i := range k {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphaNum))))
rid, err := generateResultId()
if err != nil {
return err
}
k[i] = alphaNum[idx.Int64()]
}
r.RId = string(k)
err := db.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error
r.RId = rid
err = db.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error
if err == gorm.ErrRecordNotFound {
break
}
@ -201,19 +204,6 @@ func (r *Result) GenerateId() error {
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
// given the ResultId
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) {
r := Result{
BaseRecipient: BaseRecipient{
FirstName: "John",
LastName: "Doe",
Email: "johndoe@example.com",
},
}
expected := &mail.Address{
Name: "John Doe",
@ -29,7 +31,7 @@ func (s *ModelsSuite) TestFormatAddress(c *check.C) {
c.Assert(r.FormatAddress(), check.Equals, expected.String())
r = Result{
Email: "johndoe@example.com",
BaseRecipient: BaseRecipient{Email: "johndoe@example.com"},
}
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) {
group := Group{Name: "Test Group"}
group.Targets = []Target{
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"},
Target{Email: "test1@example.com", FirstName: "Duplicate", LastName: "Duplicate"},
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"},
Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "Duplicate", LastName: "Duplicate"}},
Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
}
group.UserId = 1
ch.Assert(PostGroup(&group), check.Equals, nil)

View File

@ -1,9 +1,7 @@
package models
import (
"bytes"
"errors"
"html/template"
"time"
log "github.com/gophish/gophish/logger"
@ -36,7 +34,6 @@ func (t *Template) Validate() error {
case t.Text == "" && t.HTML == "":
return ErrTemplateMissingParameter
}
var buff bytes.Buffer
// Test that the variables used in the template
// validate with no issues
td := struct {
@ -47,31 +44,27 @@ func (t *Template) Validate() error {
From string
}{
Result{
BaseRecipient: BaseRecipient{
Email: "foo@bar.com",
FirstName: "Foo",
LastName: "Bar",
Position: "Test",
},
},
"http://foo.bar",
"http://foo.bar/track",
"<img src='http://foo.bar/track",
"John Doe <foo@bar.com>",
}
tmpl, err := template.New("html_template").Parse(t.HTML)
_, err := ExecuteTemplate(t.HTML, td)
if err != nil {
return err
}
err = tmpl.Execute(&buff, td)
_, err = ExecuteTemplate(t.Text, td)
if err != nil {
return err
}
tmpl, err = template.New("text_template").Parse(t.Text)
if err != nil {
return err
}
err = tmpl.Execute(&buff, td)
return err
return nil
}
// 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]
}
t := models.Target{
BaseRecipient: models.BaseRecipient{
FirstName: fn,
LastName: ln,
Email: ea,
Position: ps,
},
}
ts = append(ts, t)
}

View File

@ -52,9 +52,11 @@ func buildCSVRequest(csvPayload string) (*http.Request, error) {
func (s *UtilSuite) TestParseCSVEmail() {
expected := models.Target{
BaseRecipient: models.BaseRecipient{
FirstName: "John",
LastName: "Doe",
Email: "johndoe@example.com",
},
}
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
func (w *Worker) SendTestEmail(s *models.SendTestEmailRequest) error {
func (w *Worker) SendTestEmail(s *models.EmailRequest) error {
go func() {
mailer.Mailer.Queue <- []mailer.Mail{s}
}()

View File

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