diff --git a/db/db_mysql/migrations/20170219122503_0.2.1_email_headers.sql b/db/db_mysql/migrations/20170219122503_0.2.1_email_headers.sql new file mode 100644 index 00000000..d1d94e16 --- /dev/null +++ b/db/db_mysql/migrations/20170219122503_0.2.1_email_headers.sql @@ -0,0 +1,12 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE TABLE IF NOT EXISTS headers( + id integer primary key auto_increment, + `key` varchar(255), + `value` varchar(255), + `smtp_id` bigint +); +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP TABLE headers; diff --git a/db/db_sqlite3/migrations/20170219122503_0.2.1_email_headers.sql b/db/db_sqlite3/migrations/20170219122503_0.2.1_email_headers.sql new file mode 100644 index 00000000..b21155df --- /dev/null +++ b/db/db_sqlite3/migrations/20170219122503_0.2.1_email_headers.sql @@ -0,0 +1,12 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE TABLE IF NOT EXISTS headers( + id integer primary key autoincrement, + key varchar(255), + value varchar(255), + "smtp_id" bigint +); +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP TABLE headers; diff --git a/models/campaign.go b/models/campaign.go index 9b6d9ecb..9172b69e 100644 --- a/models/campaign.go +++ b/models/campaign.go @@ -189,6 +189,11 @@ func (c *Campaign) getDetails() error { c.SMTP = SMTP{Name: "[Deleted]"} Logger.Printf("%s: sending profile not found for campaign\n", err) } + err = db.Where("smtp_id=?", c.SMTP.Id).Find(&c.SMTP.Headers).Error + if err != nil && err != gorm.ErrRecordNotFound { + Logger.Println(err) + return err + } return nil } diff --git a/models/models_test.go b/models/models_test.go index 1ac56402..7dfe7b24 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -246,6 +246,24 @@ func (s *ModelsSuite) TestPostSMTPNoFrom(c *check.C) { c.Assert(err, check.Equals, ErrFromAddressNotSpecified) } +func (s *ModelsSuite) TestPostSMTPValidHeader(c *check.C) { + smtp := SMTP{ + Name: "Test SMTP", + Host: "1.1.1.1:25", + FromAddress: "Foo Bar ", + UserId: 1, + Headers: []Header{ + Header{Key: "Reply-To", Value: "test@example.com"}, + Header{Key: "X-Mailer", Value: "gophish"}, + }, + } + err = PostSMTP(&smtp) + c.Assert(err, check.Equals, nil) + ss, err := GetSMTPs(1) + c.Assert(err, check.Equals, nil) + c.Assert(len(ss), check.Equals, 1) +} + func (s *ModelsSuite) TestPostPage(c *check.C) { html := ` diff --git a/models/smtp.go b/models/smtp.go index 298217a2..ae6ea9e7 100644 --- a/models/smtp.go +++ b/models/smtp.go @@ -6,6 +6,8 @@ import ( "strconv" "strings" "time" + + "github.com/jinzhu/gorm" ) // SMTP contains the attributes needed to handle the sending of campaign emails @@ -19,9 +21,19 @@ type SMTP struct { Password string `json:"password,omitempty"` FromAddress string `json:"from_address"` IgnoreCertErrors bool `json:"ignore_cert_errors"` + Headers []Header `json:"headers"` ModifiedDate time.Time `json:"modified_date"` } +// Header contains the fields and methods for a sending profile to have +// custom headers +type Header struct { + Id int64 `json:"-"` + SMTPId int64 `json:"-"` + Key string `json:"key"` + Value string `json:"value"` +} + // ErrFromAddressNotSpecified is thrown when there is no "From" address // specified in the SMTP configuration var ErrFromAddressNotSpecified = errors.New("No From Address specified") @@ -70,8 +82,16 @@ func GetSMTPs(uid int64) ([]SMTP, error) { err := db.Where("user_id=?", uid).Find(&ss).Error if err != nil { Logger.Println(err) + return ss, err } - return ss, err + for i, _ := range ss { + err = db.Where("smtp_id=?", ss[i].Id).Find(&ss[i].Headers).Error + if err != nil && err != gorm.ErrRecordNotFound { + Logger.Println(err) + return ss, err + } + } + return ss, nil } // GetSMTP returns the SMTP, if it exists, specified by the given id and user_id. @@ -81,6 +101,11 @@ func GetSMTP(id int64, uid int64) (SMTP, error) { if err != nil { Logger.Println(err) } + err = db.Where("smtp_id=?", s.Id).Find(&s.Headers).Error + if err != nil && err != gorm.ErrRecordNotFound { + Logger.Println(err) + return s, err + } return s, err } @@ -90,6 +115,11 @@ func GetSMTPByName(n string, uid int64) (SMTP, error) { err := db.Where("user_id=? and name=?", uid, n).Find(&s).Error if err != nil { Logger.Println(err) + return s, err + } + err = db.Where("smtp_id=?", s.Id).Find(&s.Headers).Error + if err != nil && err != gorm.ErrRecordNotFound { + Logger.Println(err) } return s, err } @@ -106,6 +136,15 @@ func PostSMTP(s *SMTP) error { if err != nil { Logger.Println(err) } + // Save custom headers + for i, _ := range s.Headers { + s.Headers[i].SMTPId = s.Id + err := db.Save(&s.Headers[i]).Error + if err != nil { + Logger.Println(err) + return err + } + } return err } @@ -121,12 +160,32 @@ func PutSMTP(s *SMTP) error { if err != nil { Logger.Println(err) } + // Delete all custom headers, and replace with new ones + err = db.Where("smtp_id=?", s.Id).Delete(&Header{}).Error + if err != nil && err != gorm.ErrRecordNotFound { + Logger.Println(err) + return err + } + for i, _ := range s.Headers { + s.Headers[i].SMTPId = s.Id + err := db.Save(&s.Headers[i]).Error + if err != nil { + Logger.Println(err) + return err + } + } return err } // DeleteSMTP deletes an existing SMTP in the database. // An error is returned if a SMTP with the given user id and SMTP id is not found. func DeleteSMTP(id int64, uid int64) error { + // Delete all custom headers + err = db.Where("smtp_id=?", id).Delete(&Header{}).Error + if err != nil { + Logger.Println(err) + return err + } err = db.Where("user_id=?", uid).Delete(SMTP{Id: id}).Error if err != nil { Logger.Println(err) diff --git a/models/template.go b/models/template.go index 38527340..dbb24dc0 100644 --- a/models/template.go +++ b/models/template.go @@ -64,6 +64,7 @@ func (t *Template) Validate() error { if err != nil { return err } + tmpl, err = template.New("text_template").Parse(t.Text) if err != nil { return err @@ -81,6 +82,7 @@ func GetTemplates(uid int64) ([]Template, error) { return ts, err } for i, _ := range ts { + // Get Attachments err = db.Where("template_id=?", ts[i].Id).Find(&ts[i].Attachments).Error if err == nil && len(ts[i].Attachments) == 0 { ts[i].Attachments = make([]Attachment, 0) @@ -101,6 +103,8 @@ func GetTemplate(id int64, uid int64) (Template, error) { Logger.Println(err) return t, err } + + // Get Attachments err = db.Where("template_id=?", t.Id).Find(&t.Attachments).Error if err != nil && err != gorm.ErrRecordNotFound { Logger.Println(err) @@ -120,6 +124,8 @@ func GetTemplateByName(n string, uid int64) (Template, error) { Logger.Println(err) return t, err } + + // Get Attachments err = db.Where("template_id=?", t.Id).Find(&t.Attachments).Error if err != nil && err != gorm.ErrRecordNotFound { Logger.Println(err) @@ -142,6 +148,8 @@ func PostTemplate(t *Template) error { Logger.Println(err) return err } + + // Save every attachment for i, _ := range t.Attachments { Logger.Println(t.Attachments[i].Name) t.Attachments[i].TemplateId = t.Id @@ -177,6 +185,8 @@ func PutTemplate(t *Template) error { return err } } + + // Save final template err = db.Where("id=?", t.Id).Save(t).Error if err != nil { Logger.Println(err) @@ -188,11 +198,14 @@ func PutTemplate(t *Template) error { // DeleteTemplate deletes an existing template in the database. // An error is returned if a template with the given user id and template id is not found. func DeleteTemplate(id int64, uid int64) error { + // Delete attachments err := db.Where("template_id=?", id).Delete(&Attachment{}).Error if err != nil { Logger.Println(err) return err } + + // Finally, delete the template itself err = db.Where("user_id=?", uid).Delete(Template{Id: id}).Error if err != nil { Logger.Println(err) diff --git a/static/js/dist/app/sending_profiles.min.js b/static/js/dist/app/sending_profiles.min.js index 4d37d6a4..9feb4b4e 100644 --- a/static/js/dist/app/sending_profiles.min.js +++ b/static/js/dist/app/sending_profiles.min.js @@ -1 +1 @@ -function sendTestEmail(){var e={template:{},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:"",smtp:{from_address:$("#from").val(),host:$("#host").val(),username:$("#username").val(),password:$("#password").val(),ignore_cert_errors:$("#ignore_cert_errors").prop("checked")}};btnHtml=$("#sendTestModalSubmit").html(),$("#sendTestModalSubmit").html(' Sending'),api.send_test_email(e).success(function(e){$("#sendTestEmailModal\\.flashes").empty().append('
\t Email Sent!
'),$("#sendTestModalSubmit").html(btnHtml)}).error(function(e){$("#sendTestEmailModal\\.flashes").empty().append('
\t '+e.responseJSON.message+"
"),$("#sendTestModalSubmit").html(btnHtml)})}function save(e){var a={};a.name=$("#name").val(),a.interface_type=$("#interface_type").val(),a.from_address=$("#from").val(),a.host=$("#host").val(),a.username=$("#username").val(),a.password=$("#password").val(),a.ignore_cert_errors=$("#ignore_cert_errors").prop("checked"),e!=-1?(a.id=profiles[e].id,api.SMTPId.put(a).success(function(e){successFlash("Profile edited successfully!"),load(),dismiss()}).error(function(e){modalError(e.responseJSON.message)})):api.SMTP.post(a).success(function(e){successFlash("Profile added successfully!"),load(),dismiss()}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#modal\\.flashes").empty(),$("#name").val(""),$("#interface_type").val("SMTP"),$("#from").val(""),$("#host").val(""),$("#username").val(""),$("#password").val(""),$("#ignore_cert_errors").prop("checked",!0),$("#modal").modal("hide")}function deleteProfile(e){confirm("Delete "+profiles[e].name+"?")&&api.SMTPId.delete(profiles[e].id).success(function(e){successFlash(e.message),load()})}function edit(e){$("#modalSubmit").unbind("click").click(function(){save(e)});var a={};e!=-1&&(a=profiles[e],$("#name").val(a.name),$("#interface_type").val(a.interface_type),$("#from").val(a.from_address),$("#host").val(a.host),$("#username").val(a.username),$("#password").val(a.password),$("#ignore_cert_errors").prop("checked",a.ignore_cert_errors))}function copy(e){$("#modalSubmit").unbind("click").click(function(){save(-1)});var a={};a=profiles[e],$("#name").val("Copy of "+a.name),$("#interface_type").val(a.interface_type),$("#from").val(a.from_address),$("#host").val(a.host),$("#username").val(a.username),$("#password").val(a.password),$("#ignore_cert_errors").prop("checked",a.ignore_cert_errors)}function load(){$("#profileTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.SMTP.get().success(function(e){profiles=e,$("#loading").hide(),profiles.length>0?($("#profileTable").show(),profileTable=$("#profileTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),profileTable.clear(),$.each(profiles,function(e,a){profileTable.row.add([escapeHtml(a.name),a.interface_type,moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"
\t\t
"]).draw()}),$('[data-toggle="tooltip"]').tooltip()):$("#emptyMessage").show()}).error(function(){$("#loading").hide(),errorFlash("Error fetching profiles")})}var profiles=[];$(document).ready(function(){$(".modal").on("hidden.bs.modal",function(e){$(this).removeClass("fv-modal-stack"),$("body").data("fv_open_modals",$("body").data("fv_open_modals")-1)}),$(".modal").on("shown.bs.modal",function(e){"undefined"==typeof $("body").data("fv_open_modals")&&$("body").data("fv_open_modals",0),$(this).hasClass("fv-modal-stack")||($(this).addClass("fv-modal-stack"),$("body").data("fv_open_modals",$("body").data("fv_open_modals")+1),$(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").on("focusin.bs.modal",$.proxy(function(e){this.$element[0]===e.target||this.$element.has(e.target).length||$(e.target).closest(".cke_dialog, .cke").length||this.$element.trigger("focus")},this))},$("#modal").on("hidden.bs.modal",function(e){dismiss()}),load()}); \ No newline at end of file +function sendTestEmail(){var e=[];$.each($("#headersTable").DataTable().rows().data(),function(a,s){e.push({key:unescapeHtml(s[0]),value:unescapeHtml(s[1])})});var a={template:{},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:"",smtp:{from_address:$("#from").val(),host:$("#host").val(),username:$("#username").val(),password:$("#password").val(),ignore_cert_errors:$("#ignore_cert_errors").prop("checked"),headers:e}};btnHtml=$("#sendTestModalSubmit").html(),$("#sendTestModalSubmit").html(' Sending'),api.send_test_email(a).success(function(e){$("#sendTestEmailModal\\.flashes").empty().append('
\t Email Sent!
'),$("#sendTestModalSubmit").html(btnHtml)}).error(function(e){$("#sendTestEmailModal\\.flashes").empty().append('
\t '+e.responseJSON.message+"
"),$("#sendTestModalSubmit").html(btnHtml)})}function save(e){var a={headers:[]};$.each($("#headersTable").DataTable().rows().data(),function(e,s){a.headers.push({key:unescapeHtml(s[0]),value:unescapeHtml(s[1])})}),a.name=$("#name").val(),a.interface_type=$("#interface_type").val(),a.from_address=$("#from").val(),a.host=$("#host").val(),a.username=$("#username").val(),a.password=$("#password").val(),a.ignore_cert_errors=$("#ignore_cert_errors").prop("checked"),e!=-1?(a.id=profiles[e].id,api.SMTPId.put(a).success(function(e){successFlash("Profile edited successfully!"),load(),dismiss()}).error(function(e){modalError(e.responseJSON.message)})):api.SMTP.post(a).success(function(e){successFlash("Profile added successfully!"),load(),dismiss()}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#modal\\.flashes").empty(),$("#name").val(""),$("#interface_type").val("SMTP"),$("#from").val(""),$("#host").val(""),$("#username").val(""),$("#password").val(""),$("#ignore_cert_errors").prop("checked",!0),$("#headersTable").dataTable().DataTable().clear().draw(),$("#modal").modal("hide")}function deleteProfile(e){confirm("Delete "+profiles[e].name+"?")&&api.SMTPId.delete(profiles[e].id).success(function(e){successFlash(e.message),load()})}function edit(e){headers=$("#headersTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)});var a={};e!=-1&&(a=profiles[e],$("#name").val(a.name),$("#interface_type").val(a.interface_type),$("#from").val(a.from_address),$("#host").val(a.host),$("#username").val(a.username),$("#password").val(a.password),$("#ignore_cert_errors").prop("checked",a.ignore_cert_errors),$.each(a.headers,function(e,a){addCustomHeader(a.key,a.value)}))}function copy(e){$("#modalSubmit").unbind("click").click(function(){save(-1)});var a={};a=profiles[e],$("#name").val("Copy of "+a.name),$("#interface_type").val(a.interface_type),$("#from").val(a.from_address),$("#host").val(a.host),$("#username").val(a.username),$("#password").val(a.password),$("#ignore_cert_errors").prop("checked",a.ignore_cert_errors)}function load(){$("#profileTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.SMTP.get().success(function(e){profiles=e,$("#loading").hide(),profiles.length>0?($("#profileTable").show(),profileTable=$("#profileTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),profileTable.clear(),$.each(profiles,function(e,a){profileTable.row.add([escapeHtml(a.name),a.interface_type,moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"
\t\t
"]).draw()}),$('[data-toggle="tooltip"]').tooltip()):$("#emptyMessage").show()}).error(function(){$("#loading").hide(),errorFlash("Error fetching profiles")})}function addCustomHeader(e,a){var s=[escapeHtml(e),escapeHtml(a),''],t=headers.DataTable(),o=t.column(0).data().indexOf(escapeHtml(e));o>=0?t.row(o,{order:"index"}).data(s):t.row.add(s),t.draw()}var profiles=[];$(document).ready(function(){$(".modal").on("hidden.bs.modal",function(e){$(this).removeClass("fv-modal-stack"),$("body").data("fv_open_modals",$("body").data("fv_open_modals")-1)}),$(".modal").on("shown.bs.modal",function(e){"undefined"==typeof $("body").data("fv_open_modals")&&$("body").data("fv_open_modals",0),$(this).hasClass("fv-modal-stack")||($(this).addClass("fv-modal-stack"),$("body").data("fv_open_modals",$("body").data("fv_open_modals")+1),$(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").on("focusin.bs.modal",$.proxy(function(e){this.$element[0]===e.target||this.$element.has(e.target).length||$(e.target).closest(".cke_dialog, .cke").length||this.$element.trigger("focus")},this))},$("#modal").on("hidden.bs.modal",function(e){dismiss()}),$("#headersForm").on("submit",function(){return headerKey=$("#headerKey").val(),headerValue=$("#headerValue").val(),""!=headerKey&&""!=headerValue&&(addCustomHeader(headerKey,headerValue),$("#headersForm>div>input").val(""),$("#headerKey").focus(),!1)}),$("#headersTable").on("click","span>i.fa-trash-o",function(){headers.DataTable().row($(this).parents("tr")).remove().draw()}),load()}); \ No newline at end of file diff --git a/static/js/src/app/sending_profiles.js b/static/js/src/app/sending_profiles.js index 1fbc23db..272ce73f 100644 --- a/static/js/src/app/sending_profiles.js +++ b/static/js/src/app/sending_profiles.js @@ -2,6 +2,13 @@ var profiles = [] // Attempts to send a test email by POSTing to /campaigns/ function sendTestEmail() { + var headers = []; + $.each($("#headersTable").DataTable().rows().data(), function(i, header) { + headers.push({ + key: unescapeHtml(header[0]), + value: unescapeHtml(header[1]), + }) + }) var test_email_request = { template: {}, first_name: $("input[name=to_first_name]").val(), @@ -14,7 +21,8 @@ function sendTestEmail() { host: $("#host").val(), username: $("#username").val(), password: $("#password").val(), - ignore_cert_errors: $("#ignore_cert_errors").prop("checked") + ignore_cert_errors: $("#ignore_cert_errors").prop("checked"), + headers: headers, } } btnHtml = $("#sendTestModalSubmit").html() @@ -35,7 +43,15 @@ function sendTestEmail() { // Save attempts to POST to /smtp/ function save(idx) { - var profile = {} + var profile = { + headers: [] + } + $.each($("#headersTable").DataTable().rows().data(), function(i, header) { + profile.headers.push({ + key: unescapeHtml(header[0]), + value: unescapeHtml(header[1]), + }) + }) profile.name = $("#name").val() profile.interface_type = $("#interface_type").val() profile.from_address = $("#from").val() @@ -77,6 +93,7 @@ function dismiss() { $("#username").val("") $("#password").val("") $("#ignore_cert_errors").prop("checked", true) + $("#headersTable").dataTable().DataTable().clear().draw() $("#modal").modal('hide') } @@ -91,6 +108,14 @@ function deleteProfile(idx) { } function edit(idx) { + headers = $("#headersTable").dataTable({ + destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy + columnDefs: [{ + orderable: false, + targets: "no-sort" + }] + }) + $("#modalSubmit").unbind('click').click(function() { save(idx) }) @@ -104,6 +129,9 @@ function edit(idx) { $("#username").val(profile.username) $("#password").val(profile.password) $("#ignore_cert_errors").prop("checked", profile.ignore_cert_errors) + $.each(profile.headers, function(i, record) { + addCustomHeader(record.key, record.value) + }); } } @@ -167,6 +195,34 @@ function load() { }) } +function addCustomHeader(header, value) { + // Create new data row. + var newRow = [ + escapeHtml(header), + escapeHtml(value), + '' + ]; + + // Check table to see if header already exists. + var headersTable = headers.DataTable(); + var existingRowIndex = headersTable + .column(0) // Email column has index of 2 + .data() + .indexOf(escapeHtml(header)); + + // Update or add new row as necessary. + if (existingRowIndex >= 0) { + headersTable + .row(existingRowIndex, { + order: "index" + }) + .data(newRow); + } else { + headersTable.row.add(newRow); + } + headersTable.draw(); +} + $(document).ready(function() { // Setup multiple modals // Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html @@ -208,5 +264,26 @@ $(document).ready(function() { $('#modal').on('hidden.bs.modal', function(event) { dismiss() }); + // Code to deal with custom email headers + $("#headersForm").on('submit', function() { + headerKey = $("#headerKey").val(); + headerValue = $("#headerValue").val(); + + if (headerKey == "" || headerValue == "") { + return false; + } + addCustomHeader(headerKey, headerValue); + // Reset user input. + $("#headersForm>div>input").val(''); + $("#headerKey").focus(); + return false; + }); + // Handle Deletion + $("#headersTable").on("click", "span>i.fa-trash-o", function() { + headers.DataTable() + .row($(this).parents('tr')) + .remove() + .draw(); + }); load() }) diff --git a/static/js/src/app/templates.js b/static/js/src/app/templates.js index 5c4e1255..d48ddc70 100644 --- a/static/js/src/app/templates.js +++ b/static/js/src/app/templates.js @@ -42,6 +42,7 @@ function save(idx) { type: target[4], }) }) + if (idx != -1) { template.id = templates[idx].id api.templateId.put(template) @@ -169,6 +170,7 @@ function edit(idx) { } else { $("#use_tracker_checkbox").prop("checked", false) } + } // Handle Deletion $("#attachmentsTable").unbind('click').on("click", "span>i.fa-trash-o", function() { @@ -346,4 +348,5 @@ $(document).ready(function() { dismiss() }); load() + }) diff --git a/templates/sending_profiles.html b/templates/sending_profiles.html index bdbb1ea3..a3ddbf21 100644 --- a/templates/sending_profiles.html +++ b/templates/sending_profiles.html @@ -48,7 +48,7 @@ Name - Interface Type + Interface Type Last Modified Date @@ -60,83 +60,105 @@