diff --git a/controllers/api.go b/controllers/api.go index 589b92e5..6ca0baca 100644 --- a/controllers/api.go +++ b/controllers/api.go @@ -421,6 +421,44 @@ func API_Import_Site(w http.ResponseWriter, r *http.Request) { return } +// 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{} + if r.Method != "POST" { + JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest) + return + } + err := json.NewDecoder(r.Body).Decode(s) + if err != nil { + 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 + } + // Get the template requested by name + s.Template, err = models.GetTemplateByName(s.Template.Name, ctx.Get(r, "user_id").(int64)) + if err == gorm.RecordNotFound { + Logger.Printf("Error - Template %s does not exist", s.Template.Name) + JSONResponse(w, models.Response{Success: false, Message: models.ErrTemplateNotFound.Error()}, http.StatusBadRequest) + } else if err != nil { + Logger.Println(err) + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) + return + } + // Send the test email + err = worker.SendTestEmail(s) + if err != nil { + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) + return + } + JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK) + return +} + // JSONResponse attempts to set the status code, c, and marshal the given interface, d, into a response that // is written to the given ResponseWriter. func JSONResponse(w http.ResponseWriter, d interface{}, c int) { diff --git a/controllers/route.go b/controllers/route.go index 835249d9..5276811f 100644 --- a/controllers/route.go +++ b/controllers/route.go @@ -8,12 +8,12 @@ import ( "net/http" "os" - ctx "github.com/gorilla/context" - "github.com/gorilla/mux" - "github.com/gorilla/sessions" "github.com/gophish/gophish/auth" mid "github.com/gophish/gophish/middleware" "github.com/gophish/gophish/models" + ctx "github.com/gorilla/context" + "github.com/gorilla/mux" + "github.com/gorilla/sessions" "github.com/justinas/nosurf" ) @@ -48,6 +48,7 @@ func CreateAdminRouter() http.Handler { api.HandleFunc("/templates/{id:[0-9]+}", Use(API_Templates_Id, mid.RequireAPIKey)) api.HandleFunc("/pages/", Use(API_Pages, mid.RequireAPIKey)) api.HandleFunc("/pages/{id:[0-9]+}", Use(API_Pages_Id, mid.RequireAPIKey)) + api.HandleFunc("/util/send_test_email", Use(API_Send_Test_Email, mid.RequireAPIKey)) api.HandleFunc("/import/group", API_Import_Group) api.HandleFunc("/import/email", API_Import_Email) api.HandleFunc("/import/site", API_Import_Site) @@ -67,6 +68,7 @@ func CreateAdminRouter() http.Handler { csrfHandler.ExemptGlob("/api/pages") csrfHandler.ExemptGlob("/api/pages/*") csrfHandler.ExemptGlob("/api/import/*") + csrfHandler.ExemptGlob("/api/util/*") csrfHandler.ExemptGlob("/static/*") return Use(csrfHandler.ServeHTTP, mid.GetContext) } diff --git a/models/campaign.go b/models/campaign.go index 3d701d92..524da7f0 100644 --- a/models/campaign.go +++ b/models/campaign.go @@ -63,6 +63,30 @@ func (c *Campaign) Validate() error { return nil } +// SendTestEmailRequest is the structure of a request +// to send a test email to test an SMTP connection +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"` + Target +} + +// Validate ensures the SendTestEmailRequest structure +// is valid. +func (s *SendTestEmailRequest) Validate() error { + switch { + case s.Template.Name == "": + return ErrTemplateNotSpecified + case s.Email == "": + return ErrEmailNotSpecified + } + return nil +} + // UpdateStatus changes the campaign status appropriately func (c *Campaign) UpdateStatus(s string) error { // This could be made simpler, but I think there's a bug in gorm diff --git a/models/group.go b/models/group.go index 070325e5..73cd7a2f 100644 --- a/models/group.go +++ b/models/group.go @@ -34,6 +34,9 @@ type Target struct { Position string `json:"position"` } +// ErrNoEmailSpecified 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 var ErrGroupNameNotSpecified = errors.New("Group name not specified") diff --git a/static/js/app/campaigns.js b/static/js/app/campaigns.js index dafce85d..1e429446 100644 --- a/static/js/app/campaigns.js +++ b/static/js/app/campaigns.js @@ -50,6 +50,43 @@ function launch() { }) } +// Attempts to send a test email by POSTing to /campaigns/ +function sendTestEmail() { + var test_email_request = { + template: { + name: $("#template").val() + }, + first_name: $("input[name=to_first_name]").val(), + last_name: $("input[name=to_last_name]").val(), + email: $("input[name=to_email]").val(), + position: $("input[name=to_position]").val(), + url: $("#url").val(), + page: { + name: $("#page").val() + }, + smtp: { + from_address: $("input[name=from]").val(), + host: $("input[name=host]").val(), + username: $("input[name=username]").val(), + password: $("input[name=password]").val(), + } + } + btnHtml = $("#sendTestModalSubmit").html() + $("#sendTestModalSubmit").html(' Sending') + // Send the test email + api.send_test_email(test_email_request) + .success(function(data) { + $("#sendTestEmailModal\\.flashes").empty().append("
\ + Email Sent!
") + $("#sendTestModalSubmit").html(btnHtml) + }) + .error(function(data) { + $("#sendTestEmailModal\\.flashes").empty().append("
\ + " + data.responseJSON.message + "
") + $("#sendTestModalSubmit").html(btnHtml) + }) +} + function dismiss() { $("#modal\\.flashes").empty() $("#modal").modal('hide') @@ -103,6 +140,43 @@ function edit(campaign) { } $(document).ready(function() { + // Setup multiple modals + // Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html + $('.modal').on('hidden.bs.modal', function(event) { + $(this).removeClass('fv-modal-stack'); + $('body').data('fv_open_modals', $('body').data('fv_open_modals') - 1); + }); + $('.modal').on('shown.bs.modal', function(event) { + // Keep track of the number of open modals + if (typeof($('body').data('fv_open_modals')) == 'undefined') { + $('body').data('fv_open_modals', 0); + } + // if the z-index of this modal has been set, ignore. + if ($(this).hasClass('fv-modal-stack')) { + return; + } + $(this).addClass('fv-modal-stack'); + // Increment the number of open modals + $('body').data('fv_open_modals', $('body').data('fv_open_modals') + 1); + // Setup the appropriate z-index + $(this).css('z-index', 1040 + (10 * $('body').data('fv_open_modals'))); + $('.modal-backdrop').not('.fv-modal-stack').css('z-index', 1039 + (10 * $('body').data('fv_open_modals'))); + $('.modal-backdrop').not('fv-modal-stack').addClass('fv-modal-stack'); + }); + $.fn.modal.Constructor.prototype.enforceFocus = function() { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function(e) { + if ( + this.$element[0] !== e.target && !this.$element.has(e.target).length + // CKEditor compatibility fix start. + && !$(e.target).closest('.cke_dialog, .cke').length + // CKEditor compatibility fix end. + ) { + this.$element.trigger('focus'); + } + }, this)); + }; api.campaigns.get() .success(function(cs) { campaigns = cs diff --git a/templates/campaigns.html b/templates/campaigns.html index 100f4877..1a87be07 100644 --- a/templates/campaigns.html +++ b/templates/campaigns.html @@ -97,6 +97,7 @@
+ @@ -121,11 +122,48 @@ + + {{end}} {{define "scripts"}} diff --git a/worker/worker.go b/worker/worker.go index 478fb12f..a467b6d0 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -2,6 +2,7 @@ package worker import ( "bytes" + "errors" "log" "net/mail" "net/smtp" @@ -130,3 +131,66 @@ func processCampaign(c *models.Campaign) { Logger.Println(err) } } + +func SendTestEmail(s *models.SendTestEmailRequest) error { + e := email.Email{ + Subject: s.Template.Subject, + From: s.SMTP.FromAddress, + } + var auth smtp.Auth + if s.SMTP.Username != "" && s.SMTP.Password != "" { + auth = smtp.PlainAuth("", s.SMTP.Username, s.SMTP.Password, strings.Split(s.SMTP.Host, ":")[0]) + } + f, err := mail.ParseAddress(s.SMTP.FromAddress) + if err != nil { + Logger.Println(err) + return err + } + ft := f.Name + if ft == "" { + ft = f.Address + } + Logger.Println("Creating email using template") + // Parse the templates + var subjBuff bytes.Buffer + var htmlBuff bytes.Buffer + var textBuff bytes.Buffer + tmpl, err := template.New("html_template").Parse(s.Template.HTML) + if err != nil { + Logger.Println(err) + } + err = tmpl.Execute(&htmlBuff, s) + if err != nil { + Logger.Println(err) + } + e.HTML = htmlBuff.Bytes() + tmpl, err = template.New("text_template").Parse(s.Template.Text) + if err != nil { + Logger.Println(err) + } + err = tmpl.Execute(&textBuff, s) + if err != nil { + Logger.Println(err) + } + e.Text = textBuff.Bytes() + tmpl, err = template.New("text_template").Parse(s.Template.Subject) + if err != nil { + Logger.Println(err) + } + err = tmpl.Execute(&subjBuff, s) + if err != nil { + Logger.Println(err) + } + e.Subject = string(subjBuff.Bytes()) + e.To = []string{s.Email} + Logger.Printf("Sending Email to %s\n", s.Email) + err = e.Send(s.SMTP.Host, auth) + if err != nil { + Logger.Println(err) + // For now, let's split the error and return + // the last element (the most descriptive error message) + serr := strings.Split(err.Error(), ":") + return errors.New(serr[len(serr)-1]) + } + return err +}