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
|
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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">×</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>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue