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/", 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]+}/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("/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
|
as.handler = router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,11 @@ import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/NYTimes/gziphandler"
|
"github.com/NYTimes/gziphandler"
|
||||||
|
@ -111,6 +113,9 @@ func (as *AdminServer) registerRoutes() {
|
||||||
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
||||||
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), 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("/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
|
// Create the API routes
|
||||||
api := api.NewServer(api.WithWorker(as.worker))
|
api := api.NewServer(api.WithWorker(as.worker))
|
||||||
router.PathPrefix("/api/").Handler(api)
|
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)
|
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,
|
// Login handles the authentication flow for a user. If credentials are valid,
|
||||||
// a session is created
|
// a session is created
|
||||||
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"net/mail"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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())
|
log.Errorf("Error searching email for rids from user '%s': %s", m.Email.From, err.Error())
|
||||||
} else {
|
} else {
|
||||||
if len(rids) < 1 {
|
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)
|
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 {
|
for rid := range rids {
|
||||||
log.Infof("User '%s' reported email with rid %s", m.Email.From, rid)
|
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)
|
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 handles all of the "import" functions in the api
|
||||||
import_email: function (req) {
|
import_email: function (req) {
|
||||||
return query("/import/email", "POST", req, false)
|
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>
|
<li>
|
||||||
<a href="/sending_profiles">Sending Profiles</a>
|
<a href="/sending_profiles">Sending Profiles</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/reported">Reported Emails</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/settings">Account Settings</span></a>
|
<a href="/settings">Account Settings</span></a>
|
||||||
</li>
|
</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