Merge pull request #97 from gophish/34-csv-export

Added ability to send a test email
pull/98/head
Jordan Wright 2016-01-24 20:07:47 -06:00
commit eb6f3ed62a
8 changed files with 275 additions and 27 deletions

View File

@ -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) {

View File

@ -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)
}

View File

@ -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

View File

@ -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")

View File

@ -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('<i class="fa fa-spinner fa-spin"></i> Sending')
// Send the test email
api.send_test_email(test_email_request)
.success(function(data) {
$("#sendTestEmailModal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-success\">\
<i class=\"fa fa-check-circle\"></i> Email Sent!</div>")
$("#sendTestModalSubmit").html(btnHtml)
})
.error(function(data) {
$("#sendTestEmailModal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
$("#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

View File

@ -15,10 +15,10 @@ function modalError(message) {
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
}
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)
}
}

View File

@ -97,6 +97,7 @@
<label class="control-label" for="smtp_server">Password:</label>
<input type="password" class="form-control" placeholder="Password" value="" name="password">
<br />
<button type="button" data-toggle="modal" data-target="#sendTestEmailModal" class="btn btn-primary"><i class="fa fa-envelope"></i> Send Test Email</button>
</div>
</div>
</div>
@ -121,7 +122,44 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="launch()"><i class="fa fa-envelope"></i> Launch Campaign</button>
<button type="button" class="btn btn-primary" onclick="launch()"><i class="fa fa-rocket"></i> Launch Campaign</button>
</div>
</div>
</div>
</div>
<!-- Send Test Email Modal -->
<div class="modal" id="sendTestEmailModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<!-- New Email Modal -->
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="sendTestEmailModalTitle">Send Test Email</h4>
</div>
<div class="modal-body">
<div class="row" id="sendTestEmailModal.flashes"></div>
<div class="row">
<div class="col-sm-12">
<label class="control-label" for="to">Send Test Email to:</label>
</div>
<br>
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="First Name" name="to_first_name">
</div>
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="Last Name" name="to_last_name">
</div>
<div class="col-sm-4">
<input type="email" class="form-control" placeholder="Email" name="to_email" required>
</div>
<div class="col-sm-4">
<input type="text" class="form-control" placeholder="Position" name="to_position">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal" class="btn btn-default">Cancel</button>
<button type="button" class="btn btn-primary" id="sendTestModalSubmit" onclick="sendTestEmail()"><i class="fa fa-envelope"></i> Send</button>
</div>
</div>
</div>

View File

@ -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
}