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..2768ca75 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/static/js/gophish.js b/static/js/gophish.js
index ca9c0e11..86090932 100644
--- a/static/js/gophish.js
+++ b/static/js/gophish.js
@@ -15,10 +15,10 @@ function modalError(message) {
" + message + "")
}
-function query(endpoint, method, data) {
+function query(endpoint, method, data, async) {
return $.ajax({
url: "/api" + endpoint + "?api_key=" + user.api_key,
- async: false,
+ async: async,
method: method,
data: JSON.stringify(data),
dataType: "json",
@@ -34,108 +34,113 @@ var api = {
campaigns: {
// get() - Queries the API for GET /campaigns
get: function() {
- return query("/campaigns/", "GET", {})
+ return query("/campaigns/", "GET", {}, false)
},
// post() - Posts a campaign to POST /campaigns
post: function(data) {
- return query("/campaigns/", "POST", data)
+ return query("/campaigns/", "POST", data, false)
}
},
// campaignId contains the endpoints for /campaigns/:id
campaignId: {
// get() - Queries the API for GET /campaigns/:id
get: function(id) {
- return query("/campaigns/" + id, "GET", {})
+ return query("/campaigns/" + id, "GET", {}, false)
},
// delete() - Deletes a campaign at DELETE /campaigns/:id
delete: function(id) {
- return query("/campaigns/" + id, "DELETE", {})
+ return query("/campaigns/" + id, "DELETE", {}, false)
}
},
// groups contains the endpoints for /groups
groups: {
// get() - Queries the API for GET /groups
get: function() {
- return query("/groups/", "GET", {})
+ return query("/groups/", "GET", {}, false)
},
// post() - Posts a campaign to POST /groups
post: function(group) {
- return query("/groups/", "POST", group)
+ return query("/groups/", "POST", group, false)
}
},
// groupId contains the endpoints for /groups/:id
groupId: {
// get() - Queries the API for GET /groups/:id
get: function(id) {
- return query("/groups/" + id, "GET", {})
+ return query("/groups/" + id, "GET", {}, false)
},
// put() - Puts a campaign to PUT /groups/:id
put: function(group) {
- return query("/groups/" + group.id, "PUT", group)
+ return query("/groups/" + group.id, "PUT", group, false)
},
// delete() - Deletes a campaign at DELETE /groups/:id
delete: function(id) {
- return query("/groups/" + id, "DELETE", {})
+ return query("/groups/" + id, "DELETE", {}, false)
}
},
// templates contains the endpoints for /templates
templates: {
// get() - Queries the API for GET /templates
get: function() {
- return query("/templates/", "GET", {})
+ return query("/templates/", "GET", {}, false)
},
// post() - Posts a campaign to POST /templates
post: function(template) {
- return query("/templates/", "POST", template)
+ return query("/templates/", "POST", template, false)
}
},
// templateId contains the endpoints for /templates/:id
templateId: {
// get() - Queries the API for GET /templates/:id
get: function(id) {
- return query("/templates/" + id, "GET", {})
+ return query("/templates/" + id, "GET", {}, false)
},
// put() - Puts a campaign to PUT /templates/:id
put: function(template) {
- return query("/templates/" + template.id, "PUT", template)
+ return query("/templates/" + template.id, "PUT", template, false)
},
// delete() - Deletes a campaign at DELETE /templates/:id
delete: function(id) {
- return query("/templates/" + id, "DELETE", {})
+ return query("/templates/" + id, "DELETE", {}, false)
}
},
// pages contains the endpoints for /pages
pages: {
// get() - Queries the API for GET /pages
get: function() {
- return query("/pages/", "GET", {})
+ return query("/pages/", "GET", {}, false)
},
// post() - Posts a campaign to POST /pages
post: function(page) {
- return query("/pages/", "POST", page)
+ return query("/pages/", "POST", page, false)
}
},
// templateId contains the endpoints for /templates/:id
pageId: {
// get() - Queries the API for GET /templates/:id
get: function(id) {
- return query("/pages/" + id, "GET", {})
+ return query("/pages/" + id, "GET", {}, false)
},
// put() - Puts a campaign to PUT /templates/:id
put: function(page) {
- return query("/pages/" + page.id, "PUT", page)
+ return query("/pages/" + page.id, "PUT", page, false)
},
// delete() - Deletes a campaign at DELETE /templates/:id
delete: function(id) {
- return query("/pages/" + id, "DELETE", {})
+ return query("/pages/" + id, "DELETE", {}, false)
}
},
// import handles all of the "import" functions in the api
import_email: function(raw) {
- return query("/import/email", "POST", {})
+ return query("/import/email", "POST", {}, false)
},
+ // clone_site handles importing a site by url
clone_site: function(req) {
- return query("/import/site", "POST", req)
+ return query("/import/site", "POST", req, false)
+ },
+ // send_test_email sends an email to the specified email address
+ send_test_email: function(req){
+ return query("/util/send_test_email", "POST", req, true)
}
}
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
+}