mirror of https://github.com/gophish/gophish
Initial work on reported emails
parent
3c74dd43e6
commit
d046da81a5
|
@ -0,0 +1,134 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
/*
|
||||
// ReportedEmailsSave handles requests for the /api/reportedemails/save endpoint
|
||||
func (as *Server) ReportedEmailsSave(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "POST" {
|
||||
em := models.ReportedEmail{}
|
||||
err := json.NewDecoder(r.Body).Decode(&em)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid email data."}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = models.SaveReportedEmail(&em)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Successfully saved reported email."}, http.StatusCreated)
|
||||
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
// ReportedEmailAttachment handles requests for the /api/reported/attachments endpoint
|
||||
func (as *Server) ReportedEmailAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
|
||||
att, err := models.GetReportedEmailAttachment(ctx.Get(r, "user_id").(int64), id)
|
||||
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
//JSONResponse(w, ems, http.StatusOK)
|
||||
data, err := base64.StdEncoding.DecodeString(att.Content)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", att.Header)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
|
||||
}
|
||||
|
||||
// ReportedEmails handles requests for the /api/reported endpoint
|
||||
func (as *Server) ReportedEmails(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
emailid := int64(-1)
|
||||
offset := int64(-1)
|
||||
limit := int64(-1)
|
||||
|
||||
if _, ok := vars["id"]; ok {
|
||||
emailid, _ = strconv.ParseInt(vars["id"], 0, 64)
|
||||
}
|
||||
|
||||
if _, ok := vars["range"]; ok {
|
||||
r := strings.Split(vars["range"], ",")
|
||||
offset, _ = strconv.ParseInt(r[0], 0, 64)
|
||||
limit, _ = strconv.ParseInt(r[1], 0, 64)
|
||||
}
|
||||
|
||||
switch {
|
||||
// GET: Return all emails
|
||||
case r.Method == "GET":
|
||||
|
||||
ems, err := models.GetReportedEmails(ctx.Get(r, "user_id").(int64), emailid, limit, offset)
|
||||
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, ems, http.StatusOK)
|
||||
|
||||
// PUT: Update an email
|
||||
case r.Method == "PUT":
|
||||
// Get existing email by id
|
||||
ems, err := models.GetReportedEmail(ctx.Get(r, "user_id").(int64), emailid)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(ems) > 0 {
|
||||
em := ems[0]
|
||||
err := json.NewDecoder(r.Body).Decode(&em)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid data"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = models.SaveReportedEmail(em)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Failed to update email"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Email record udpated"}, http.StatusCreated)
|
||||
} else {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Unable to locate email"}, http.StatusCreated)
|
||||
}
|
||||
case r.Method == "DELETE":
|
||||
ems, err := models.GetReportedEmail(ctx.Get(r, "user_id").(int64), emailid)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(ems) > 0 {
|
||||
err := models.DeleteReportedEmail(emailid)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Failed to delete email"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Email deleted"}, http.StatusCreated)
|
||||
} else {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Unable to locate email"}, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -76,6 +76,10 @@ func (as *Server) registerRoutes() {
|
|||
router.HandleFunc("/webhooks/", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
router.HandleFunc("/webhooks/{id:[0-9]+}/validate", mid.Use(as.ValidateWebhook, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
router.HandleFunc("/webhooks/{id:[0-9]+}", mid.Use(as.Webhook, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
router.HandleFunc("/reported/", as.ReportedEmails) // Return all reported emails
|
||||
router.HandleFunc("/reported/{id:[0-9]+}", as.ReportedEmails) // Fetch an individual email e.g /reported/3
|
||||
router.HandleFunc("/reported/{range:[0-9]+,[0-9]+}", as.ReportedEmails) // Fetch a range of emails e.g. /reported/0,10
|
||||
router.HandleFunc("/reported/attachment/{id:[0-9]+}", as.ReportedEmailAttachment) // Download attachment
|
||||
as.handler = router
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,11 @@ import (
|
|||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/NYTimes/gziphandler"
|
||||
|
@ -111,6 +113,9 @@ func (as *AdminServer) registerRoutes() {
|
|||
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
||||
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
router.HandleFunc("/webhooks", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
router.HandleFunc("/reported", mid.Use(as.Reported, mid.RequireLogin))
|
||||
router.HandleFunc("/reported/attachment/{id:[0-9]+}", mid.Use(as.ReportedEmailAttachment, mid.RequireLogin))
|
||||
|
||||
// Create the API routes
|
||||
api := api.NewServer(api.WithWorker(as.worker))
|
||||
router.PathPrefix("/api/").Handler(api)
|
||||
|
@ -246,6 +251,36 @@ func (as *AdminServer) Webhooks(w http.ResponseWriter, r *http.Request) {
|
|||
getTemplate(w, "webhooks").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// Reported handles the display of user reported emails that aren't Gophish campaigns
|
||||
func (as *AdminServer) Reported(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "Reported Emails"
|
||||
getTemplate(w, "reported").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// ReportedEmailAttachment retrieves an attachment by id
|
||||
func (as *AdminServer) ReportedEmailAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
user := ctx.Get(r, "user").(models.User)
|
||||
attID, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
|
||||
att, err := models.GetReportedEmailAttachment(user.Id, attID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
w.Write([]byte("Unable to query attachment"))
|
||||
} else {
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(att.Content)
|
||||
if err != nil {
|
||||
w.Write([]byte("Unable to load attachment"))
|
||||
} else {
|
||||
w.Header().Set("Content-Type", att.Header)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login handles the authentication flow for a user. If credentials are valid,
|
||||
// a session is created
|
||||
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
CREATE TABLE IF NOT EXISTS `reported` (id bigint primary key autoincrement, user_id integer ,reported_by_name varchar(255), reported_by_email varchar(255), reported_time datetime, reported_html varchar(255), reported_text varchar(255), reported_subject varchar(255),imap_uid varchar(255), status varchar(255), notes varchar(255));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `reported_attachments` (id bigint primary key autoincrement, rid bigint, filename varchar(255), header varchar(255), size bigint, content varchar(255));
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
DROP TABLE `reported`;
|
||||
DROP TABLE `reported_attachments`;
|
|
@ -0,0 +1,10 @@
|
|||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
CREATE TABLE IF NOT EXISTS "reported" ("id" integer primary key autoincrement, "user_id" integer ,"reported_by_name" varchar(255), "reported_by_enaio" varchar(255) "reported_time" datetime, "reported_html" varchar(255), "reported_text" varchar(255), "reported_subject" varchar(255),"imap_uid" varchar(255), "status" varchar(255), "notes" varchar(255));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "reported_attachments" ("id" integer primary key autoincrement, "rid" integer, "filename" varchar(255), "header" varchar(255), "size" integer, "content" varchar(255));
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
DROP TABLE "reported";
|
||||
DROP TABLE "reported_attachments";
|
|
@ -9,6 +9,8 @@ package imap
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -156,8 +158,33 @@ func checkForNewEmails(im models.IMAP) {
|
|||
log.Errorf("Error searching email for rids from user '%s': %s", m.Email.From, err.Error())
|
||||
} else {
|
||||
if len(rids) < 1 {
|
||||
// In the future this should be an alert in Gophish
|
||||
log.Infof("User '%s' reported email with subject '%s'. This is not a GoPhish campaign; you should investigate it.", m.Email.From, m.Email.Subject)
|
||||
|
||||
// Save reported email to the database
|
||||
atts := []*models.ReportedAttachment{}
|
||||
for _, a := range m.Attachments {
|
||||
na := &models.ReportedAttachment{Filename: a.Filename, Header: a.Header.Get("Content-Type"), Size: len(a.Content), Content: base64.StdEncoding.EncodeToString(a.Content)}
|
||||
atts = append(atts, na)
|
||||
}
|
||||
|
||||
e, err := mail.ParseAddress(m.Email.From)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
em := &models.ReportedEmail{
|
||||
UserId: im.UserId,
|
||||
ReportedByName: e.Name,
|
||||
ReportedByEmail: e.Address,
|
||||
ReportedHTML: string(m.HTML),
|
||||
ReportedText: string(m.Text),
|
||||
ReportedSubject: string(m.Subject),
|
||||
IMAPUID: -1, // https://github.com/emersion/go-imap/issues/353
|
||||
ReportedTime: time.Now().UTC(),
|
||||
Attachments: atts,
|
||||
Status: "Unknown"}
|
||||
|
||||
models.SaveReportedEmail(em)
|
||||
}
|
||||
for rid := range rids {
|
||||
log.Infof("User '%s' reported email with rid %s", m.Email.From, rid)
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
)
|
||||
|
||||
// ReportedEmail contains the attributes for non-campaign emails reported by users
|
||||
type ReportedEmail struct {
|
||||
//Id int64 `json:"id" gorm:"column:id; primary_key:yes;AUTO_INCREMENT"`
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"` // ID of the user account
|
||||
ReportedByName string `json:"reported_by_name"` // Email of the user reporting the email
|
||||
ReportedByEmail string `json:"reported_by_email"` // Email of the user reporting the email
|
||||
ReportedTime time.Time `json:"reported_time"` // When the email was reported
|
||||
ReportedHTML string `json:"reported_html"`
|
||||
ReportedText string `json:"reported_text"`
|
||||
ReportedSubject string `json:"reported_subject"`
|
||||
|
||||
/*EmailFrom string `json:"email_from"`
|
||||
EmailTo string `json:"email_to"`
|
||||
EmailCC string `json:"email_cc"`
|
||||
EmailSubject string `json:"email_subject"`
|
||||
EmailTime string `json:"email_time"`
|
||||
EmailText string `json:"email_text"`
|
||||
EmailHTML string `json:"email_html"`
|
||||
EmailHeaders string `json:"email_headers"`
|
||||
EmailBlob string `json:"email_blob"`*/
|
||||
|
||||
IMAPUID int64 `json:"imap_uid"`
|
||||
Status string `json:"status"`
|
||||
Notes string `json:"notes"` // Free form notes for operator to give additional info
|
||||
|
||||
Attachments []*ReportedAttachment `json:"attachments" gorm:"foreignkey:Rid"`
|
||||
}
|
||||
|
||||
//Todo: Add Enabled boolean, and attachments option
|
||||
|
||||
// ReportedEmailAttachment contains email attachments
|
||||
type ReportedAttachment struct {
|
||||
Rid int64 `json:"-"` // Foreign key
|
||||
Id int64 `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
Header string `json:"header"`
|
||||
Size int `json:"size"` // File size in bytes
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// TableName specifies the database tablename for Gorm to use
|
||||
func (em ReportedEmail) TableName() string {
|
||||
return "reported"
|
||||
}
|
||||
|
||||
// GetReportedEmailAttachment gets an attachment
|
||||
func GetReportedEmailAttachment(uid, id int64) (ReportedAttachment, error) {
|
||||
|
||||
att := ReportedAttachment{}
|
||||
|
||||
err := db.Debug().Table("reported_attachments").Select("reported_attachments.filename, reported_attachments.header, reported_attachments.content").Joins("left join reported on reported.id = reported_attachments.rid").Where("reported.user_id=? AND reported_attachments.id=?", uid, id).Take(&att).Error
|
||||
|
||||
return att, err
|
||||
}
|
||||
|
||||
// GetReportedEmails gets reported emails
|
||||
func GetReportedEmails(uid, emailid, limit, offset int64) ([]*ReportedEmail, error) {
|
||||
|
||||
ems := []*ReportedEmail{}
|
||||
var err error
|
||||
|
||||
// We have three conditions; fetch all email, fetch one email by id, or fetch a subset of emails by limit and offset
|
||||
if emailid == -1 {
|
||||
if offset == -1 {
|
||||
err = db.Debug().Preload("Attachments").Where("user_id=?", uid).Find(&ems).Error
|
||||
} else {
|
||||
err = db.Preload("Attachments").Where("user_id=?", uid).Order("ReportedTime", true).Offset(offset).Limit(limit).Find(&ems).Error
|
||||
}
|
||||
} else {
|
||||
err = db.Preload("Attachments").Where("user_id=? AND id=?", uid, emailid).Find(&ems).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
// Remove attachmetns and HTML/plaintext content for bulk requests. TODO: Don't retrieve these in the first place. Maybe with joins.
|
||||
if emailid == -1 {
|
||||
for _, e := range ems {
|
||||
e.ReportedHTML = ""
|
||||
e.ReportedText = ""
|
||||
if len(e.Attachments) > 0 { // Remove attachment content, but leave other details (filename, size, header)
|
||||
for _, a := range e.Attachments {
|
||||
a.Content = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse order so newest emails are first. Could not figure out how to add ORDER to the Preload() in the queries
|
||||
for i, j := 0, len(ems)-1; i < j; i, j = i+1, j-1 {
|
||||
ems[i], ems[j] = ems[j], ems[i]
|
||||
}
|
||||
return ems, err
|
||||
}
|
||||
|
||||
// GetReportedEmail gets a single reported emails
|
||||
func GetReportedEmail(uid, emailid int64) ([]*ReportedEmail, error) {
|
||||
|
||||
ems := []*ReportedEmail{}
|
||||
err := db.Preload("Attachments").Where("user_id=? AND id=?", uid, emailid).Find(&ems).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
return ems, err
|
||||
}
|
||||
|
||||
// SaveReportedEmail updates IMAP settings for a user in the database.
|
||||
func SaveReportedEmail(em *ReportedEmail) error {
|
||||
|
||||
// Insert into the DB
|
||||
err := db.Save(em).Error
|
||||
if err != nil {
|
||||
log.Error("Unable to save to database: ", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteReportedEmail deletes
|
||||
func DeleteReportedEmail(id int64) error {
|
||||
err := db.Where("id=?", id).Delete(&ReportedEmail{}).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -277,6 +277,27 @@ var api = {
|
|||
return query("/webhooks/" + id + "/validate", "POST", {}, true)
|
||||
},
|
||||
},
|
||||
// report handles (non-campaign) reported emails
|
||||
reported: {
|
||||
get: function() {
|
||||
return query("/reported/", "GET", {}, !1)
|
||||
},
|
||||
getone: function(id) {
|
||||
return query("/reported/" + id, "GET", {}, true)
|
||||
},
|
||||
//post: function(email) {
|
||||
// return query("/reported/" + email.id, "POST", email, true)
|
||||
//},
|
||||
put: function (email) {
|
||||
return query("/reported/" + email.id, "PUT", email, true)
|
||||
},
|
||||
delete: function(id) {
|
||||
return query("/reported/" + id, "DELETE", {}, false)
|
||||
},
|
||||
//update: function(e) {
|
||||
// return query("/reported/update", "POST", e, true)
|
||||
//}
|
||||
},
|
||||
// import handles all of the "import" functions in the api
|
||||
import_email: function (req) {
|
||||
return query("/import/email", "POST", req, false)
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
let emails = []
|
||||
let notes = {}
|
||||
|
||||
let statusBtnClass = {
|
||||
"Unknown" : "btn btn-warning dropdown-toggle statusbtn",
|
||||
"Safe" : "btn btn-success dropdown-toggle statusbtn",
|
||||
"Harmful" : "btn btn-danger dropdown-toggle statusbtn"
|
||||
}
|
||||
|
||||
const load = () => {
|
||||
$("#reportedTable").hide()
|
||||
$("#loading").show()
|
||||
api.reported.get()
|
||||
.success((em) => {
|
||||
emails = em
|
||||
$("#loading").hide()
|
||||
$("#reportedTable").show()
|
||||
let reportedTable = $("#reportedTable").DataTable({
|
||||
destroy: true,
|
||||
"aaSorting": [], // Disable default sort
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
}]
|
||||
});
|
||||
reportedTable.clear();
|
||||
$.each(emails, (i, email) => {
|
||||
|
||||
statusBtn = '<div class="dropdown">\
|
||||
<button id="btnstatus-'+email.id+'" class="' + statusBtnClass[email.status] + '" data-toggle="dropdown">' + email.status + '</button>\
|
||||
<ul class="dropdown-menu" id="' + email.id + '">\
|
||||
<li><a onclick="updateStatus(' + email.id + ', \'Safe\')" href="#">Safe</a></li>\
|
||||
<li><a onclick="updateStatus(' + email.id + ', \'Harmful\')" href="#">Harmful</a></li>\
|
||||
<li><a onclick="updateStatus(' + email.id + ', \'Unknown\')" href="#">Unknown</a></li>\
|
||||
<li class="divider"></li>\
|
||||
<li><a onclick="loadNotes(' + email.id + ')" class="notes" href="#">Notes</a></li>\
|
||||
</ul>\
|
||||
</div>'
|
||||
|
||||
notes[email.id] = email.notes
|
||||
|
||||
attHTML =
|
||||
"<div class='dropdown'>\
|
||||
<button class='btn btn-primary dropdown-toggle' type='button' data-toggle='dropdown'>" + email.attachments.length + " files\
|
||||
<span class='caret'></span></button>"
|
||||
|
||||
if (email.attachments.length > 0 ){
|
||||
attHTML += "<ul class='dropdown-menu'>"
|
||||
$.each(email.attachments, function(i, a){
|
||||
attHTML += "<li id="+ a.id +"><a href=\"/reported/attachment/\" onclick=\"window.open('/reported/attachment/" + a.id + "', 'newwindow', 'width=640, height=480'); return false;\">" + a.filename + " ("+ humanFileSize(a.size) +")</a></li>"
|
||||
});
|
||||
attHTML += "</ul>"
|
||||
|
||||
}
|
||||
attHTML += "</div>"
|
||||
|
||||
subj = escapeHtml(email.reported_subject)
|
||||
if (subj.length > 24) {
|
||||
subj = subj.substring(0, 24) + "..."
|
||||
}
|
||||
|
||||
reportedTable.row.add([
|
||||
"<span data-toggle='tooltip' title='" + escapeHtml(email.reported_by_name) + "'> " + escapeHtml(email.reported_by_email) + "</span>",
|
||||
//escapeHtml(email.reported_subject.substring(0, 24) + "..."),
|
||||
subj,
|
||||
moment.utc(email.reported_time).fromNow(),
|
||||
attHTML,
|
||||
statusBtn,
|
||||
"<div class=''><button class='btn btn-primary edit_button' onclick='viewEmail(" + email.id + ")' data-backdrop='static' data-user-id='" + email.id + "'>\
|
||||
<i class='fa fa-eye'></i>\
|
||||
</button>\
|
||||
<button class='btn btn-danger delete_button' onclick='deleteEmail(" + email.id + ")' data-user-id='" + email.id + "'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
})
|
||||
})
|
||||
.error(() => {
|
||||
errorFlash("Error fetching reported emails")
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
load()
|
||||
window.setInterval(function(){ //Refresh every 10 seconds
|
||||
load()
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
function updateStatus(emailID, newstatus){
|
||||
|
||||
// Update button
|
||||
btn = $("#btnstatus-" + emailID)
|
||||
btn.attr('class', statusBtnClass[newstatus])
|
||||
btn.text(newstatus)
|
||||
btn.val(newstatus)
|
||||
|
||||
//Update server side value
|
||||
email = {
|
||||
id: parseInt(emailID),
|
||||
status: newstatus
|
||||
}
|
||||
|
||||
api.reported.put(email)
|
||||
.error(function (data) {
|
||||
Swal.fire({
|
||||
type: 'error',
|
||||
title: data.responseJSON.message
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function loadNotes(emailID){
|
||||
|
||||
email = {
|
||||
id: parseInt(emailID),
|
||||
}
|
||||
|
||||
$("#notes").val(notes[emailID]);
|
||||
$("#notes-emailid").val(emailID);
|
||||
$("#modal\\.flashes").empty()
|
||||
$('#modal').modal('show');
|
||||
|
||||
}
|
||||
|
||||
function deleteEmail(id) {
|
||||
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will delete the email from here, but not from your mail server.",
|
||||
type: "warning",
|
||||
animation: false,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Delete",
|
||||
confirmButtonColor: "#428bca",
|
||||
reverseButtons: true,
|
||||
allowOutsideClick: false,
|
||||
preConfirm: function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
api.reported.delete(id)
|
||||
.success((msg) => {
|
||||
resolve()
|
||||
})
|
||||
.error((data) => {
|
||||
reject(data.responseJSON.message)
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
Swal.showValidationMessage(error)
|
||||
})
|
||||
}
|
||||
}).then(function (result) {
|
||||
load()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function viewEmail(id){
|
||||
|
||||
$("#modal-email\\.flashes").empty()
|
||||
$('#modal-email').modal('show');
|
||||
|
||||
api.reported.getone(id)
|
||||
.success((em) => {
|
||||
|
||||
if (em.length > 0 ) { // Should always be one, but safe to check.
|
||||
rtext = em[0].reported_text.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
||||
rhtml = em[0].reported_html
|
||||
|
||||
//$("#email-plaintext").attr('value', btoa(rtext))
|
||||
//$("#email-html").attr('value', btoa(rhtml))
|
||||
$("#email-plaintext").data("value", rtext)
|
||||
$("#email-html").data("value", rhtml)
|
||||
|
||||
|
||||
$("#email-body").attr("srcdoc", rtext); // Load plaintext by default
|
||||
} else {
|
||||
modalError("Error loading email")
|
||||
}
|
||||
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError("Error loading email: " + data.responseJSON.message)
|
||||
})
|
||||
|
||||
|
||||
};
|
||||
|
||||
function viewplaintext() {
|
||||
rtext = $("#email-plaintext").data("value")
|
||||
$("#email-body").attr("srcdoc", rtext);
|
||||
}
|
||||
|
||||
function viewhtml() {
|
||||
rhtml = $("#email-html").data("value")
|
||||
$("#email-body").attr("srcdoc", rhtml);
|
||||
}
|
||||
|
||||
$("#modalSubmit").unbind('click').click(() => {
|
||||
|
||||
emailID = $("#notes-emailid").attr('value')
|
||||
newnotes = $("#notes").val()
|
||||
notes[emailID] = newnotes
|
||||
|
||||
email = {
|
||||
"id": parseInt(emailID),
|
||||
"notes": notes[emailID]
|
||||
}
|
||||
|
||||
api.reported.put(email)
|
||||
.success(function (data) {
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError("Error saving notes: " + data.responseJSON.message)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
// Convert attachment byte file size to human readable format
|
||||
function humanFileSize(bytes, si=true, dp=0) {
|
||||
const thresh = si ? 1000 : 1024;
|
||||
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
|
||||
const units = si
|
||||
? ['kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
let u = -1;
|
||||
const r = 10**dp;
|
||||
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||
|
||||
|
||||
return bytes.toFixed(dp) + ' ' + units[u];
|
||||
}
|
|
@ -20,6 +20,9 @@
|
|||
<li>
|
||||
<a href="/sending_profiles">Sending Profiles</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/reported">Reported Emails</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings">Account Settings</span></a>
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
{{define "body"}}
|
||||
|
||||
<style>
|
||||
.statusbtn {
|
||||
width: 90px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||
<div class="row">
|
||||
<h1 class="page-header">
|
||||
{{.Title}}
|
||||
</h1>
|
||||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
<div id="loading">
|
||||
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
||||
</div>
|
||||
|
||||
<div id="emptyMessage" class="row" style="display:none;">
|
||||
<div class="alert alert-info">
|
||||
<p>Suspicious emails that your users have reported will be available here.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<table id="reportedTable" class="table" style="display:none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reported by</th>
|
||||
<th>Subject</th>
|
||||
<th>When reported</th>
|
||||
<th>Attachments</th>
|
||||
<th>Status</th>
|
||||
<th class="col-md-2 no-sort">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Modal -->
|
||||
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<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="groupModalLabel">Notes</h4>
|
||||
</div>
|
||||
<div class="modal-body" id="modal_body">
|
||||
<div class="row" id="modal.flashes"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<textarea rows="10" id="notes" class="gophish-editor form-control" placeholder="Enter notes here"></textarea>
|
||||
</div>
|
||||
<input type="hidden" id="notes-emailid" value="">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Notes Modal -->
|
||||
|
||||
<!-- Email viewer Modal -->
|
||||
<div class="modal fade" id="modal-email" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<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="groupModalLabel">Reported email:</h4>
|
||||
</div>
|
||||
<div class="modal-body" id="modal_body">
|
||||
<div class="row" id="modal.flashes"></div>
|
||||
|
||||
<!--<div id="email-body"></div>-->
|
||||
<!--<iframe id="email-body" sandbox></iframe>-->
|
||||
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<iframe id="email-body" class="embed-responsive-item" sandbox></iframe>
|
||||
</div>
|
||||
<input type="hidden" id="email-html" value="">
|
||||
<input type="hidden" id="email-plaintext" value="">
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="viewplaintext()" class="btn btn-primary">View plaintext</button>
|
||||
<button type="button" onclick="viewhtml()" class="btn btn-primary">View HTML</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Email viewer Modal -->
|
||||
|
||||
{{end}} {{define "scripts"}}
|
||||
<!-- <script src="/js/dist/app/reported.min.js"></script> -->
|
||||
<!-- <script src="https://raw.githubusercontent.com/HubSpot/humanize/master/dist/humanize.min.js"></script> -->
|
||||
<script src="/js/src/app/reported.js"></script>
|
||||
{{end}}
|
Loading…
Reference in New Issue