mirror of https://github.com/gophish/gophish
Added ability to send a test email before launching a campaign
parent
33947086b3
commit
e4d6e68147
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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() {
|
||||
$("#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
|
||||
|
|
|
@ -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,11 +122,48 @@
|
|||
</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">×</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}}
|
||||
{{define "scripts"}}
|
||||
<script src="/js/hogan.js"></script>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue