Added ability to send a test email before launching a campaign

pull/97/head
Jordan Wright 2016-01-24 20:03:53 -06:00
parent 33947086b3
commit e4d6e68147
7 changed files with 247 additions and 4 deletions

View File

@ -421,6 +421,44 @@ func API_Import_Site(w http.ResponseWriter, r *http.Request) {
return 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 // JSONResponse attempts to set the status code, c, and marshal the given interface, d, into a response that
// is written to the given ResponseWriter. // is written to the given ResponseWriter.
func JSONResponse(w http.ResponseWriter, d interface{}, c int) { func JSONResponse(w http.ResponseWriter, d interface{}, c int) {

View File

@ -8,12 +8,12 @@ import (
"net/http" "net/http"
"os" "os"
ctx "github.com/gorilla/context"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/gophish/gophish/auth" "github.com/gophish/gophish/auth"
mid "github.com/gophish/gophish/middleware" mid "github.com/gophish/gophish/middleware"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
ctx "github.com/gorilla/context"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/justinas/nosurf" "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("/templates/{id:[0-9]+}", Use(API_Templates_Id, mid.RequireAPIKey))
api.HandleFunc("/pages/", Use(API_Pages, mid.RequireAPIKey)) api.HandleFunc("/pages/", Use(API_Pages, mid.RequireAPIKey))
api.HandleFunc("/pages/{id:[0-9]+}", Use(API_Pages_Id, 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/group", API_Import_Group)
api.HandleFunc("/import/email", API_Import_Email) api.HandleFunc("/import/email", API_Import_Email)
api.HandleFunc("/import/site", API_Import_Site) 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/pages/*") csrfHandler.ExemptGlob("/api/pages/*")
csrfHandler.ExemptGlob("/api/import/*") csrfHandler.ExemptGlob("/api/import/*")
csrfHandler.ExemptGlob("/api/util/*")
csrfHandler.ExemptGlob("/static/*") csrfHandler.ExemptGlob("/static/*")
return Use(csrfHandler.ServeHTTP, mid.GetContext) return Use(csrfHandler.ServeHTTP, mid.GetContext)
} }

View File

@ -63,6 +63,30 @@ func (c *Campaign) Validate() error {
return nil 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 // UpdateStatus changes the campaign status appropriately
func (c *Campaign) UpdateStatus(s string) error { func (c *Campaign) UpdateStatus(s string) error {
// This could be made simpler, but I think there's a bug in gorm // 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"` 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 // ErrGroupNameNotSpecified is thrown when a group name is not specified
var ErrGroupNameNotSpecified = errors.New("Group name 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-exclamation-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() { function dismiss() {
$("#modal\\.flashes").empty() $("#modal\\.flashes").empty()
$("#modal").modal('hide') $("#modal").modal('hide')
@ -103,6 +140,43 @@ function edit(campaign) {
} }
$(document).ready(function() { $(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() api.campaigns.get()
.success(function(cs) { .success(function(cs) {
campaigns = cs campaigns = cs

View File

@ -97,6 +97,7 @@
<label class="control-label" for="smtp_server">Password:</label> <label class="control-label" for="smtp_server">Password:</label>
<input type="password" class="form-control" placeholder="Password" value="" name="password"> <input type="password" class="form-control" placeholder="Password" value="" name="password">
<br /> <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> </div>
</div> </div>
@ -121,11 +122,48 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <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>
</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>
</div>
{{end}} {{end}}
{{define "scripts"}} {{define "scripts"}}
<script src="/js/hogan.js"></script> <script src="/js/hogan.js"></script>

View File

@ -2,6 +2,7 @@ package worker
import ( import (
"bytes" "bytes"
"errors"
"log" "log"
"net/mail" "net/mail"
"net/smtp" "net/smtp"
@ -130,3 +131,66 @@ func processCampaign(c *models.Campaign) {
Logger.Println(err) 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
}