diff --git a/controllers/api.go b/controllers/api.go
index ca99f369..1bac5f12 100644
--- a/controllers/api.go
+++ b/controllers/api.go
@@ -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
}
diff --git a/controllers/api_test.go b/controllers/api_test.go
index 40758413..8e024ece 100644
--- a/controllers/api_test.go
+++ b/controllers/api_test.go
@@ -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)
diff --git a/controllers/phish.go b/controllers/phish.go
index d37c7954..4d5a2d99 100644
--- a/controllers/phish.go
+++ b/controllers/phish.go
@@ -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
diff --git a/controllers/phish_test.go b/controllers/phish_test.go
index 4b034bc7..02288708 100644
--- a/controllers/phish_test.go
+++ b/controllers/phish_test.go
@@ -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)
+}
diff --git a/db/db_mysql/migrations/20180527213648_0.7.0_store_email_request.sql b/db/db_mysql/migrations/20180527213648_0.7.0_store_email_request.sql
new file mode 100644
index 00000000..a2008b89
--- /dev/null
+++ b/db/db_mysql/migrations/20180527213648_0.7.0_store_email_request.sql
@@ -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
diff --git a/db/db_sqlite3/migrations/20180527213648_0.7.0_store_email_request.sql b/db/db_sqlite3/migrations/20180527213648_0.7.0_store_email_request.sql
new file mode 100644
index 00000000..69d68050
--- /dev/null
+++ b/db/db_sqlite3/migrations/20180527213648_0.7.0_store_email_request.sql
@@ -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
+
diff --git a/models/campaign.go b/models/campaign.go
index 8c20d3d1..90d2c873 100644
--- a/models/campaign.go
+++ b/models/campaign.go
@@ -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{
- Email: t.Email,
- Position: t.Position,
+ 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,
diff --git a/models/email_request.go b/models/email_request.go
index c255b007..f4b1280f 100644
--- a/models/email_request.go
+++ b/models/email_request.go
@@ -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 {
- Template Template `json:"template"`
- Page Page `json:"page"`
- 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:"-"`
+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" 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()
}
diff --git a/models/email_request_test.go b/models/email_request_test.go
index 542c47f8..58ae5e9d 100644
--- a/models/email_request_test.go
+++ b/models/email_request_test.go
@@ -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{
- FirstName: "First",
- LastName: "Last",
- Email: "firstlast@example.com",
- }
- req := &SendTestEmailRequest{
+ req := &EmailRequest{
SMTP: smtp,
Template: template,
- Target: target,
+ BaseRecipient: BaseRecipient{
+ FirstName: "First",
+ LastName: "Last",
+ Email: "firstlast@example.com",
+ },
+ FromAddress: smtp.FromAddress,
}
msg := gomail.NewMessage()
@@ -104,23 +106,24 @@ func (s *ModelsSuite) TestEmailRequestURLTemplating(ch *check.C) {
Text: "{{.URL}}",
HTML: "{{.URL}}",
}
- target := Target{
- FirstName: "First",
- LastName: "Last",
- Email: "firstlast@example.com",
- }
- req := &SendTestEmailRequest{
+ req := &EmailRequest{
SMTP: smtp,
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()
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{
- FirstName: "First",
- LastName: "Last",
- Email: "firstlast@example.com",
- }
- req := &SendTestEmailRequest{
+ req := &EmailRequest{
SMTP: smtp,
Template: template,
- Target: target,
+ BaseRecipient: BaseRecipient{
+ FirstName: "First",
+ LastName: "Last",
+ Email: "firstlast@example.com",
+ },
+ 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)
+}
diff --git a/models/group.go b/models/group.go
index 2415d159..a7f0248a 100644
--- a/models/group.go
+++ b/models/group.go
@@ -46,14 +46,33 @@ type GroupTarget struct {
// Target contains the fields needed for individual targets specified by the user
// Groups contain 1..* Targets, but 1 Target may belong to 1..* Groups
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"`
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
}
diff --git a/models/group_test.go b/models/group_test.go
index c03c279f..20713939 100644
--- a/models/group_test.go
+++ b/models/group_test.go
@@ -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)
@@ -34,14 +34,22 @@ func (s *ModelsSuite) TestPostGroupNoTargets(c *check.C) {
func (s *ModelsSuite) TestGetGroups(c *check.C) {
// Add groups.
PostGroup(&Group{
- Name: "Test Group 1",
- Targets: []Target{Target{Email: "test1@example.com"}},
- UserId: 1,
+ Name: "Test Group 1",
+ Targets: []Target{
+ Target{
+ BaseRecipient: BaseRecipient{Email: "test1@example.com"},
+ },
+ },
+ UserId: 1,
})
PostGroup(&Group{
- Name: "Test Group 2",
- Targets: []Target{Target{Email: "test2@example.com"}},
- UserId: 1,
+ Name: "Test Group 2",
+ Targets: []Target{
+ Target{
+ BaseRecipient: BaseRecipient{Email: "test2@example.com"},
+ },
+ },
+ UserId: 1,
})
// Get groups and test result.
@@ -65,9 +73,13 @@ func (s *ModelsSuite) TestGetGroupsNoGroups(c *check.C) {
func (s *ModelsSuite) TestGetGroup(c *check.C) {
// Add group.
originalGroup := &Group{
- Name: "Test Group",
- Targets: []Target{Target{Email: "test@example.com"}},
- UserId: 1,
+ Name: "Test Group",
+ Targets: []Target{
+ Target{
+ BaseRecipient: BaseRecipient{Email: "test@example.com"},
+ },
+ },
+ UserId: 1,
}
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) {
// Add group.
PostGroup(&Group{
- Name: "Test Group",
- Targets: []Target{Target{Email: "test@example.com"}},
- UserId: 1,
+ Name: "Test Group",
+ Targets: []Target{
+ Target{
+ BaseRecipient: BaseRecipient{Email: "test@example.com"},
+ },
+ },
+ UserId: 1,
})
// Get group and test result.
@@ -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)
diff --git a/models/maillog.go b/models/maillog.go
index c09d0fee..cd4a6fd4 100644
--- a/models/maillog.go
+++ b/models/maillog.go
@@ -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(),
- "",
- 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)
}
diff --git a/models/models_test.go b/models/models_test.go
index b840f8f0..a4c1ed4c 100644
--- a/models/models_test.go
+++ b/models/models_test.go
@@ -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)
diff --git a/models/result.go b/models/result.go
index 431b6f50..b18efc26 100644
--- a/models/result.go
+++ b/models/result.go
@@ -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))))
- if err != nil {
- return err
- }
- k[i] = alphaNum[idx.Int64()]
+ rid, err := generateResultId()
+ if err != nil {
+ return err
}
- 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) {
diff --git a/models/result_test.go b/models/result_test.go
index e7cbdeb0..5b81b9bf 100644
--- a/models/result_test.go
+++ b/models/result_test.go
@@ -18,9 +18,11 @@ func (s *ModelsSuite) TestGenerateResultId(c *check.C) {
func (s *ModelsSuite) TestFormatAddress(c *check.C) {
r := Result{
- FirstName: "John",
- LastName: "Doe",
- Email: "johndoe@example.com",
+ 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)
diff --git a/models/template.go b/models/template.go
index 8d71c789..b6630d38 100644
--- a/models/template.go
+++ b/models/template.go
@@ -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{
- Email: "foo@bar.com",
- FirstName: "Foo",
- LastName: "Bar",
- Position: "Test",
+ BaseRecipient: BaseRecipient{
+ Email: "foo@bar.com",
+ FirstName: "Foo",
+ LastName: "Bar",
+ Position: "Test",
+ },
},
"http://foo.bar",
"http://foo.bar/track",
"",
+ 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
+}
diff --git a/util/util.go b/util/util.go
index f0ffb0b9..414f2828 100644
--- a/util/util.go
+++ b/util/util.go
@@ -100,10 +100,12 @@ func ParseCSV(r *http.Request) ([]models.Target, error) {
ps = record[pi]
}
t := models.Target{
- FirstName: fn,
- LastName: ln,
- Email: ea,
- Position: ps,
+ BaseRecipient: models.BaseRecipient{
+ FirstName: fn,
+ LastName: ln,
+ Email: ea,
+ Position: ps,
+ },
}
ts = append(ts, t)
}
diff --git a/util/util_test.go b/util/util_test.go
index d11ed5b5..1274071c 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -52,9 +52,11 @@ func buildCSVRequest(csvPayload string) (*http.Request, error) {
func (s *UtilSuite) TestParseCSVEmail() {
expected := models.Target{
- FirstName: "John",
- LastName: "Doe",
- Email: "johndoe@example.com",
+ BaseRecipient: models.BaseRecipient{
+ FirstName: "John",
+ LastName: "Doe",
+ Email: "johndoe@example.com",
+ },
}
csvPayload := fmt.Sprintf("%s,%s,<%s>", expected.FirstName, expected.LastName, expected.Email)
diff --git a/worker/worker.go b/worker/worker.go
index 5b918443..c73f555b 100644
--- a/worker/worker.go
+++ b/worker/worker.go
@@ -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}
}()
diff --git a/worker/worker_test.go b/worker/worker_test.go
index 292c784d..9ad6383d 100644
--- a/worker/worker_test.go
+++ b/worker/worker_test.go
@@ -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)